From 390f2c33e5580a0ba3ef7fcce73f3cfb84f34c8b Mon Sep 17 00:00:00 2001 From: Archicratia Date: Thu, 26 Feb 2026 21:06:01 +0100 Subject: [PATCH] ci: fix deploy workflow (warmup + hotpatch) --- .gitea/workflows/deploy-staging-live.yml | 135 +++++++++++++++++++---- 1 file changed, 111 insertions(+), 24 deletions(-) diff --git a/.gitea/workflows/deploy-staging-live.yml b/.gitea/workflows/deploy-staging-live.yml index 7401691..ce9eb16 100644 --- a/.gitea/workflows/deploy-staging-live.yml +++ b/.gitea/workflows/deploy-staging-live.yml @@ -106,7 +106,8 @@ jobs: exit 0 fi - CHANGED="$(git show --name-only --pretty="" "$SHA" | sed '/^$/d' || true)" + # merge commit safe: -m => considère les parents (liste de fichiers plus fiable) + CHANGED="$(git show -m --name-only --pretty="" "$SHA" | sed '/^$/d' || true)" echo "== changed files ==" echo "$CHANGED" | sed -n '1,240p' @@ -184,27 +185,68 @@ jobs: [[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; } PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}" + retry_url() { + local url="$1"; local tries="${2:-45}"; local delay="${3:-1}" + local i=1 + while (( i <= tries )); do + if curl -fsS --max-time 5 --retry 2 --retry-delay 0 --retry-all-errors "$url" >/dev/null 2>&1; then + return 0 + fi + echo "… wait [$i/$tries] $url" + sleep "$delay" + ((i++)) + done + return 1 + } + + fetch_html() { + local url="$1"; local tries="${2:-45}"; local delay="${3:-1}" + local i=1 + while (( i <= tries )); do + local html + html="$(curl -fsS --max-time 8 --retry 2 --retry-delay 0 --retry-all-errors "$url" 2>/dev/null || true)" + if [[ -n "$html" ]]; then + printf "%s" "$html" + return 0 + fi + echo "… wait HTML [$i/$tries] $url" + sleep "$delay" + ((i++)) + done + return 1 + } + + dump_container() { + local name="$1" + echo "== docker ps (filter=$name) ==" + docker ps -a --filter "name=$name" --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' || true + echo "== docker logs (tail) $name ==" + docker logs --tail 200 "$name" 2>/dev/null || true + } + TS="$(date -u +%Y%m%d-%H%M%S)" echo "TS='$TS'" >> /tmp/deploy.env docker image tag archicratie-web:blue "archicratie-web:blue.BAK.${TS}" || true docker image tag archicratie-web:green "archicratie-web:green.BAK.${TS}" || true - # ✅ use cache (DO NOT --no-cache) + docker compose -p "$PROJ" -f docker-compose.yml config >/dev/null + docker compose -p "$PROJ" -f docker-compose.yml build web_blue - # ✅ hard fix: remove existing container if name conflicts docker rm -f archicratie-web-blue || true - docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_blue - curl -fsS "http://127.0.0.1:8081/para-index.json" >/dev/null - curl -fsS "http://127.0.0.1:8081/annotations-index.json" >/dev/null - curl -fsS "http://127.0.0.1:8081/pagefind/pagefind.js" >/dev/null + retry_url "http://127.0.0.1:8081/para-index.json" 60 1 || { dump_container archicratie-web-blue; exit 31; } + retry_url "http://127.0.0.1:8081/annotations-index.json" 60 1 || { dump_container archicratie-web-blue; exit 32; } + retry_url "http://127.0.0.1:8081/pagefind/pagefind.js" 60 1 || { dump_container archicratie-web-blue; exit 33; } - CANON="$(curl -fsS "http://127.0.0.1:8081/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)" + HTML="$(fetch_html "http://127.0.0.1:8081/archicrat-ia/chapitre-1/" 60 1 || true)" + CANON="$(echo "$HTML" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)" echo "canonical(blue)=$CANON" echo "$CANON" | grep -q 'https://staging\.archicratie\.trans-hands\.synology\.me/' || { - echo "❌ staging canonical mismatch"; exit 3; + dump_container archicratie-web-blue + echo "❌ staging canonical mismatch" + exit 34 } echo "✅ staging OK" @@ -221,6 +263,45 @@ jobs: PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}" TS="${TS:-$(date -u +%Y%m%d-%H%M%S)}" + retry_url() { + local url="$1"; local tries="${2:-45}"; local delay="${3:-1}" + local i=1 + while (( i <= tries )); do + if curl -fsS --max-time 5 --retry 2 --retry-delay 0 --retry-all-errors "$url" >/dev/null 2>&1; then + return 0 + fi + echo "… wait [$i/$tries] $url" + sleep "$delay" + ((i++)) + done + return 1 + } + + fetch_html() { + local url="$1"; local tries="${2:-45}"; local delay="${3:-1}" + local i=1 + while (( i <= tries )); do + local html + html="$(curl -fsS --max-time 8 --retry 2 --retry-delay 0 --retry-all-errors "$url" 2>/dev/null || true)" + if [[ -n "$html" ]]; then + printf "%s" "$html" + return 0 + fi + echo "… wait HTML [$i/$tries] $url" + sleep "$delay" + ((i++)) + done + return 1 + } + + dump_container() { + local name="$1" + echo "== docker ps (filter=$name) ==" + docker ps -a --filter "name=$name" --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' || true + echo "== docker logs (tail) $name ==" + docker logs --tail 200 "$name" 2>/dev/null || true + } + rollback() { echo "⚠️ rollback green -> previous image tag (best effort)" docker image tag "archicratie-web:green.BAK.${TS}" archicratie-web:green || true @@ -228,24 +309,36 @@ jobs: docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green || true } - set +e - docker compose -p "$PROJ" -f docker-compose.yml build web_green + docker compose -p "$PROJ" -f docker-compose.yml config >/dev/null + + if ! docker compose -p "$PROJ" -f docker-compose.yml build web_green; then + dump_container archicratie-web-green + rollback + exit 40 + fi docker rm -f archicratie-web-green || true - docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green + if ! docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans web_green; then + dump_container archicratie-web-green + rollback + exit 41 + fi - curl -fsS "http://127.0.0.1:8082/para-index.json" >/dev/null - curl -fsS "http://127.0.0.1:8082/annotations-index.json" >/dev/null - curl -fsS "http://127.0.0.1:8082/pagefind/pagefind.js" >/dev/null + retry_url "http://127.0.0.1:8082/para-index.json" 90 1 || { dump_container archicratie-web-green; rollback; exit 42; } + retry_url "http://127.0.0.1:8082/annotations-index.json" 90 1 || { dump_container archicratie-web-green; rollback; exit 43; } + retry_url "http://127.0.0.1:8082/pagefind/pagefind.js" 90 1 || { dump_container archicratie-web-green; rollback; exit 44; } - CANON="$(curl -fsS "http://127.0.0.1:8082/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)" + HTML="$(fetch_html "http://127.0.0.1:8082/archicrat-ia/chapitre-1/" 90 1 || true)" + CANON="$(echo "$HTML" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)" echo "canonical(green)=$CANON" echo "$CANON" | grep -q 'https://archicratie\.trans-hands\.synology\.me/' || { - echo "❌ live canonical mismatch"; rollback; exit 4; + dump_container archicratie-web-green + echo "❌ live canonical mismatch" + rollback + exit 45 } echo "✅ live OK" - set -e - name: Hotpatch annotations-index.json (deep merge shards) into blue+green run: | @@ -292,7 +385,6 @@ jobs: return out def deep_merge(dst, src): - # non destructif : ne supprime rien, n'écrase pas les scalaires existants for k, v in (src or {}).items(): if k in ("media", "refs", "comments_editorial") and is_arr(v): if k == "media": @@ -310,7 +402,6 @@ jobs: continue if is_arr(v): - # fallback: union by json string cur = dst.get(k, []) if not is_arr(cur): cur = [] seen = set() @@ -326,7 +417,6 @@ jobs: dst[k] = out continue - # scalaires: set seulement si absent / vide if k not in dst or dst.get(k) in (None, ""): dst[k] = v @@ -376,7 +466,6 @@ jobs: except Exception as e: errors.append({"file": os.path.relpath(fp, ROOT), "error": str(e)}) - # tri paras for page, obj in pages.items(): keys = list((obj.get("paras") or {}).keys()) keys.sort(key=lambda k: (para_num(k), k)) @@ -400,13 +489,11 @@ jobs: print("OK: wrote /tmp/annotations-index.json pages=", out["stats"]["pages"], "paras=", out["stats"]["paras"], "errors=", out["stats"]["errors"]) PY - # inject into running containers for c in archicratie-web-blue archicratie-web-green; do echo "== patch $c ==" docker cp /tmp/annotations-index.json "${c}:/usr/share/nginx/html/annotations-index.json" done - # quick smoke: check new file exists and is readable for p in 8081 8082; do echo "== smoke annotations-index on $p ==" curl -fsS "http://127.0.0.1:${p}/annotations-index.json" | python3 -c 'import sys,json; j=json.load(sys.stdin); print("generatedAt:", j.get("generatedAt")); print("pages:", len(j.get("pages") or {}))'