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

18 KiB
Raw Blame History

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

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=1MODE=full
  2. Sinon si HAS_HOTPATCH=1MODE=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)

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=...