# Gitea Integration Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Allow users to create Gitea branches from tickets, and view branches/commits/PRs/CI status linked to tickets — all fetched on-demand from the Gitea API. **Architecture:** Backend adds a `GiteaConfiguration` singleton entity (encrypted token), extends `Project` with `gitea_owner`/`gitea_repo` fields, and provides a `GiteaApiService` that wraps Gitea REST API calls. Custom API Platform endpoints expose config management (admin) and task-level git info. Frontend adds an admin tab for Gitea config, repo selection in ProjectDrawer, and a `TaskGitSection` component in TaskModal. **Tech Stack:** PHP 8.4, Symfony 8, API Platform 4, Doctrine ORM, Nuxt 4, Vue 3, TypeScript, Tailwind CSS **Spec:** `docs/superpowers/specs/2026-03-13-gitea-integration-design.md` --- ## Chunk 1: Backend — Entity & Migration ### Task 1: GiteaConfiguration Entity **Files:** - Create: `src/Entity/GiteaConfiguration.php` - Create: `src/Repository/GiteaConfigurationRepository.php` - [ ] **Step 1: Create GiteaConfigurationRepository** ```php findOneBy([]); } } ``` - [ ] **Step 2: Create GiteaConfiguration entity** ```php id; } public function getUrl(): ?string { return $this->url; } public function setUrl(?string $url): static { $this->url = $url; return $this; } public function getEncryptedToken(): ?string { return $this->encryptedToken; } public function setEncryptedToken(?string $encryptedToken): static { $this->encryptedToken = $encryptedToken; return $this; } public function hasToken(): bool { return null !== $this->encryptedToken; } } ``` - [ ] **Step 3: Commit** ```bash git add src/Entity/GiteaConfiguration.php src/Repository/GiteaConfigurationRepository.php git commit -m "feat : add GiteaConfiguration entity with repository" ``` ### Task 2: Add gitea fields to Project entity **Files:** - Modify: `src/Entity/Project.php` - [ ] **Step 1: Add giteaOwner and giteaRepo fields to Project** Add after the `$client` property (around line 65): ```php #[ORM\Column(length: 255, nullable: true)] #[Groups(['project:read', 'project:write'])] private ?string $giteaOwner = null; #[ORM\Column(length: 255, nullable: true)] #[Groups(['project:read', 'project:write'])] private ?string $giteaRepo = null; ``` Add getters/setters at the end of the class: ```php public function getGiteaOwner(): ?string { return $this->giteaOwner; } public function setGiteaOwner(?string $giteaOwner): static { $this->giteaOwner = $giteaOwner; return $this; } public function getGiteaRepo(): ?string { return $this->giteaRepo; } public function setGiteaRepo(?string $giteaRepo): static { $this->giteaRepo = $giteaRepo; return $this; } public function hasGiteaRepo(): bool { return null !== $this->giteaOwner && null !== $this->giteaRepo; } ``` - [ ] **Step 2: Commit** ```bash git add src/Entity/Project.php git commit -m "feat : add gitea owner/repo fields to Project entity" ``` ### Task 3: Generate and run migration **Files:** - Create: `migrations/VersionYYYYMMDDHHMMSS.php` (auto-generated) - [ ] **Step 1: Generate migration** ```bash make shell # Inside container: php bin/console doctrine:migrations:diff exit ``` - [ ] **Step 2: Run migration** ```bash make migration-migrate ``` - [ ] **Step 3: Commit** ```bash git add migrations/ git commit -m "feat : add migration for GiteaConfiguration and Project gitea fields" ``` --- ## Chunk 2: Backend — Token Encryption & GiteaApiService ### Task 4: Token encryption service **Files:** - Create: `src/Service/TokenEncryptor.php` - [ ] **Step 1: Add GITEA_ENCRYPTION_KEY to .env** Add to `.env`: ``` GITEA_ENCRYPTION_KEY= ``` - [ ] **Step 2: Create TokenEncryptor service** ```php key = sodium_hex2bin($encryptionKey); if (mb_strlen($this->key, '8bit') !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { throw new \InvalidArgumentException('GITEA_ENCRYPTION_KEY must be a valid sodium secret box key.'); } } public function encrypt(string $plaintext): string { $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $this->key); return sodium_bin2hex($nonce . $ciphertext); } public function decrypt(string $encrypted): string { $decoded = sodium_hex2bin($encrypted); $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); $plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->key); if (false === $plaintext) { throw new \RuntimeException('Failed to decrypt token.'); } return $plaintext; } } ``` - [ ] **Step 3: Commit** ```bash git add src/Service/TokenEncryptor.php .env git commit -m "feat : add TokenEncryptor service with sodium encryption" ``` ### Task 5: GiteaApiException **Files:** - Create: `src/Exception/GiteaApiException.php` - [ ] **Step 1: Create exception class** ```php slugger = new AsciiSlugger('fr'); } public function testConnection(): bool { try { $this->request('GET', '/api/v1/version'); return true; } catch (GiteaApiException) { return false; } } /** * @return array */ public function listRepositories(): array { $result = []; $page = 1; do { $data = $this->request('GET', '/api/v1/repos/search', [ 'query' => ['page' => $page, 'limit' => 50], ]); $result = array_merge($result, $data['data'] ?? []); ++$page; } while (!empty($data['data']) && count($data['data']) === 50); return $result; } public function getDefaultBranch(Project $project): string { $this->assertProjectHasRepo($project); $data = $this->request('GET', sprintf( '/api/v1/repos/%s/%s', $project->getGiteaOwner(), $project->getGiteaRepo(), )); return $data['default_branch'] ?? 'main'; } public function createBranch(Project $project, Task $task, string $type, string $baseBranch): string { $this->assertProjectHasRepo($project); $branchName = $this->generateBranchName($task, $type); $this->request('POST', sprintf( '/api/v1/repos/%s/%s/branches', $project->getGiteaOwner(), $project->getGiteaRepo(), ), [ 'json' => [ 'new_branch_name' => $branchName, 'old_branch_name' => $baseBranch, ], ]); return $branchName; } public function generateBranchName(Task $task, string $type): string { $project = $task->getProject(); if (null === $project) { throw new GiteaApiException('Task has no project.'); } $slug = $this->slugger->slug($task->getTitle())->lower()->truncate(50)->toString(); $slug = rtrim($slug, '-'); return sprintf('%s/%s-%d-%s', $type, $project->getCode(), $task->getNumber(), $slug); } /** * @return array */ public function listBranches(Project $project, string $taskCode): array { $this->assertProjectHasRepo($project); $allBranches = []; $page = 1; do { $pageBranches = $this->request('GET', sprintf( '/api/v1/repos/%s/%s/branches', $project->getGiteaOwner(), $project->getGiteaRepo(), ), [ 'query' => ['page' => $page, 'limit' => 50], ]); $allBranches = array_merge($allBranches, $pageBranches); ++$page; } while (!empty($pageBranches) && count($pageBranches) === 50); $regex = sprintf('#^[^/]+/%s($|-.+)#', preg_quote($taskCode, '#')); return array_values(array_filter($allBranches, static function (array $branch) use ($regex): bool { return 1 === preg_match($regex, $branch['name']); })); } /** * @return array */ public function listCommits(Project $project, string $branch): array { $this->assertProjectHasRepo($project); return $this->request('GET', sprintf( '/api/v1/repos/%s/%s/commits', $project->getGiteaOwner(), $project->getGiteaRepo(), ), [ 'query' => ['sha' => $branch, 'limit' => 30], ]); } /** * @return array */ public function listPullRequests(Project $project, string $taskCode): array { $this->assertProjectHasRepo($project); $branches = $this->listBranches($project, $taskCode); $prs = []; foreach ($branches as $branch) { $branchPrs = $this->request('GET', sprintf( '/api/v1/repos/%s/%s/pulls', $project->getGiteaOwner(), $project->getGiteaRepo(), ), [ 'query' => ['state' => 'all', 'head' => $branch['name']], ]); $prs = array_merge($prs, $branchPrs); } // Fetch CI status for each PR foreach ($prs as &$pr) { $sha = $pr['head']['sha'] ?? null; if (null !== $sha) { try { $pr['ci_statuses'] = $this->request('GET', sprintf( '/api/v1/repos/%s/%s/commits/%s/statuses', $project->getGiteaOwner(), $project->getGiteaRepo(), $sha, )); } catch (GiteaApiException) { $pr['ci_statuses'] = []; } } } return $prs; } private function getConfiguration(): GiteaConfiguration { $config = $this->configRepository->findSingleton(); if (null === $config) { throw new GiteaApiException('Gitea is not configured.'); } return $config; } private function getDecryptedToken(GiteaConfiguration $config): string { $encrypted = $config->getEncryptedToken(); if (null === $encrypted) { throw new GiteaApiException('Gitea token is not set.'); } return $this->tokenEncryptor->decrypt($encrypted); } private function assertProjectHasRepo(Project $project): void { if (!$project->hasGiteaRepo()) { throw new GiteaApiException('Project has no Gitea repository configured.'); } } /** * @param array $options */ private function request(string $method, string $path, array $options = []): array { $config = $this->getConfiguration(); $token = $this->getDecryptedToken($config); $options['headers'] = array_merge($options['headers'] ?? [], [ 'Authorization' => 'token ' . $token, 'Accept' => 'application/json', ]); $options['timeout'] = 10; try { $response = $this->httpClient->request($method, rtrim($config->getUrl(), '/') . $path, $options); return $response->toArray(); } catch (ExceptionInterface $e) { throw new GiteaApiException('Gitea API error: ' . $e->getMessage(), 0, $e); } } } ``` - [ ] **Step 2: Commit** ```bash git add src/Service/GiteaApiService.php git commit -m "feat : add GiteaApiService with branch/commit/PR methods" ``` --- ## Chunk 3: Backend — API Endpoints ### Task 7: Gitea Settings API Resource & Providers/Processors **Files:** - Create: `src/ApiResource/GiteaSettings.php` - Create: `src/State/GiteaSettingsProvider.php` - Create: `src/State/GiteaSettingsProcessor.php` - [ ] **Step 1: Create GiteaSettings API Resource** ```php ['gitea_settings:read']], provider: GiteaSettingsProvider::class, security: "is_granted('ROLE_ADMIN')", ), new Put( uriTemplate: '/settings/gitea', denormalizationContext: ['groups' => ['gitea_settings:write']], normalizationContext: ['groups' => ['gitea_settings:read']], provider: GiteaSettingsProvider::class, processor: GiteaSettingsProcessor::class, security: "is_granted('ROLE_ADMIN')", ), ], )] final class GiteaSettings { #[Groups(['gitea_settings:read', 'gitea_settings:write'])] public ?string $url = null; #[Groups(['gitea_settings:write'])] public ?string $token = null; #[Groups(['gitea_settings:read'])] public bool $hasToken = false; } ``` - [ ] **Step 2: Create GiteaSettingsProvider** ```php configRepository->findSingleton(); $dto = new GiteaSettings(); if (null !== $config) { $dto->url = $config->getUrl(); $dto->hasToken = $config->hasToken(); } return $dto; } } ``` - [ ] **Step 3: Create GiteaSettingsProcessor** ```php configRepository->findSingleton(); if (null === $config) { $config = new GiteaConfiguration(); } $config->setUrl($data->url); if (null !== $data->token && '' !== $data->token) { $config->setEncryptedToken($this->tokenEncryptor->encrypt($data->token)); } $this->em->persist($config); $this->em->flush(); $result = new GiteaSettings(); $result->url = $config->getUrl(); $result->hasToken = $config->hasToken(); return $result; } } ``` - [ ] **Step 4: Commit** ```bash git add src/ApiResource/GiteaSettings.php src/State/GiteaSettingsProvider.php src/State/GiteaSettingsProcessor.php git commit -m "feat : add Gitea settings API resource with provider/processor" ``` ### Task 8: Gitea Test Connection Endpoint **Files:** - Create: `src/ApiResource/GiteaTestConnection.php` - Create: `src/State/GiteaTestConnectionProvider.php` - [ ] **Step 1: Create GiteaTestConnection API Resource** ```php ['gitea_test:read']], provider: GiteaTestConnectionProvider::class, processor: GiteaTestConnectionProvider::class, security: "is_granted('ROLE_ADMIN')", ), ], )] final class GiteaTestConnection { #[Groups(['gitea_test:read'])] public bool $success = false; } ``` - [ ] **Step 2: Create GiteaTestConnectionProvider** ```php success = $this->giteaApiService->testConnection(); return $result; } } ``` - [ ] **Step 3: Commit** ```bash git add src/ApiResource/GiteaTestConnection.php src/State/GiteaTestConnectionProvider.php git commit -m "feat : add Gitea test connection endpoint" ``` ### Task 9: Gitea Repositories List Endpoint **Files:** - Create: `src/ApiResource/GiteaRepository.php` - Create: `src/State/GiteaRepositoryProvider.php` - [ ] **Step 1: Create GiteaRepository API Resource** ```php ['gitea_repo:read']], provider: GiteaRepositoryProvider::class, security: "is_granted('ROLE_ADMIN')", ), ], )] final class GiteaRepository { #[Groups(['gitea_repo:read'])] public string $fullName = ''; #[Groups(['gitea_repo:read'])] public string $name = ''; #[Groups(['gitea_repo:read'])] public string $owner = ''; } ``` - [ ] **Step 2: Create GiteaRepositoryProvider** ```php giteaApiService->listRepositories(); } catch (GiteaApiException) { return []; } return array_map(static function (array $repo): GiteaRepository { $dto = new GiteaRepository(); $dto->fullName = $repo['full_name'] ?? ''; $dto->name = $repo['name'] ?? ''; $dto->owner = $repo['owner']['login'] ?? ''; return $dto; }, $repos); } } ``` - [ ] **Step 3: Commit** ```bash git add src/ApiResource/GiteaRepository.php src/State/GiteaRepositoryProvider.php git commit -m "feat : add Gitea repositories list endpoint" ``` ### Task 10: Task Gitea Branches Endpoint **Files:** - Create: `src/ApiResource/GiteaBranch.php` - Create: `src/State/GiteaBranchProvider.php` - Create: `src/State/GiteaBranchProcessor.php` - [ ] **Step 1: Create GiteaBranch API Resource** ```php ['gitea_branch:read']], provider: GiteaBranchProvider::class, ), new Post( uriTemplate: '/tasks/{taskId}/gitea/branches', denormalizationContext: ['groups' => ['gitea_branch:write']], normalizationContext: ['groups' => ['gitea_branch:read']], provider: GiteaBranchProvider::class, processor: GiteaBranchProcessor::class, ), ], )] final class GiteaBranch { #[Groups(['gitea_branch:read'])] public string $name = ''; #[Groups(['gitea_branch:write'])] public string $type = 'feature'; #[Groups(['gitea_branch:write'])] public string $baseBranch = 'main'; /** * @var array */ #[Groups(['gitea_branch:read'])] public array $commits = []; } ``` - [ ] **Step 2: Create GiteaBranchProvider** ```php em->getRepository(Task::class)->find($uriVariables['taskId'] ?? 0); if (null === $task || null === $task->getProject()) { return []; } $project = $task->getProject(); if (!$project->hasGiteaRepo()) { return []; } $taskCode = $project->getCode() . '-' . $task->getNumber(); try { $branches = $this->giteaApiService->listBranches($project, $taskCode); } catch (GiteaApiException) { return []; } $result = []; foreach ($branches as $branch) { $dto = new GiteaBranch(); $dto->name = $branch['name']; try { $commits = $this->giteaApiService->listCommits($project, $branch['name']); $dto->commits = array_map(static fn(array $c): array => [ 'sha' => substr($c['sha'] ?? '', 0, 7), 'message' => $c['commit']['message'] ?? '', 'author' => $c['commit']['author']['name'] ?? '', 'date' => $c['commit']['author']['date'] ?? $c['created'] ?? '', ], $commits); } catch (GiteaApiException) { $dto->commits = []; } $result[] = $dto; } return $result; } } ``` - [ ] **Step 3: Create GiteaBranchProcessor** ```php em->getRepository(Task::class)->find($uriVariables['taskId'] ?? 0); if (null === $task || null === $task->getProject()) { throw new NotFoundHttpException('Task not found.'); } $project = $task->getProject(); if (!$project->hasGiteaRepo()) { throw new BadRequestHttpException('Project has no Gitea repository.'); } if (!in_array($data->type, self::ALLOWED_TYPES, true)) { throw new BadRequestHttpException('Invalid branch type.'); } try { $branchName = $this->giteaApiService->createBranch($project, $task, $data->type, $data->baseBranch); } catch (GiteaApiException $e) { throw new BadRequestHttpException($e->getMessage()); } $result = new GiteaBranch(); $result->name = $branchName; return $result; } } ``` - [ ] **Step 4: Commit** ```bash git add src/ApiResource/GiteaBranch.php src/State/GiteaBranchProvider.php src/State/GiteaBranchProcessor.php git commit -m "feat : add task Gitea branches endpoints (list + create)" ``` ### Task 11: Task Gitea Pull Requests Endpoint **Files:** - Create: `src/ApiResource/GiteaPullRequest.php` - Create: `src/State/GiteaPullRequestProvider.php` - [ ] **Step 1: Create GiteaPullRequest API Resource** ```php ['gitea_pr:read']], provider: GiteaPullRequestProvider::class, ), ], )] final class GiteaPullRequest { #[Groups(['gitea_pr:read'])] public int $number = 0; #[Groups(['gitea_pr:read'])] public string $title = ''; #[Groups(['gitea_pr:read'])] public string $state = ''; #[Groups(['gitea_pr:read'])] public bool $merged = false; #[Groups(['gitea_pr:read'])] public string $headBranch = ''; #[Groups(['gitea_pr:read'])] public string $author = ''; #[Groups(['gitea_pr:read'])] public string $url = ''; /** * @var array */ #[Groups(['gitea_pr:read'])] public array $ciStatuses = []; } ``` - [ ] **Step 2: Create GiteaPullRequestProvider** ```php em->getRepository(Task::class)->find($uriVariables['taskId'] ?? 0); if (null === $task || null === $task->getProject()) { return []; } $project = $task->getProject(); if (!$project->hasGiteaRepo()) { return []; } $taskCode = $project->getCode() . '-' . $task->getNumber(); try { $prs = $this->giteaApiService->listPullRequests($project, $taskCode); } catch (GiteaApiException) { return []; } return array_map(static function (array $pr): GiteaPullRequest { $dto = new GiteaPullRequest(); $dto->number = $pr['number'] ?? 0; $dto->title = $pr['title'] ?? ''; $dto->state = $pr['state'] ?? ''; $dto->merged = $pr['merged'] ?? false; $dto->headBranch = $pr['head']['ref'] ?? ''; $dto->author = $pr['user']['login'] ?? ''; $dto->url = $pr['html_url'] ?? ''; $dto->ciStatuses = array_map(static fn(array $s): array => [ 'context' => $s['context'] ?? '', 'status' => $s['status'] ?? '', 'target_url' => $s['target_url'] ?? '', ], $pr['ci_statuses'] ?? []); return $dto; }, $prs); } } ``` - [ ] **Step 3: Commit** ```bash git add src/ApiResource/GiteaPullRequest.php src/State/GiteaPullRequestProvider.php git commit -m "feat : add task Gitea pull requests endpoint" ``` ### Task 12: Branch Name Generation Endpoint **Files:** - Create: `src/ApiResource/GiteaBranchName.php` - Create: `src/State/GiteaBranchNameProvider.php` - [ ] **Step 1: Create GiteaBranchName API Resource** This endpoint generates the branch name without creating it (for the "copy" button). ```php ['gitea_branch_name:read']], provider: GiteaBranchNameProvider::class, ), ], )] final class GiteaBranchName { #[Groups(['gitea_branch_name:read'])] public string $name = ''; } ``` - [ ] **Step 2: Create GiteaBranchNameProvider** ```php em->getRepository(Task::class)->find($uriVariables['taskId'] ?? 0); if (null === $task) { throw new NotFoundHttpException('Task not found.'); } $type = $uriVariables['type'] ?? 'feature'; if (!in_array($type, self::ALLOWED_TYPES, true)) { throw new BadRequestHttpException('Invalid branch type.'); } $dto = new GiteaBranchName(); $dto->name = $this->giteaApiService->generateBranchName($task, $type); return $dto; } } ``` - [ ] **Step 3: Commit** ```bash git add src/ApiResource/GiteaBranchName.php src/State/GiteaBranchNameProvider.php git commit -m "feat : add branch name generation endpoint" ``` --- ## Chunk 4: Frontend — Service Layer & DTOs ### Task 13: Frontend Gitea DTOs **Files:** - Create: `frontend/services/dto/gitea.ts` - [ ] **Step 1: Create Gitea DTOs** ```typescript export type GiteaSettings = { url: string | null hasToken: boolean } export type GiteaSettingsWrite = { url: string | null token: string | null } export type GiteaRepository = { fullName: string name: string owner: string } export type GiteaBranch = { name: string commits: GiteaCommit[] } export type GiteaCommit = { sha: string message: string author: string date: string } export type GiteaBranchCreate = { type: string baseBranch: string } export type GiteaPullRequest = { number: number title: string state: string merged: boolean headBranch: string author: string url: string ciStatuses: GiteaCiStatus[] } export type GiteaCiStatus = { context: string status: string target_url: string } export type GiteaBranchName = { name: string } export type GiteaTestResult = { success: boolean } ``` - [ ] **Step 2: Commit** ```bash git add frontend/services/dto/gitea.ts git commit -m "feat : add Gitea TypeScript DTOs" ``` ### Task 14: Frontend Gitea Service **Files:** - Create: `frontend/services/gitea.ts` - [ ] **Step 1: Create Gitea service** ```typescript import type { GiteaSettings, GiteaSettingsWrite, GiteaRepository, GiteaBranch, GiteaBranchCreate, GiteaPullRequest, GiteaBranchName, GiteaTestResult, } from './dto/gitea' import type { HydraCollection } from '~/utils/api' import { extractHydraMembers } from '~/utils/api' export function useGiteaService() { const api = useApi() async function getSettings(): Promise { return api.get('/settings/gitea') } async function saveSettings(payload: GiteaSettingsWrite): Promise { return api.put('/settings/gitea', payload as Record, { toastSuccessKey: 'gitea.settings.saved', }) } async function testConnection(): Promise { return api.post('/settings/gitea/test') } async function listRepositories(): Promise { const data = await api.get>('/gitea/repositories') return extractHydraMembers(data) } async function listBranches(taskId: number): Promise { const data = await api.get>(`/tasks/${taskId}/gitea/branches`) return extractHydraMembers(data) } async function createBranch(taskId: number, payload: GiteaBranchCreate): Promise { return api.post(`/tasks/${taskId}/gitea/branches`, payload as Record, { toastSuccessKey: 'gitea.branch.created', }) } async function listPullRequests(taskId: number): Promise { const data = await api.get>(`/tasks/${taskId}/gitea/pull-requests`) return extractHydraMembers(data) } async function getBranchName(taskId: number, type: string): Promise { return api.get(`/tasks/${taskId}/gitea/branch-name/${type}`) } return { getSettings, saveSettings, testConnection, listRepositories, listBranches, createBranch, listPullRequests, getBranchName, } } ``` - [ ] **Step 2: Commit** ```bash git add frontend/services/gitea.ts git commit -m "feat : add Gitea frontend service" ``` ### Task 15: Add Project gitea fields to DTO **Files:** - Modify: `frontend/services/dto/project.ts` - [ ] **Step 1: Add giteaOwner and giteaRepo to Project type** Add to the `Project` type after `client`: ```typescript giteaOwner: string | null giteaRepo: string | null ``` Add to `ProjectWrite` type after `client`: ```typescript giteaOwner?: string | null giteaRepo?: string | null ``` - [ ] **Step 2: Commit** ```bash git add frontend/services/dto/project.ts git commit -m "feat : add gitea fields to Project DTO" ``` --- ## Chunk 5: Frontend — Admin Gitea Tab ### Task 16: i18n keys **Files:** - Modify: `frontend/i18n/locales/fr.json` - [ ] **Step 1: Add Gitea i18n keys** Add a `"gitea"` section to the JSON: ```json "common": { "cancel": "Annuler", "loading": "Chargement..." }, "gitea": { "settings": { "title": "Configuration Gitea", "url": "URL du serveur", "urlPlaceholder": "https://git.example.com", "token": "Token API", "tokenPlaceholder": "Entrez un nouveau token", "tokenConfigured": "Token configuré", "save": "Enregistrer", "saved": "Configuration Gitea sauvegardée.", "testConnection": "Tester la connexion", "testSuccess": "Connexion réussie.", "testFailed": "Connexion échouée." }, "branch": { "title": "Git", "create": "Créer une branche", "created": "Branche créée avec succès.", "copy": "Copier le nom", "copied": "Nom de branche copié.", "type": "Type", "baseBranch": "Branche de base", "preview": "Aperçu", "types": { "feature": "feature", "fix": "fix", "refactor": "refactor", "hotfix": "hotfix", "chore": "chore" }, "noBranches": "Aucune branche liée.", "commits": "Commits", "noCommits": "Aucun commit." }, "pr": { "title": "Pull Requests", "noPrs": "Aucune pull request.", "open": "Ouverte", "merged": "Mergée", "closed": "Fermée", "ci": "CI/CD" }, "error": "Erreur de connexion à Gitea.", "notConfigured": "Gitea non configuré pour ce projet." } ``` - [ ] **Step 2: Commit** ```bash git add frontend/i18n/locales/fr.json git commit -m "feat : add Gitea i18n keys" ``` ### Task 17: AdminGiteaTab component **Files:** - Create: `frontend/components/admin/AdminGiteaTab.vue` - [ ] **Step 1: Create AdminGiteaTab** ```vue ``` - [ ] **Step 2: Commit** ```bash git add frontend/components/admin/AdminGiteaTab.vue git commit -m "feat : add AdminGiteaTab component" ``` ### Task 18: Add Gitea tab to admin page **Files:** - Modify: `frontend/pages/admin.vue` - [ ] **Step 1: Add Gitea tab entry** Add to the `tabs` array: ```typescript { key: 'gitea', label: 'Gitea' }, ``` Add the type to `TabKey`: Update the type union to include `'gitea'`. Add the component in template: ```vue ``` - [ ] **Step 2: Commit** ```bash git add frontend/pages/admin.vue git commit -m "feat : add Gitea tab to admin page" ``` --- ## Chunk 6: Frontend — ProjectDrawer & TaskModal Integration ### Task 19: Add Gitea repo selector to ProjectDrawer **Files:** - Modify: `frontend/components/project/ProjectDrawer.vue` - [ ] **Step 1: Add Gitea repo selection** Add to the template, after the ColorPicker section (around line 34), before the submit button: ```vue
``` Add to script: ```typescript import { useGiteaService } from '~/services/gitea' import type { GiteaRepository } from '~/services/dto/gitea' const { listRepositories } = useGiteaService() const giteaRepos = ref([]) const giteaRepoOptions = computed(() => giteaRepos.value.map(r => ({ label: r.fullName, value: r.fullName })) ) ``` Add `giteaRepoFullName` to the form reactive: ```typescript giteaRepoFullName: null as string | null, ``` In the watch that populates the form, add: ```typescript form.giteaRepoFullName = props.project?.giteaOwner && props.project?.giteaRepo ? `${props.project.giteaOwner}/${props.project.giteaRepo}` : null ``` In `handleSubmit`, parse the fullName and add to payload: ```typescript if (form.giteaRepoFullName) { const [owner, repo] = form.giteaRepoFullName.split('/') payload.giteaOwner = owner payload.giteaRepo = repo } else { payload.giteaOwner = null payload.giteaRepo = null } ``` Load repos on mount: ```typescript onMounted(async () => { try { giteaRepos.value = await listRepositories() } catch { // Gitea not configured, ignore } }) ``` - [ ] **Step 2: Commit** ```bash git add frontend/components/project/ProjectDrawer.vue git commit -m "feat : add Gitea repo selector to ProjectDrawer" ``` ### Task 20: Create TaskGitSection component **Files:** - Create: `frontend/components/task/TaskGitSection.vue` - [ ] **Step 1: Create TaskGitSection** ```vue ``` - [ ] **Step 2: Commit** ```bash git add frontend/components/task/TaskGitSection.vue git commit -m "feat : add TaskGitSection component" ``` ### Task 21: Integrate TaskGitSection into TaskModal **Files:** - Modify: `frontend/components/task/TaskModal.vue` - [ ] **Step 1: Add TaskGitSection to TaskModal** Add the import in the script section: ```typescript import type { GiteaSettings } from '~/services/dto/gitea' import { useGiteaService } from '~/services/gitea' ``` Add reactive state: ```typescript const giteaUrl = ref('') const { getSettings: getGiteaSettings } = useGiteaService() ``` Add a computed to check if project has gitea configured: ```typescript const hasGitea = computed(() => { return !!props.task?.project?.giteaOwner && !!props.task?.project?.giteaRepo && !!giteaUrl.value }) ``` Load gitea URL on mount (only if project has gitea configured): ```typescript onMounted(async () => { if (props.task?.project?.giteaOwner && props.task?.project?.giteaRepo) { try { const settings = await getGiteaSettings() giteaUrl.value = settings.url ?? '' } catch { // Gitea not available } } }) ``` Add the component in the template, between the Description section and the Footer (after line 121, before line 123): ```vue ``` - [ ] **Step 2: Commit** ```bash git add frontend/components/task/TaskModal.vue git commit -m "feat : integrate TaskGitSection into TaskModal" ``` --- ## Chunk 7: Final Wiring & Verification ### Task 22: Generate encryption key and add to Docker env **Files:** - Modify: `docker/.env.docker.local` (NOT committed — secrets only) - Modify: `.env` - [ ] **Step 1: Generate a sodium key** ```bash php -r "echo sodium_bin2hex(sodium_crypto_secretbox_keygen());" ``` - [ ] **Step 2: Add generated key to `docker/.env.docker.local`** This file is a local override and must NOT be committed to version control: ``` GITEA_ENCRYPTION_KEY= ``` - [ ] **Step 3: Add placeholder to `.env` (committed)** ``` GITEA_ENCRYPTION_KEY= ``` Note: The `TokenEncryptor` will throw a clear error if this is empty, guiding the developer to set the real value. - [ ] **Step 4: Commit only `.env`** ```bash git add .env git commit -m "feat : add GITEA_ENCRYPTION_KEY placeholder to .env" ``` ### Task 23: Verify the full stack - [ ] **Step 1: Run migrations** ```bash make migration-migrate ``` - [ ] **Step 2: Start Nuxt dev server** ```bash make dev-nuxt ``` - [ ] **Step 3: Verify admin Gitea tab** Navigate to the admin page and verify the Gitea tab appears with URL/token fields and test button. - [ ] **Step 4: Verify ProjectDrawer** Open a project drawer and verify the Gitea repo selector appears (if Gitea is configured). - [ ] **Step 5: Verify TaskModal** Open a task modal for a project with a Gitea repo configured. Verify the Git section appears with create branch button and info display. - [ ] **Step 6: Fix PHP CS** ```bash make php-cs-fixer-allow-risky ``` - [ ] **Step 7: Final commit if needed** ```bash git add -A git commit -m "fix : PHP CS Fixer adjustments for Gitea integration" ```