# OPS — Déploiement Archicratie Web Edition (Mac Studio → DS220+)
Objectif : déployer une nouvelle version du site sur le NAS (DS220+) sans jamais casser la prod, en utilisant un schéma blue/green piloté par DSM Reverse Proxy, avec une procédure robuste même quand docker compose build est instable sur le NAS.

> 🟧 **LEGACY / HISTORIQUE** — Ce document n’est plus la source de vérité.

> Référence actuelle : docs/DEPLOY_PROD_SYNOLOGY_DS220.md (canonique).

> Statut : gelé (on n’édite plus que pour ajouter un lien vers le canonique, si nécessaire).

> Raison : doublon → risque de divergence → risque d’erreur en prod.

> Si tu lis ceci pour déployer : stop → ouvre le canonique.

> ⚠️ LEGACY — Ne pas suivre pour déployer.
> Doc conservé pour historique.
> Canon : `DEPLOY_PROD_SYNOLOGY_DS220.md` + `OPS-SYNC-TRIPLE-SOURCE.md`.

## Pourquoi ce doc existe encore

- Historique : il capture des repères (domaines, ports, logique blue/green) tels qu’ils ont été consolidés pendant la phase d’implémentation.
- Sécurité : éviter la divergence documentaire (un seul pas-à-pas officiel).
- Maintenance : si tu dois déployer, tu suis le canonique ; ici tu ne viens que pour comprendre “d’où ça vient”.

## Ce qu’il faut faire aujourd’hui (canonique)

➡️ Déploiement = `docs/DEPLOY_PROD_SYNOLOGY_DS220.md` (procédure détaillée, à jour).

## Mise à jour (2026-03-03) — Gate CI de déploiement (SKIP / HOTPATCH / FULL) + preuves A/B

La procédure de déploiement “vivante” est désormais pilotée par **Gitea Actions** via le workflow :
- `.gitea/workflows/deploy-staging-live.yml`

Ce workflow décide automatiquement :
- **FULL** (rebuild + restart blue + green) dès qu’un changement impacte le build (ex: `src/content/`, `src/pages/`, `scripts/`, `src/anchors/`, etc.)
- **HOTPATCH** (patch JSON + copie media) quand le changement ne concerne que `src/annotations/` et/ou `public/media/`
- **SKIP** sinon

Les preuves et la procédure de test reproductible A/B sont documentées dans :
➡️ `docs/runbooks/DEPLOY-BLUE-GREEN.md` → section “CI Deploy gate (merge-proof) + Tests A/B + preuve alias injection”.

## Schéma (résumé, sans commandes)

- Ne jamais toucher au slot live.
- Construire/tester sur l’autre slot.
- Smoke test.
- Bascule DSM Reverse Proxy (8081 ↔ 8082).
- Rollback DSM si besoin.

<details>

> 🚫 NE PAS UTILISER POUR PROD — ARCHIVE UNIQUEMENT

<summary>Archive — ancien pas-à-pas (NE PAS SUIVRE)</summary>

> ⚠️ Archive. Ce contenu est conservé pour mémoire.

## 0) Repères essentiels
Noms & domaines
    • Site public (prod) : https://archicratie.trans-hands.synology.me
    • Gitea public : https://gitea.archicratie.trans-hands.synology.me
Attention : tu as eu un “piège” de typo (trans-hands-synology.me ≠ trans-hands.synology.me). Toujours vérifier l’orthographe exacte.
Ports locaux NAS
    • web_blue : 127.0.0.1:8081 → container:80
    • web_green : 127.0.0.1:8082 → container:80
    • DSM Reverse Proxy pointe soit vers 8081 soit vers 8082.
Règle d’or “safe prod”
    • On ne touche jamais au slot live (celui pointé par DSM).
    • On construit/teste sur l’autre slot.
    • On bascule DSM (10 secondes).
    • On rollback DSM (10 secondes) si besoin.

## 1) Blue/Green : qui fait quoi ?
Convention simple et pro
    • blue = slot A (souvent “actif” par défaut) → 8081
    • green = slot B (staging/next) → 8082
