diff --git a/docs/runbooks/DEPLOY-BLUE-GREEN.md b/docs/runbooks/DEPLOY-BLUE-GREEN.md index 26afbde..801236b 100644 --- a/docs/runbooks/DEPLOY-BLUE-GREEN.md +++ b/docs/runbooks/DEPLOY-BLUE-GREEN.md @@ -320,4 +320,227 @@ But : prouver que staging+live servent bien les endpoints essentiels (et que le ```bash curl -fsSI http://127.0.0.1:8081/ | head -n 1 -curl -fsSI http://127.0.0.1:8082/ | head -n 1 \ No newline at end of file +curl -fsSI http://127.0.0.1:8082/ | head -n 1 + +--- + +## 10) CI Deploy (Gitea Actions) — Gate SKIP / HOTPATCH / FULL (merge-proof) + preuves + +Cette section documente le comportement **canonique** du workflow : +- `.gitea/workflows/deploy-staging-live.yml` + +Objectif : **zéro surprise**. +On ne veut plus “penser à force=1”. +Le gate doit décider automatiquement, y compris sur des **merge commits**. + +### 10.1 — Principe (ce que fait réellement le gate) + +Le job `deploy` calcule les fichiers modifiés entre : +- `BEFORE` = commit précédent (avant le push sur main) +- `AFTER` = commit actuel (après le push / merge sur main) + +Puis il classe le déploiement dans un mode : + +- **MODE=full** + - rebuild image + restart `archicratie-web-blue` (8081) + `archicratie-web-green` (8082) + - warmup endpoints (para-index, annotations-index, pagefind.js) + - vérification canonical staging + live + +- **MODE=hotpatch** + - rebuild d’un `annotations-index.json` consolidé depuis `src/annotations/**` + - patch direct dans les conteneurs en cours d’exécution (blue+green) + - copie des médias modifiés `public/media/**` vers `/usr/share/nginx/html/media/**` + - smoke sur `/annotations-index.json` des deux ports + +- **MODE=skip** + - pas de déploiement (on évite le bruit) + +⚠️ Important : le mode “hotpatch” **ne rebuild pas** Astro. +Donc toute modification de contenu, routes, scripts, anchors, etc. doit déclencher **full**. + +### 10.2 — Matrice de décision (règles officielles) + +Le gate définit deux flags : +- `HAS_FULL=1` si changement “build-impacting” +- `HAS_HOTPATCH=1` si changement “annotations/media only” + +Règle de priorité : +1) Si `HAS_FULL=1` → **MODE=full** +2) Sinon si `HAS_HOTPATCH=1` → **MODE=hotpatch** +3) Sinon → **MODE=skip** + +#### 10.2.1 — Changements qui déclenchent FULL (build-impacting) + +Exemples typiques (non exhaustif, mais on couvre le cœur) : +- `src/content/**` (contenu MD/MDX) +- `src/pages/**` (routes Astro) +- `src/anchors/**` (aliases d’ancres) +- `scripts/**` (tooling postbuild : injection, index, tests) +- `src/layouts/**`, `src/components/**`, `src/styles/**` (rendu et scripts inline) +- `astro.config.mjs`, `package.json`, `package-lock.json` +- `Dockerfile`, `docker-compose.yml`, `nginx.conf` +- `.gitea/workflows/**` (changement infra CI/CD) + +=> On veut **full** pour garantir cohérence et éviter “site partiellement mis à jour”. + +#### 10.2.2 — Changements qui déclenchent HOTPATCH (sans rebuild) + +Uniquement : +- `src/annotations/**` (shards YAML) +- `public/media/**` (assets média) + +=> On veut hotpatch pour vitesse et éviter rebuild NAS. + +### 10.3 — “Merge-proof” : pourquoi on ne lit PAS seulement `git show $SHA` + +Sur un merge commit, `git show --name-only $SHA` peut être trompeur selon le contexte. +La méthode robuste est : +- utiliser `event.json` (Gitea Actions) pour récupérer `before` et `after` +- calculer `git diff --name-only BEFORE AFTER` + +C’est ce qui rend le gate **merge-proof**. + +### 10.4 — Tests de preuve A/B (reproductibles) + +Ces tests valident le gate sans ambiguïté. +But : vérifier que le mode choisi est EXACTEMENT celui attendu. + +#### Test A — toucher `src/content/...` (FULL auto) + +1) Créer une branche test +2) Modifier 1 fichier dans `src/content/` (ex : ajouter une ligne de commentaire non destructive) +3) PR → merge dans `main` +4) Vérifier dans `deploy-staging-live.yml` : + +Attendus : +- `Gate flags: HAS_FULL=1 HAS_HOTPATCH=0` +- `✅ build-impacting change -> MODE=full (rebuild+restart)` +- Les étapes FULL (blue puis green) s’exécutent réellement + +#### Test B — toucher `src/annotations/...` uniquement (HOTPATCH auto) + +1) Créer une branche test +2) Modifier 1 fichier sous `src/annotations/**` (ex: un champ comment, ts, etc.) +3) PR → merge dans `main` +4) Vérifier dans `deploy-staging-live.yml` : + +Attendus : +- `Gate flags: HAS_FULL=0 HAS_HOTPATCH=1` +- `✅ annotations/media change -> MODE=hotpatch` +- Les étapes FULL sont “skip” (durée 0s) +- L’étape HOTPATCH s’exécute réellement + +### 10.5 — Preuve opérationnelle côté NAS (2 URLs + 2 commandes) + +But : prouver que staging+live servent bien les endpoints essentiels (et que le déploiement n’a pas “fait semblant”). + +#### 10.5.1 — Deux URLs à vérifier (staging et live) + +- Staging (blue) : `http://127.0.0.1:8081/` +- Live (green) : `http://127.0.0.1:8082/` + +#### 10.5.2 — Deux commandes minimales (zéro débat) + +en bash : +curl -fsSI http://127.0.0.1:8081/ | head -n 1 +curl -fsSI http://127.0.0.1:8082/ | head -n 1 + +Attendu : HTTP/1.1 200 OK des deux côtés. + +10.6 — Preuve “alias injection” (ancre ancienne → nouvelle) sur une page + +Contexte : lorsqu’un paragraphe change (ex: ticket “Proposer” appliqué), +l’ID de paragraphe peut changer, mais on doit préserver les liens anciens via : + +src/anchors/anchor-aliases.json + +injection build-time dans dist (span .para-alias) + +10.6.1 — Check rapide (staging + live) + +Remplacer OLD/NEW par tes ids réels : + +Attendu : HTTP/1.1 200 OK des deux côtés. + +10.6 — Preuve “alias injection” (ancre ancienne → nouvelle) sur une page + +Contexte : lorsqu’un paragraphe change (ex: ticket “Proposer” appliqué), +l’ID de paragraphe peut changer, mais on doit préserver les liens anciens via : + +src/anchors/anchor-aliases.json + +injection build-time dans dist (span .para-alias) + +10.6.1 — Check rapide (staging + live) + +Remplacer OLD/NEW par tes ids réels : + +OLD="p-1-60c7ea48" +NEW="p-1-a21087b0" + +for P in 8081 8082; do + echo "=== $P ===" + HTML="$(curl -fsS "http://127.0.0.1:${P}/archicrat-ia/chapitre-3/" | tr -d '\r')" + echo "OLD count: $(printf '%s' "$HTML" | grep -o "$OLD" | wc -l | tr -d ' ')" + echo "NEW count: $(printf '%s' "$HTML" | grep -o "$NEW" | wc -l | tr -d ' ')" + printf '%s\n' "$HTML" | grep -nE "$OLD|$NEW|class=\"para-alias\"" | head -n 40 || true +done + +Attendu : + +présence d’un alias : + +présence du nouveau paragraphe :

... + +10.6.2 — Check “lien ancien ne casse pas” (HTTP 200) + +for P in 8081 8082; do + curl -fsSI "http://127.0.0.1:${P}/archicrat-ia/chapitre-3/#${OLD}" | head -n 1 +done + +Attendu : HTTP/1.1 200 OK et navigation fonctionnelle côté navigateur. + +10.7 — Troubleshooting gate (symptômes typiques) +Symptom 1 : job bloqué “Set up job” très longtemps + +Causes fréquentes : + +runner indisponible / capacity saturée + +runner ne récupère pas les tâches (fetch_timeout trop court + réseau instable) + +erreur dans “Gate — decide …” qui casse bash (et donne l’impression d’un hang) + +Commandes NAS (diagnostic rapide) : + +docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | grep -E 'gitea-act-runner|registry|archicratie-web' +docker logs --since 30m --tail 400 gitea-act-runner | tail -n 200 +Symptom 2 : conditional binary operator expected + +Cause : + +test bash du type [[ "$X" == "1" && "$Y" == "2" ]] mal formé + +variable vide non quotée + +usage d’un opérateur non supporté dans la shell effective + +Fix : + +set -euo pipefail + +toujours quoter : [[ "${VAR:-}" == "..." ]] + +logguer BEFORE/AFTER/FORCE et s’assurer qu’ils ne sont pas vides + +Symptom 3 : le gate liste “trop de fichiers” alors qu’on a changé 1 seul fichier + +Cause : + +comparaison faite sur le mauvais range (ex: git show sur merge, ou mauvais parent) +Fix : + +toujours utiliser git diff --name-only "$BEFORE" "$AFTER" (merge-proof) + +confirmer dans le log : Gate ctx: BEFORE=... AFTER=... +