docs : plan d'implementation phase 2a deploy et versions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
800
docs/superpowers/plans/2026-04-06-phase2a-deploy-versions.md
Normal file
800
docs/superpowers/plans/2026-04-06-phase2a-deploy-versions.md
Normal file
@@ -0,0 +1,800 @@
|
||||
# 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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
final readonly class GiteaRegistryService
|
||||
{
|
||||
public function __construct(
|
||||
private HttpClientInterface $httpClient,
|
||||
#[Autowire('%env(GITEA_API_URL)%')]
|
||||
private string $giteaApiUrl,
|
||||
#[Autowire('%env(GITEA_API_TOKEN)%')]
|
||||
private string $giteaApiToken,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* List available tags for a container image.
|
||||
*
|
||||
* @param string $registryImage e.g. "gitea.malio.fr/malio-dev/sirh"
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Environment;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
final class DeployService
|
||||
{
|
||||
/**
|
||||
* @return array{success: bool, output: string, exitCode: int}
|
||||
*/
|
||||
public function deploy(Environment $environment, string $tag): array
|
||||
{
|
||||
$scriptPath = $environment->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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\ApiResource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use App\State\TagListProvider;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/applications/{slug}/tags',
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
provider: TagListProvider::class,
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class TagList
|
||||
{
|
||||
/** @var list<string> */
|
||||
public array $tags = [];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create DeployResult DTO**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\ApiResource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\State\DeployProcessor;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Post(
|
||||
uriTemplate: '/environments/{id}/deploy',
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
processor: DeployProcessor::class,
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class DeployResult
|
||||
{
|
||||
public bool $success = false;
|
||||
public string $output = '';
|
||||
public string $tag = '';
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ApiResource/TagList.php src/ApiResource/DeployResult.php
|
||||
git commit -m "feat : add TagList and DeployResult API Platform DTOs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: TagListProvider
|
||||
|
||||
**Files:**
|
||||
- Create: `src/State/TagListProvider.php`
|
||||
|
||||
- [ ] **Step 1: Create the provider**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\TagList;
|
||||
use App\Repository\ApplicationRepository;
|
||||
use App\Service\GiteaRegistryService;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final readonly class TagListProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ApplicationRepository $applicationRepository,
|
||||
private GiteaRegistryService $giteaRegistryService,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): TagList
|
||||
{
|
||||
$slug = $uriVariables['slug'] ?? '';
|
||||
$application = $this->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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\ApiResource\DeployResult;
|
||||
use App\Repository\EnvironmentRepository;
|
||||
use App\Service\DeployService;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final readonly class DeployProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EnvironmentRepository $environmentRepository,
|
||||
private DeployService $deployService,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): DeployResult
|
||||
{
|
||||
$id = $uriVariables['id'] ?? null;
|
||||
$environment = $id ? $this->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<TagListResponse> {
|
||||
return useApi().get<TagListResponse>(`/applications/${slug}/tags`, undefined, {
|
||||
toast: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function deploy(envId: number, tag: string): Promise<DeployResult> {
|
||||
return useApi().post<DeployResult>(`/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 `<script setup>`, add the imports after existing ones:
|
||||
|
||||
```typescript
|
||||
import type { DeployResult } from '~/services/dto/deploy'
|
||||
import { getAvailableTags, deploy } from '~/services/deploy'
|
||||
```
|
||||
|
||||
Add state variables after the existing ones (after `pendingMaintenanceByEnvId`):
|
||||
|
||||
```typescript
|
||||
// Deploy modal
|
||||
const showDeployModal = ref(false)
|
||||
const deployEnvId = ref<number | null>(null)
|
||||
const deployTags = ref<string[]>([])
|
||||
const selectedTag = ref('')
|
||||
const loadingTags = ref(false)
|
||||
const isDeploying = ref(false)
|
||||
const deployResult = ref<DeployResult | null>(null)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add deploy functions**
|
||||
|
||||
Add these functions after `removeLogFile`:
|
||||
|
||||
```typescript
|
||||
async function openDeployModal(env: Environment) {
|
||||
deployEnvId.value = env.id!
|
||||
selectedTag.value = ''
|
||||
deployResult.value = null
|
||||
deployTags.value = []
|
||||
showDeployModal.value = true
|
||||
loadingTags.value = true
|
||||
try {
|
||||
const response = await getAvailableTags(slug)
|
||||
deployTags.value = response.tags ?? []
|
||||
if (deployTags.value.length > 0) {
|
||||
selectedTag.value = deployTags.value[0]
|
||||
}
|
||||
} finally {
|
||||
loadingTags.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeploy() {
|
||||
if (!deployEnvId.value || !selectedTag.value) return
|
||||
isDeploying.value = true
|
||||
deployResult.value = null
|
||||
try {
|
||||
deployResult.value = await deploy(deployEnvId.value, selectedTag.value)
|
||||
if (deployResult.value.success) {
|
||||
await loadApplication()
|
||||
}
|
||||
} finally {
|
||||
isDeploying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeDeployModal() {
|
||||
showDeployModal.value = false
|
||||
deployResult.value = null
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add deploy button to each environment**
|
||||
|
||||
In the template, in the environment action buttons area (the `<div class="flex gap-2">` that contains the maintenance toggle button), add a deploy button before the maintenance button:
|
||||
|
||||
```vue
|
||||
<MalioButton
|
||||
:label="t('environments.deploy.button')"
|
||||
icon-name="mdi:rocket-launch-outline"
|
||||
icon-position="left"
|
||||
@click="openDeployModal(env)"
|
||||
/>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add deploy modal to template**
|
||||
|
||||
Add after the existing environment modal (before the closing `</div>` of the root template):
|
||||
|
||||
```vue
|
||||
<!-- Deploy modal -->
|
||||
<AppModal
|
||||
v-model="showDeployModal"
|
||||
:submit-label="isDeploying ? t('environments.deploy.deploying') : t('environments.deploy.confirm')"
|
||||
:cancel-label="t('applications.form.cancel')"
|
||||
:loading="isDeploying"
|
||||
max-width="xl"
|
||||
@submit="handleDeploy"
|
||||
@update:model-value="!$event && closeDeployModal()"
|
||||
>
|
||||
<template #title>{{ t('environments.deploy.title') }}</template>
|
||||
|
||||
<!-- Tag selection -->
|
||||
<div v-if="!deployResult">
|
||||
<div v-if="loadingTags" class="py-8 text-center text-neutral-400">
|
||||
<Icon name="mdi:loading" size="24" class="animate-spin" />
|
||||
<p class="mt-2 text-sm">{{ t('environments.deploy.loadingTags') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="deployTags.length === 0" class="py-8 text-center text-neutral-400">
|
||||
<Icon name="mdi:package-variant" size="32" />
|
||||
<p class="mt-2 text-sm">{{ t('environments.deploy.noTags') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<label class="mb-1 block text-sm font-medium text-neutral-700">
|
||||
{{ t('environments.deploy.selectTag') }}
|
||||
</label>
|
||||
<select
|
||||
v-model="selectedTag"
|
||||
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
>
|
||||
<option v-for="tag in deployTags" :key="tag" :value="tag">
|
||||
{{ tag }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deploy result -->
|
||||
<div v-else>
|
||||
<div
|
||||
class="mb-4 flex items-center gap-2 rounded-lg px-4 py-3"
|
||||
:class="deployResult.success
|
||||
? 'bg-green-50 text-green-700'
|
||||
: 'bg-red-50 text-red-700'"
|
||||
>
|
||||
<Icon
|
||||
:name="deployResult.success ? 'mdi:check-circle' : 'mdi:alert-circle'"
|
||||
size="20"
|
||||
/>
|
||||
<span class="text-sm font-semibold">
|
||||
{{ deployResult.success
|
||||
? t('environments.deploy.success')
|
||||
: t('environments.deploy.error')
|
||||
}}
|
||||
</span>
|
||||
<span class="ml-auto text-xs font-mono">{{ deployResult.tag }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-semibold uppercase tracking-wider text-neutral-400">
|
||||
{{ t('environments.deploy.output') }}
|
||||
</p>
|
||||
<pre class="max-h-80 overflow-auto rounded-lg bg-neutral-900 p-4 text-xs text-green-400 font-mono whitespace-pre-wrap">{{ deployResult.output }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Override footer when showing result -->
|
||||
<template v-if="deployResult" #footer>
|
||||
<MalioButton
|
||||
:label="t('applications.form.cancel')"
|
||||
variant="tertiary"
|
||||
@click="closeDeployModal"
|
||||
/>
|
||||
</template>
|
||||
</AppModal>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify in browser**
|
||||
|
||||
Run `make dev-nuxt`, login, go to `/applications/sirh`:
|
||||
- Each environment should show a "Deployer" button with rocket icon
|
||||
- Clicking it opens a modal
|
||||
- The tag list will load (may fail if GITEA_API_TOKEN not configured — that's expected)
|
||||
- The deploy will fail in dev (script not accessible) — that's expected
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/pages/applications/[slug].vue
|
||||
git commit -m "feat : add deploy modal with tag selection and result display"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Build and verify
|
||||
|
||||
- [ ] **Step 1: Clear Symfony cache**
|
||||
|
||||
```bash
|
||||
docker exec -t -u www-data php-central-fpm php bin/console cache:clear
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build frontend**
|
||||
|
||||
```bash
|
||||
make build-nuxtJS
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify API endpoints exist**
|
||||
|
||||
```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"}'
|
||||
|
||||
# Tags endpoint (will return error about token, that's OK)
|
||||
docker exec -t php-central-fpm curl -s -b /tmp/cookies http://nginx/api/applications/sirh/tags
|
||||
|
||||
# Deploy endpoint (will return error about script, that's OK)
|
||||
docker exec -t php-central-fpm curl -s -b /tmp/cookies -X POST http://nginx/api/environments/1/deploy \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"tag":"latest"}'
|
||||
```
|
||||
|
||||
Both endpoints should return structured JSON responses (even if errors), not 404s.
|
||||
|
||||
- [ ] **Step 4: Final commit if adjustments needed**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix : adjustments from end-to-end testing"
|
||||
```
|
||||
Reference in New Issue
Block a user