Mais : la vérité = DSM.
Ce n’est pas “blue” ou “green” qui décide : c’est le Reverse Proxy DSM qui pointe vers 8081 ou 8082.
➡️ Donc :
    • blue et green sont deux environnements identiques
    • l’un est “live”, l’autre est “next”
    • après bascule, les rôles s’inversent

## 2) Arborescence NAS (standard à stabiliser)
Chemin racine :
    • /volume2/docker/archicratie-web/
Sous-dossiers recommandés :
    • incoming/ : dépôt d’archives venant du Mac (upload File Station / scp)
    • releases/ : releases dépliées, horodatées
    • current : symlink vers la release “courante” (le code utilisé pour builder)
    • ops/ : scripts d’exploitation (smoke, which-live, helpers)
    • current__backup_before_cleanup/ : backup historique (optionnel)
Exemple sain :
/volume2/docker/archicratie-web/
  incoming/
    archicratie-web-20260130-104937.tar.gz
    archicratie-web-20260130-104937.tar.gz.sha256
  releases/
    20260130-104937/
      app/   (code déplié prêt)
  current -> releases/20260130-104937/app
  ops/
    smoke.sh
    which-live.sh

## 3) Permissions NAS (ce qui est “propre”)
Tu as eu des problèmes de release dépliée “root:root” → impossible à lire en user.
Politique simple
    • propriétaire : archicratia:users
    • dossiers : 750
    • fichiers : 640
    • incoming peut être 770 si tu y upload souvent via File Station.
Commandes (à lancer à la racine) :
BASE="/volume2/docker/archicratie-web"

sudo chown -R archicratia:users "$BASE"
sudo chmod 750 "$BASE" "$BASE/ops" "$BASE/releases" "$BASE/current__backup_before_cleanup" 2>/dev/null || true
sudo chmod 770 "$BASE/incoming" 2>/dev/null || true

# Important : options find AVANT les tests
find "$BASE/releases" -maxdepth 3 -type d -exec chmod 750 {} \;
find "$BASE/releases" -maxdepth 3 -type f -exec chmod 640 {} \;

⚠️ Éviter chmod 700 * “au hasard” : ça peut te bloquer toi-même + bloquer des outils DSM.
Le trio 750/640 est ton bon compromis.

## 4) Variables Gitea (les 3 variables qui conditionnent “Proposer”)
Dans .env sur le NAS (dans current/ ou dans la release) :
PUBLIC_GITEA_BASE=https://gitea.archicratie.trans-hands.synology.me
PUBLIC_GITEA_OWNER=Archicratia
PUBLIC_GITEA_REPO=archicratie-edition

Point critique : la casse du OWNER
Tu as déjà vu le symptôme :
    • archicratia/archicratie-edition → peut renvoyer 404
    • Archicratia/archicratie-edition → OK
Donc : OWNER doit être exactement la casse que Gitea attend.
Test rapide (NAS) :
BASE="https://gitea.archicratie.trans-hands.synology.me"
curl -kI "$BASE/Archicratia/archicratie-edition/" | head -n 8
✅ attendu : HTTP/2 200

## 5) Côté Mac Studio : préparer une release “propre” (sans scories macOS)
Pourquoi
Tu as eu :
    • fichiers ._*
    • xattrs LIBARCHIVE.xattr.com.apple.*
    • warnings à l’extraction
➡️ Ce n’est pas bloquant, mais c’est sale et ça te pollue les releases.
Script Mac : release-pack.sh (sans Git, ultra simple)
À placer à la racine du repo site/ sur Mac.
#!/bin/zsh
set -euo pipefail

TS="${1:-$(date +%Y%m%d-%H%M%S)}"
NAME="archicratie-web-$TS"
OUT_DIR="_release_out"
ARCHIVE="$OUT_DIR/$NAME.tar.gz"
SHA="$ARCHIVE.sha256"

mkdir -p "$OUT_DIR"

### Évite les AppleDouble (._*) + certaines métadonnées
export COPYFILE_DISABLE=1
export COPY_EXTENDED_ATTRIBUTES_DISABLE=1

### Nettoyages “safe”
find . -name ".DS_Store" -delete 2>/dev/null || true

### Crée un dossier staging
STAGE="$(mktemp -d)"
mkdir -p "$STAGE/$NAME"

