# 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 n’es pas authentifié, tu verras un 302 vers auth... : c’est 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, c’est que current n’est pas un symlink correct OU que le parent n’est 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 l’ancien 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 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) ```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 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=...