# Phase 2a — Deploy & Available Versions Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Allow admins to list available Docker image tags from Gitea registry and deploy a chosen version to an environment, all from the Central UI. **Architecture:** Two new PHP services (GiteaRegistryService for tag listing via Docker Registry V2 API, DeployService for script execution via Symfony Process). Two new API Platform endpoints exposed via custom providers/processors. Frontend adds a deploy modal on the environment detail. Docker socket mounted for deploy script execution. **Tech Stack:** PHP 8.4, Symfony 8, API Platform 4, Symfony HttpClient, Symfony Process, Nuxt 4, Vue 3 --- ## File Structure ### Backend — Create | File | Responsibility | |------|---------------| | `src/Service/GiteaRegistryService.php` | HTTP client for Gitea Docker Registry V2 API | | `src/Service/DeployService.php` | Execute deploy.sh via Symfony Process | | `src/ApiResource/TagList.php` | API Platform DTO for tag list response | | `src/ApiResource/DeployResult.php` | API Platform DTO for deploy result response | | `src/State/TagListProvider.php` | Provider that calls GiteaRegistryService | | `src/State/DeployProcessor.php` | Processor that calls DeployService | ### Backend — Modify | File | Change | |------|--------| | `.env` | Add GITEA_API_URL, GITEA_API_TOKEN | | `docker-compose.yml` | Mount Docker socket in dev | | `infra/prod/docker-compose.yml` | Mount Docker socket + deploy dirs in prod | | `infra/prod/Dockerfile` | Install docker-cli in prod image | ### Frontend — Create | File | Responsibility | |------|---------------| | `frontend/services/deploy.ts` | API calls for tags + deploy | | `frontend/services/dto/deploy.ts` | TypeScript types for Tag and DeployResult | ### Frontend — Modify | File | Change | |------|--------| | `frontend/pages/applications/[slug].vue` | Add deploy button + deploy modal | | `frontend/i18n/locales/fr.json` | Deploy translation keys | --- ## Task 1: Environment variables and Docker config **Files:** - Modify: `.env` - Modify: `docker-compose.yml` - Modify: `infra/prod/docker-compose.yml` - Modify: `infra/prod/Dockerfile` - [ ] **Step 1: Add env vars to `.env`** Add at the end of the file: ``` ###> gitea ### GITEA_API_URL=https://gitea.malio.fr GITEA_API_TOKEN=change_me_in_env_local ###< gitea ### ``` - [ ] **Step 2: Mount Docker socket in dev docker-compose.yml** In `docker-compose.yml`, add to the `php` service `volumes` list: ```yaml - /var/run/docker.sock:/var/run/docker.sock ``` - [ ] **Step 3: Mount Docker socket + deploy dirs in prod docker-compose** In `infra/prod/docker-compose.yml`, add to the `app` service `volumes` list: ```yaml - /var/run/docker.sock:/var/run/docker.sock - /var/www/sirh/deploy:/var/www/sirh/deploy:ro - /var/www/lesstime/deploy:/var/www/lesstime/deploy:ro - /var/www/inventory/deploy:/var/www/inventory/deploy:ro ``` - [ ] **Step 4: Install docker-cli in prod Dockerfile** In `infra/prod/Dockerfile`, in the Stage 3 (production) `apt-get install` line, add `docker.io` to the list: Replace: ```dockerfile RUN apt-get update && apt-get install -y \ libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \ nginx supervisor \ ``` With: ```dockerfile RUN apt-get update && apt-get install -y \ libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \ nginx supervisor docker.io \ ``` - [ ] **Step 5: Commit** ```bash git add .env docker-compose.yml infra/prod/docker-compose.yml infra/prod/Dockerfile git commit -m "feat : add Gitea env vars, mount Docker socket and deploy dirs" ``` --- ## Task 2: GiteaRegistryService **Files:** - Create: `src/Service/GiteaRegistryService.php` - [ ] **Step 1: Create the service** ```php */ public function listTags(string $registryImage): array { $parts = explode('/', $registryImage); if (\count($parts) < 3) { throw new \InvalidArgumentException(sprintf('Invalid registry image format: "%s". Expected "registry/owner/package".', $registryImage)); } $owner = $parts[1]; $package = implode('/', \array_slice($parts, 2)); $url = sprintf('%s/v2/%s/%s/tags/list', $this->giteaApiUrl, $owner, $package); $response = $this->httpClient->request('GET', $url, [ 'headers' => [ 'Authorization' => sprintf('token %s', $this->giteaApiToken), ], 'timeout' => 10, ]); $data = $response->toArray(); $tags = $data['tags'] ?? []; // Sort: versions (vX.Y.Z) first descending, then others usort($tags, function (string $a, string $b): int { $aIsVersion = str_starts_with($a, 'v'); $bIsVersion = str_starts_with($b, 'v'); if ($aIsVersion && $bIsVersion) { return version_compare(ltrim($b, 'v'), ltrim($a, 'v')); } if ($aIsVersion) { return -1; } if ($bIsVersion) { return 1; } return strcmp($a, $b); }); return $tags; } } ``` - [ ] **Step 2: Commit** ```bash git add src/Service/GiteaRegistryService.php git commit -m "feat : add GiteaRegistryService for listing container tags" ``` --- ## Task 3: DeployService **Files:** - Create: `src/Service/DeployService.php` - [ ] **Step 1: Create the service** ```php getDeployScriptPath(); if (null === $scriptPath || !file_exists($scriptPath)) { return [ 'success' => false, 'output' => sprintf('Deploy script not found: %s', $scriptPath ?? 'null'), 'exitCode' => 1, ]; } $process = new Process( ['bash', $scriptPath, $tag], dirname($scriptPath), ); $process->setTimeout(300); $process->run(); return [ 'success' => $process->isSuccessful(), 'output' => $process->getOutput() . $process->getErrorOutput(), 'exitCode' => $process->getExitCode() ?? 1, ]; } } ``` - [ ] **Step 2: Commit** ```bash git add src/Service/DeployService.php git commit -m "feat : add DeployService for executing deploy scripts" ``` --- ## Task 4: API Platform DTOs **Files:** - Create: `src/ApiResource/TagList.php` - Create: `src/ApiResource/DeployResult.php` - [ ] **Step 1: Create TagList DTO** ```php */ public array $tags = []; } ``` - [ ] **Step 2: Create DeployResult DTO** ```php applicationRepository->findOneBy(['slug' => $slug]); if (null === $application) { throw new NotFoundHttpException(sprintf('Application "%s" not found.', $slug)); } $dto = new TagList(); $dto->tags = $this->giteaRegistryService->listTags($application->getRegistryImage()); return $dto; } } ``` - [ ] **Step 2: Verify endpoint works** ```bash # Login docker exec -t php-central-fpm curl -s -c /tmp/cookies -X POST http://nginx/api/login_check \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin"}' # List tags (will fail if GITEA_API_TOKEN is not set in .env.local, that's expected in dev) docker exec -t php-central-fpm curl -s -b /tmp/cookies http://nginx/api/applications/sirh/tags ``` - [ ] **Step 3: Commit** ```bash git add src/State/TagListProvider.php git commit -m "feat : add TagListProvider for listing registry tags" ``` --- ## Task 6: DeployProcessor **Files:** - Create: `src/State/DeployProcessor.php` - [ ] **Step 1: Create the processor** ```php environmentRepository->find($id) : null; if (null === $environment) { throw new NotFoundHttpException(sprintf('Environment "%s" not found.', $id)); } $requestData = $context['request']?->toArray() ?? []; $tag = $requestData['tag'] ?? null; if (null === $tag || '' === $tag) { throw new BadRequestHttpException('The "tag" field is required.'); } $result = $this->deployService->deploy($environment, $tag); $dto = new DeployResult(); $dto->success = $result['success']; $dto->output = $result['output']; $dto->tag = $tag; return $dto; } } ``` - [ ] **Step 2: Commit** ```bash git add src/State/DeployProcessor.php git commit -m "feat : add DeployProcessor for triggering deployments" ``` --- ## Task 7: Frontend types and service **Files:** - Create: `frontend/services/dto/deploy.ts` - Create: `frontend/services/deploy.ts` - [ ] **Step 1: Create deploy types** ```typescript type Tag = { name: string } type TagListResponse = { tags: string[] } type DeployResult = { success: boolean output: string tag: string } ``` - [ ] **Step 2: Create deploy service** ```typescript import type { TagListResponse, DeployResult } from './dto/deploy' export function getAvailableTags(slug: string): Promise { return useApi().get(`/applications/${slug}/tags`, undefined, { toast: false, }) } export function deploy(envId: number, tag: string): Promise { return useApi().post(`/environments/${envId}/deploy`, { tag }, { toast: false, }) } ``` - [ ] **Step 3: Commit** ```bash git add frontend/services/dto/deploy.ts frontend/services/deploy.ts git commit -m "feat : add frontend deploy types and service" ``` --- ## Task 8: i18n translations **Files:** - Modify: `frontend/i18n/locales/fr.json` - [ ] **Step 1: Add deploy translations** Add the following keys to the existing `environments` section in `fr.json`: ```json { "environments": { "...existing keys...", "deploy": { "button": "Deployer", "title": "Deployer une version", "selectTag": "Version a deployer", "selectPlaceholder": "Selectionner une version", "loadingTags": "Chargement des versions...", "noTags": "Aucune version disponible", "confirm": "Deployer", "deploying": "Deploiement en cours...", "success": "Deploiement reussi", "error": "Echec du deploiement", "output": "Sortie du deploiement" } } } ``` Also add error keys: ```json { "errors": { "...existing keys...", "deploy": { "tags": "Erreur lors du chargement des versions", "deploy": "Erreur lors du deploiement" } } } ``` - [ ] **Step 2: Commit** ```bash git add frontend/i18n/locales/fr.json git commit -m "feat : add i18n translations for deploy feature" ``` --- ## Task 9: Deploy modal and button on detail page **Files:** - Modify: `frontend/pages/applications/[slug].vue` - [ ] **Step 1: Add imports and deploy state** At the top of `