### Copie le repo SANS déchets
rsync -a --delete \
  --exclude ".git/" \
  --exclude "node_modules/" \
  --exclude "dist/" \
  --exclude ".astro/" \
  --exclude "_release_out/" \
  --exclude ".DS_Store" \
  --exclude "__MACOSX/" \
  --exclude "._*" \
  ./ "$STAGE/$NAME/"

### Tar propre
tar -czf "$ARCHIVE" -C "$STAGE" "$NAME"

### Checksum
shasum -a 256 "$ARCHIVE" > "$SHA"

echo "OK: $ARCHIVE"
echo "OK: $SHA"

Usage :
chmod +x release-pack.sh
./release-pack.sh 20260130-104937

Résultat :
    • _release_out/archicratie-web-20260130-104937.tar.gz
    • _release_out/archicratie-web-20260130-104937.tar.gz.sha256

## 6) Transfert vers le NAS
Option A — DSM File Station (simple)
    • Upload les 2 fichiers dans :
        ◦ /volume2/docker/archicratie-web/incoming/
Option B — scp (si SSH)
Depuis le Mac :
scp _release_out/archicratie-web-20260130-104937.tar.gz* \
archicratia@192.168.1.20:/volume2/docker/archicratie-web/incoming/
## 7) NAS : vérifier + déplier une release (procédure canonique)
BASE="/volume2/docker/archicratie-web"
TS="20260130-104937"

cd "$BASE"

### 1) checksum (IMPORTANT)
sha256sum -c "incoming/archicratie-web-$TS.tar.gz.sha256"
### 2) créer dossier release
rm -rf "releases/$TS"
mkdir -p "releases/$TS"
### 3) extraire
tar -xzf "incoming/archicratie-web-$TS.tar.gz" -C "releases/$TS"
### 4) pointer APP vers le dossier extrait
APP="$BASE/releases/$TS/archicratie-web-$TS"
### option: normaliser en "app"
mv "$APP" "$BASE/releases/$TS/app"
APP="$BASE/releases/$TS/app"
### 5) ownership + perms
sudo chown -R archicratia:users "$BASE/releases/$TS"
find "$BASE/releases/$TS" -type d -exec chmod 750 {} \;
find "$BASE/releases/$TS" -type f -exec chmod 640 {} \;
### 6) basculer current (symlink)
ln -sfn "$APP" "$BASE/current"
### 7) sanity check : compose présent ?
ls -la "$BASE/current/docker-compose.yml" "$BASE/current/Dockerfile" "$BASE/current/nginx.conf" "$BASE/current/.env"

Cas réel déjà rencontré : la release “n’a pas les fichiers ops”
Tu as eu un tar “inner” (archicratie-web.tar.gz) qui n’embarque pas docker-compose.yml/Dockerfile/nginx.conf/.env.
Règle pro : ces fichiers doivent être dans la release.
Fix immédiat si c’est absent (tu l’as déjà fait) :
OPS_SRC="$BASE/releases/20260129-174516_clean"
cp -a "$OPS_SRC/docker-compose.yml" "$APP/"
cp -a "$OPS_SRC/Dockerfile"         "$APP/"
cp -a "$OPS_SRC/nginx.conf"         "$APP/"
cp -a "$OPS_SRC/.env"               "$APP/.env"
## 8) Build GREEN sur NAS (robuste) — IMPORTANT
Pourquoi docker compose build n’est PAS fiable sur DS220+
Tu as vu :
    • Temporary failure resolving 'deb.debian.org'
    • apt-get update qui part en Ign: puis Err:
➡️ En pratique sur ton NAS : le builder n’a pas toujours le bon réseau, même si build: network: host est déclaré.
Procédure “qui marche vraiment” (golden path)
➡️ On build en host network avec docker build, puis on déploie avec compose sans rebuild.
cd /volume2/docker/archicratie-web/current

### charge les env vars du .env
set -a
. ./.env
set +a

### BUILD image green (host network)
sudo docker build --no-cache --network host \
  --build-arg PUBLIC_GITEA_BASE="$PUBLIC_GITEA_BASE" \
  --build-arg PUBLIC_GITEA_OWNER="$PUBLIC_GITEA_OWNER" \
  --build-arg PUBLIC_GITEA_REPO="$PUBLIC_GITEA_REPO" \
  -t archicratie-web:green \
  -f Dockerfile .

