Files
archicratie-edition/docs/runbooks/DEPLOY-BLUE-GREEN.md
Archicratia 5afa5cbfda
Some checks failed
SMOKE / smoke (push) Successful in 3s
CI / build-and-anchors (push) Failing after 32s
CI / build-and-anchors (pull_request) Failing after 34s
chore: remove test annotations and media
2026-03-04 20:48:26 +01:00

547 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RUNBOOK — Déploiement Blue/Green (NAS DS220+)
> Objectif : déployer une release **sans casser**, avec rollback immédiat.
## 0) Portée
Ce runbook décrit le déploiement de lédition web Archicratie sur NAS (Synology), en mode blue/green :
- `web_blue` : upstream staging → `127.0.0.1:8081`
- `web_green` : upstream live → `127.0.0.1:8082`
- Edge Traefik publie :
- `staging.archicratie.trans-hands.synology.me` → 8081
- `archicratie.trans-hands.synology.me` → 8082
## 1) Pré-requis
- Accès shell NAS (user `archicratia`) + `sudo`
- Docker Compose Synology nécessite souvent :
- `sudo env DOCKER_API_VERSION=1.43 docker compose ...`
- Les fichiers edge Traefik sont dans :
- `/volume2/docker/edge/config/dynamic/`
## 2) Répertoires canon (NAS)
On considère ces chemins (adapter si besoin, mais rester cohérent) :
- Base : `/volume2/docker/archicratie-web`
- Releases : `/volume2/docker/archicratie-web/releases/YYYYMMDD-HHMMSS/app`
- Symlink actif : `/volume2/docker/archicratie-web/current` → pointe vers le `.../app` actif
## 3) Garde-fous (AVANT toute action)
### 3.1 Snapshot de létat actuel
en bash :
cd /volume2/docker/archicratie-web
ls -la current || true
readlink current || true
### 3.2 Vérifier létat live/staging upstream direct
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
### 3.3 Vérifier létat edge (host routing)
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
curl -sSI -H 'Host: archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
Si tu nes pas authentifié, tu verras un 302 vers auth... : cest normal.
## 4) Procédure de déploiement (release pack → nouvelle release)
### 4.1 Déposer le pack
Hypothèse : tu as un .tgz “release pack” (issu de release-pack.sh) dans incoming/ :
cd /volume2/docker/archicratie-web
ls -la incoming | tail -n 20
### 4.2 Créer un répertoire release
TS="$(date +%Y%m%d-%H%M%S)"
REL="/volume2/docker/archicratie-web/releases/$TS"
APP="$REL/app"
sudo mkdir -p "$APP"
### 4.3 Extraire le pack
PKG="/volume2/docker/archicratie-web/incoming/archicratie-web.tar.gz" # adapter au nom réel
sudo tar -xzf "$PKG" -C "$APP"
### 4.4 Sanity check (fichiers attendus)
sudo test -f "$APP/Dockerfile" && echo "OK Dockerfile"
sudo test -f "$APP/docker-compose.yml" && echo "OK compose"
sudo test -f "$APP/astro.config.mjs" && echo "OK astro config"
sudo test -f "$APP/src/layouts/EditionLayout.astro" && echo "OK layout"
sudo test -f "$APP/src/pages/archicrat-ia/index.astro" && echo "OK archicrat-ia index"
sudo test -f "$APP/docs/diagrams/archicratie-web-edition-global-verbatim-v2.svg" && echo "OK diagrams"
### 4.5 Permissions (crucial sur Synology)
But : archicratia:users doit pouvoir traverser le parent + lire le contenu.
sudo chown -R archicratia:users "$REL"
sudo chmod -R u+rwX,g+rX,o-rwx "$REL"
sudo chmod 750 "$REL" "$APP"
Vérifier :
ls -ld "$REL" "$APP"
ls -la "$APP" | head
## 5) Activation : basculer current vers la nouvelle release
### 5.1 Backup du current existant
cd /volume2/docker/archicratie-web
TS2="$(date +%F-%H%M%S)"
# on backup "current" (symlink ou dossier)
if [ -e current ] || [ -L current ]; then
sudo mv -f current "current.BAK.$TS2"
echo "✅ backup: current.BAK.$TS2"
fi
### 5.2 Recréer current (symlink propre)
sudo ln -s "$APP" current
ls -la current
readlink current
sudo test -f current/docker-compose.yml && echo "✅ OK: current/docker-compose.yml"
Si cd current échoue, cest que current nest pas un symlink correct OU que le parent nest pas traversable (permissions).
## 6) Build & run : (re)construire web_blue/web_green
### 6.1 Vérifier la config compose
cd /volume2/docker/archicratie-web/current
sudo env DOCKER_API_VERSION=1.43 docker compose -f docker-compose.yml config \
| grep -nE 'services:|web_blue:|web_green:|context:|dockerfile:|PUBLIC_SITE|REQUIRE_PUBLIC_SITE' \
| sed -n '1,220p'
### 6.2 Build propre (recommandé si changement de code/config)
sudo env DOCKER_API_VERSION=1.43 docker compose build --no-cache web_blue web_green
### 6.3 Up (force recreate)
sudo env DOCKER_API_VERSION=1.43 docker compose up -d --force-recreate web_blue web_green
### 6.4 Vérifier upstream direct (8081/8082)
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
## 7) Tests de non-régression (MINIMAL CHECKLIST)
À exécuter systématiquement après up.
### 7.1 Upstreams directs
curl -sSI http://127.0.0.1:8081/ | head -n 12
curl -sSI http://127.0.0.1:8082/ | head -n 12
### 7.2 Canonical (anti “localhost en prod”)
curl -sS http://127.0.0.1:8081/ | grep -oE 'rel="canonical" href="[^"]+"' | head -n 1
curl -sS http://127.0.0.1:8082/ | grep -oE 'rel="canonical" href="[^"]+"' | head -n 1
Attendu :
blue (8081) → https://staging.archicratie.../
green (8082) → https://archicratie.../
### 7.3 Edge routing (Host header + diag)
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/ \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
curl -sSI -H 'Host: staging.archicratie.trans-hands.synology.me' http://127.0.0.1:18080/_auth/whoami \
| grep -iE 'HTTP/|location:|x-archi-router' | head -n 30
### 7.4 Smoke UI (manuel)
Home : lien “Essai-thèse — ArchiCraT-IA” → /archicrat-ia/
TOC global : liens /archicrat-ia/* (pas de préfixe /archicratie/archicrat-ia/*)
Reading-follow/TOC local : scroll ok
## 8) Rollback (si un seul test est mauvais)
Objectif : revenir immédiatement à létat précédent.
### 8.1 Repointer current sur lancien backup
cd /volume2/docker/archicratie-web
ls -la current.BAK.* | tail -n 5
# choisir le plus récent
OLD="current.BAK.YYYY-MM-DD-HHMMSS"
sudo rm -f current
sudo ln -s "$(readlink -f "$OLD")" current 2>/dev/null || sudo ln -s "$(readlink "$OLD")" current
ls -la current
readlink current
### 8.2 Rebuild + recreate
cd /volume2/docker/archicratie-web/current
sudo env DOCKER_API_VERSION=1.43 docker compose build --no-cache web_blue web_green
sudo env DOCKER_API_VERSION=1.43 docker compose up -d --force-recreate web_blue web_green
### 8.3 Re-tester la checklist (section 7)
Si rollback OK : investiguer en environnement isolé (staging upstream uniquement, ou release dans un autre current).
## 9) Notes opérationnelles
Ne jamais modifier dist/ “à la main” sur NAS.
Si un hotfix prod est indispensable : documenter et backporter via PR Gitea.
Le canonical dépend du build : PUBLIC_SITE doit être injecté (voir runbook ENV-PUBLIC_SITE).
## 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 dun `annotations-index.json` consolidé depuis `src/annotations/**`
- patch direct dans les conteneurs en cours dexé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 dancres)
- `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`
Cest 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) sexé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 sexé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 na 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)
```bash
curl -fsSI http://127.0.0.1:8081/ | head -n 1
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 dun `annotations-index.json` consolidé depuis `src/annotations/**`
- patch direct dans les conteneurs en cours dexé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 dancres)
- `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`
Cest 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) sexé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 sexé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 na 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 : lorsquun paragraphe change (ex: ticket “Proposer” appliqué),
lID 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 : lorsquun paragraphe change (ex: ticket “Proposer” appliqué),
lID 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 dun alias : <span id="$OLD" class="para-alias"...>
présence du nouveau paragraphe : <p id="$NEW">...
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 limpression dun 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 dun opérateur non supporté dans la shell effective
Fix :
set -euo pipefail
toujours quoter : [[ "${VAR:-}" == "..." ]]
logguer BEFORE/AFTER/FORCE et sassurer quils ne sont pas vides
Symptom 3 : le gate liste “trop de fichiers” alors quon 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=...