Voir Ign: pendant apt-get n’est pas forcément une erreur.
Ce qui compte : est-ce qu’il finit par télécharger / réussir.
On s’alarme uniquement si on voit Err: ... Temporary failure resolving + exit code non-zéro.

## 9) Démarrer GREEN (sans impacter BLUE)
cd /volume2/docker/archicratie-web/current
# ne jamais lancer "up" sans préciser le service
sudo docker rm -f archicratie-web-green 2>/dev/null || true
sudo docker compose up -d --force-recreate --no-build web_green
Smoke test (port 8082) :
/volume2/docker/archicratie-web/ops/smoke.sh 8082
## 10) DSM : basculer le Reverse Proxy (10 secondes)
DSM → Panneau de configuration → Portail de connexion (ou “Portail des applications”) → Proxy inversé
Trouve la règle :
    • Source : https://archicratie.trans-hands.synology.me (443)
Destination :
    • pour BLUE : http://127.0.0.1:8081
    • pour GREEN : http://127.0.0.1:8082
Tu changes seulement le port, tu “Appliques”.
Test immédiat :
curl -kI https://archicratie.trans-hands.synology.me/ | head -n 8
## 11) Rollback (10 secondes)
Si ça ne va pas :
    • DSM Reverse Proxy → remettre l’ancien port (8081 ou 8082)
    • refresh navigateur
Option pro : ne stoppe pas l’autre container tout de suite (tu peux comparer / investiguer calmement).

## 12) Ops scripts (à garder dans /volume2/docker/archicratie-web/ops)
smoke.sh (version “complète”)
#!/bin/sh
set -eu

PORT="${1:-8081}"
BASE="http://127.0.0.1:${PORT}"

echo "Smoke test ${BASE}"

curl -fsSI "${BASE}/" | head -n 5
curl -fsSI "${BASE}/pagefind/pagefind.js" | head -n 5
curl -fsSI "${BASE}/pagefind/pagefind-ui.js" | head -n 5 || true
curl -fsSI "${BASE}/pagefind/pagefind-ui.css" | head -n 5 || true
curl -fsSI "${BASE}/pagefind/pagefind-entry.json" | head -n 5 || true

echo "OK"

which-live.sh (déduire quel port est live)
Idée : on compare les Last-Modified ou ETag entre le domaine public et les ports locaux.
#!/bin/sh
set -eu

DOMAIN="https://archicratie.trans-hands.synology.me"
A="http://127.0.0.1:8081"
B="http://127.0.0.1:8082"

etag() { curl -ksI "$1/" | awk -F': ' 'tolower($1)=="etag"{print $2}' | tr -d '\r'; }
lm()   { curl -ksI "$1/" | awk -F': ' 'tolower($1)=="last-modified"{print $2}' | tr -d '\r'; }

E_D="$(etag "$DOMAIN")"
E_A="$(etag "$A")"
E_B="$(etag "$B")"

echo "DOMAIN ETag: $E_D"
echo "8081   ETag: $E_A"
echo "8082   ETag: $E_B"

if [ -n "$E_D" ] && [ "$E_D" = "$E_A" ]; then
  echo "LIVE=8081 (blue slot probable)"
elif [ -n "$E_D" ] && [ "$E_D" = "$E_B" ]; then
  echo "LIVE=8082 (green slot probable)"
else
  echo "LIVE=INCONNU (ETag mismatch) — check Last-Modified:"
  echo "DOMAIN: $(lm "$DOMAIN")"
  echo "8081  : $(lm "$A")"
  echo "8082  : $(lm "$B")"
fi
## 13) Pannes & diagnostics (les vrais cas rencontrés)
### A) 504 via DSM Reverse Proxy
Symptôme : HTTP/2 504 côté domaine.
Check :
    1. le container répond en local :
curl -I http://127.0.0.1:8081/
curl -I http://127.0.0.1:8082/
    2. DSM Reverse Proxy destination = http://127.0.0.1:808X (pas https)
    3. ports bindés en loopback (recommandé) : 127.0.0.1:808X:80
    4. logs container :
sudo docker logs --tail=80 archicratie-web-blue
sudo docker logs --tail=80 archicratie-web-green
### B) “Proposer” ouvre Gitea mais tombe sur 404
C’est quasiment toujours l’un de ces trois points :
    1. OWNER/REPO incorrects (casse)
Test :
BASE="https://gitea.archicratie.trans-hands.synology.me"
curl -kI "$BASE/Archicratia/archicratie-edition/" | head
    • Si 404 → c’est ton .env / build args.
    • La build a embarqué une ancienne config
Vérifie dans le HTML servi (dans le container) :
sudo docker exec -it archicratie-web-green sh -lc \
'grep -Rin "const GITEA_BASE" /usr/share/nginx/html | head -n 20'
    1. Tu dois voir https://gitea.archicratie.trans-hands.synology.me + bon OWNER/REPO.
    2. Problème d’auth / droits Gitea
Si tu es loggé mais “pas autorisé”, vérifier que tu as accès au repo et que l’URL pointe au bon repo.
### C) “Proposer” échoue avant Gitea (pas de pop-up, pas de 2 choix)
Check :
    • console navigateur (JS)
    • vérifier que le script “Proposer” est bien injecté dans la page (view source)
    • tester une page simple (ex: /archicratie/)
### D) Pagefind n’indexe plus
Ce que tu as déjà observé :
    • Pagefind ignore les pages sans data-pagefind-body
    • Tu avais bien ~12 pages indexées (log Pagefind)
Check dans le container :
sudo docker exec -it archicratie-web-green sh -lc \
'ls -la /usr/share/nginx/html/pagefind | head'
Note :
    • GET /pagefind/ peut renvoyer 403 (listing interdit) : ce n’est pas un bug
    • tu testes plutôt :
        ◦ /pagefind/pagefind.js
        ◦ /pagefind/pagefind-entry.json
### E) docker compose build casse sur apt-get update (DNS)
Symptôme :
    • Temporary failure resolving 'deb.debian.org'
Diagnostic rapide :
sudo docker run --rm --network host node:22-bookworm-slim sh -lc \
'apt-get -o Acquire::ForceIPv4=true update -qq && echo OK_APT'
✅ si OK_APT : ton NAS sait sortir, c’est le builder compose qui est instable → utiliser la golden path : docker build --network host.

## 14) Dockerfile : version robuste “NAS host-network”
Dans ton Dockerfile, garde :
RUN --network=host apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/*

Option “anti-fragile” (retries + IPv4) :
RUN --network=host apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update \
 && apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
## 15) Important : ne pas se faire piéger par le docker.sock
Sur ton DS220+, /var/run/docker.sock est root:root avec 660 → ton user ne peut pas accéder Docker sans sudo.
➡️ Règle :
    • commandes Docker = sudo docker ...
    • compose = sudo docker compose ...
(Optionnel : alias)
alias d='sudo docker'
alias dc='sudo docker compose'
## 16) Workflow résumé “cockpit” (le plus important)
Depuis le Mac :
    1. ./release-pack.sh 20260130-104937
    2. upload _release_out/*.tar.gz* → NAS incoming/
Sur le NAS :
    1. vérifier checksum
    2. déplier releases/$TS + chown/chmod
    3. ln -sfn releases/$TS/app current
    4. build green :
        ◦ sudo docker build --network host ... -t archicratie-web:green .
    5. run green :
        ◦ sudo docker compose up -d --force-recreate --no-build web_green
    6. smoke :
        ◦ /volume2/docker/archicratie-web/ops/smoke.sh 8082
    7. switch DSM Reverse Proxy 8081 ↔ 8082
    8. si souci → rollback DSM

Post-scriptum : ton problème local Mac (Astro “Cannot find module astro/config”)
Ce symptôme arrive typiquement quand :
    • node_modules est incomplet/corrompu
    • tu as basculé de dossier (ex: un _release_out/... a été pris pour le vrai repo)
    • ou le process dev a reload sur un tsconfig “nouveau” mais pas les deps
Fix standard (dans le vrai dossier site/) :
rm -rf node_modules .astro dist
npm ci
npm run dev

</details>
