diff --git a/.gitea/workflows/deploy-staging-live.yml b/.gitea/workflows/deploy-staging-live.yml index 24ec8ce..5c6e200 100644 --- a/.gitea/workflows/deploy-staging-live.yml +++ b/.gitea/workflows/deploy-staging-live.yml @@ -1,4 +1,4 @@ -name: Deploy (staging + live) — annotations +name: Deploy staging+live (annotations) on: push: @@ -6,21 +6,18 @@ on: workflow_dispatch: env: - # valeurs "publiques" injectées au build (import.meta.env.PUBLIC_*) - PUBLIC_GITEA_BASE: https://gitea.archicratie.trans-hands.synology.me - PUBLIC_GITEA_OWNER: Archicratia - PUBLIC_GITEA_REPO: archicratie-edition - - # canonical/sitemap (IMPORTANT) - STAGING_PUBLIC_SITE: https://staging.archicratie.trans-hands.synology.me - LIVE_PUBLIC_SITE: https://archicratie.trans-hands.synology.me - - REQUIRE_PUBLIC_SITE: "1" + NODE_OPTIONS: --dns-result-order=ipv4first + DOCKER_API_VERSION: "1.43" + COMPOSE_VERSION: "2.29.7" defaults: run: shell: bash +concurrency: + group: deploy-staging-live-main + cancel-in-progress: false + jobs: deploy: runs-on: ubuntu-latest @@ -28,20 +25,13 @@ jobs: image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm steps: - - name: Tools sanity + install docker cli + - name: Tools sanity run: | set -euo pipefail git --version node --version npm --version - # docker cli + compose plugin + curl (dans le conteneur du job) - apt-get update -y - apt-get install -y --no-install-recommends docker.io docker-compose-plugin curl ca-certificates - docker --version - docker compose version - curl --version | head -n 1 - - name: Checkout (from event.json, no external actions) run: | set -euo pipefail @@ -53,171 +43,149 @@ jobs: const repo = ev?.repository?.clone_url || (ev?.repository?.html_url ? (ev.repository.html_url.replace(/\/$/,"") + ".git") : ""); - const after = + const sha = ev?.after || ev?.pull_request?.head?.sha || ev?.head_commit?.id || - ev?.sha || ""; - const before = - ev?.before || ""; + ev?.sha || + ""; if (!repo) throw new Error("No repository url in event.json"); - if (!after) throw new Error("No after sha in event.json"); - process.stdout.write(`REPO_URL=${JSON.stringify(repo)}\nAFTER=${JSON.stringify(after)}\nBEFORE=${JSON.stringify(before)}\n`); + if (!sha) throw new Error("No sha in event.json"); + process.stdout.write(`REPO_URL=${JSON.stringify(repo)}\nSHA=${JSON.stringify(sha)}\n`); ')" echo "Repo URL: $REPO_URL" - echo "BEFORE: $BEFORE" - echo "AFTER: $AFTER" + echo "SHA: $SHA" rm -rf .git git init -q git remote add origin "$REPO_URL" - - # fetch AFTER (+ BEFORE si dispo) - git fetch --depth 50 origin "$AFTER" + git fetch --depth 1 origin "$SHA" git -c advice.detachedHead=false checkout -q FETCH_HEAD - - # fetch BEFORE si c'est un sha plausible (pas 0000..) - if [[ -n "$BEFORE" && ! "$BEFORE" =~ ^0+$ ]]; then - git fetch --depth 50 origin "$BEFORE" || true - fi - git log -1 --oneline + echo "SHA=$SHA" >> /tmp/deploy.env + echo "REPO_URL=$REPO_URL" >> /tmp/deploy.env + - name: Gate — auto deploy only on annotations/media changes run: | set -euo pipefail + source /tmp/deploy.env - # récupère BEFORE/AFTER depuis event.json (déjà dispo dans env du shell précédent? non) - export EVENT_JSON="/var/run/act/workflow/event.json" - BEFORE="$(node --input-type=module -e 'import fs from "node:fs"; const ev=JSON.parse(fs.readFileSync(process.env.EVENT_JSON,"utf8")); process.stdout.write(String(ev?.before||""))')" - AFTER="$(git rev-parse HEAD)" - - echo "AFTER=$AFTER" - echo "BEFORE=$BEFORE" - - # si BEFORE absent/zeros → on autorise (premier push / edge case) - if [[ -z "$BEFORE" || "$BEFORE" =~ ^0+$ ]]; then - echo "ℹ️ No BEFORE sha => allow deploy" - exit 0 - fi - - # liste des fichiers changés - CHANGED="$(git diff --name-only "$BEFORE" "$AFTER" || true)" + # fichiers touchés par CE commit (merge commit inclus) + CHANGED="$(git diff-tree --no-commit-id --name-only -r "$SHA" || true)" echo "== changed files ==" - echo "$CHANGED" | sed -n '1,240p' + echo "$CHANGED" | sed -n '1,200p' - # autorisé uniquement sur ces chemins - BAD=0 - while IFS= read -r f; do - [[ -z "$f" ]] && continue - if [[ "$f" =~ ^src/annotations/ ]] || [[ "$f" =~ ^public/media/ ]]; then - continue - fi - # tout le reste => on skip l’auto-deploy (pas un échec) - echo "⚠️ non-annotation change detected: $f" - BAD=1 - done <<< "$CHANGED" - - if [[ "$BAD" -eq 1 ]]; then - echo "ℹ️ Skip auto deploy (changes not limited to annotations/media)." - echo "SKIP_DEPLOY=1" >> "$GITHUB_ENV" 2>/dev/null || true - echo "SKIP_DEPLOY=1" >> /tmp/deploy.env + # Gate strict : uniquement annotations + media + le workflow lui-même (si tu veux autoriser) + if echo "$CHANGED" | grep -qE '^(src/annotations/|public/media/)'; then + echo "GO=1" >> /tmp/deploy.env + echo "✅ deploy allowed (annotations/media change detected)" exit 0 fi - echo "✅ Allowed: annotations/media only" + echo "GO=0" >> /tmp/deploy.env + echo "ℹ️ no annotations/media change -> skip deploy" + exit 0 - - name: Build + deploy staging (blue) then smoke + - name: Install docker client + docker compose plugin (v2) run: | set -euo pipefail - if [[ -f /tmp/deploy.env ]] && grep -q '^SKIP_DEPLOY=1' /tmp/deploy.env; then - echo "ℹ️ skipped" - exit 0 - fi + source /tmp/deploy.env + [[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; } + apt-get -o Acquire::Retries=5 -o Acquire::ForceIPv4=true update + apt-get install -y --no-install-recommends ca-certificates curl docker.io + rm -rf /var/lib/apt/lists/* + + mkdir -p /usr/local/lib/docker/cli-plugins + curl -fsSL \ + "https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-x86_64" \ + -o /usr/local/lib/docker/cli-plugins/docker-compose + chmod +x /usr/local/lib/docker/cli-plugins/docker-compose + + docker version + docker compose version + + - name: Assert required vars (PUBLIC_GITEA_*) + env: + PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }} + PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }} + PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }} + run: | + set -euo pipefail + source /tmp/deploy.env + [[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; } + + test -n "${PUBLIC_GITEA_BASE:-}" || { echo "❌ missing repo var PUBLIC_GITEA_BASE"; exit 2; } + test -n "${PUBLIC_GITEA_OWNER:-}" || { echo "❌ missing repo var PUBLIC_GITEA_OWNER"; exit 2; } + test -n "${PUBLIC_GITEA_REPO:-}" || { echo "❌ missing repo var PUBLIC_GITEA_REPO"; exit 2; } + echo "✅ vars OK" + + - name: Build + deploy staging (blue) then smoke + env: + PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }} + PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }} + PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }} + run: | + set -euo pipefail + source /tmp/deploy.env + [[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; } + + # backup tags (best effort) TS="$(date -u +%Y%m%d-%H%M%S)" - echo "TS=$TS" + 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 - # backup image tag (best effort) - docker image inspect archicratie-web:blue >/dev/null 2>&1 && \ - docker image tag archicratie-web:blue "archicratie-web:blue.BAK.${TS}" || true + # build + restart staging (blue=8081) + docker compose build --no-cache web_blue + docker compose up -d --force-recreate web_blue - # IMPORTANT: forcer les args de build (staging) - export PUBLIC_SITE="$STAGING_PUBLIC_SITE" + # smoke staging (local port) + 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 - # on évite les conflits de "container name already in use" - docker rm -f archicratie-web-blue >/dev/null 2>&1 || true - - docker compose -f docker-compose.yml build web_blue - docker compose -f docker-compose.yml up -d --force-recreate web_blue - - # smoke staging (8081) - echo "== smoke staging (8081) ==" - curl -fsS -o /dev/null "http://127.0.0.1:8081/para-index.json" - curl -fsS -o /dev/null "http://127.0.0.1:8081/annotations-index.json" CANON="$(curl -fsS "http://127.0.0.1:8081/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)" - echo "canonical: $CANON" - echo "$CANON" | grep -q "$STAGING_PUBLIC_SITE" || { echo "❌ staging canonical mismatch"; exit 1; } + echo "canonical(blue)=$CANON" + echo "$CANON" | grep -q 'https://staging\.archicratie\.trans-hands\.synology\.me/' || { + echo "❌ staging canonical mismatch"; exit 3; + } + echo "✅ staging OK" - name: Build + deploy live (green) then smoke + rollback if needed + env: + PUBLIC_GITEA_BASE: ${{ vars.PUBLIC_GITEA_BASE }} + PUBLIC_GITEA_OWNER: ${{ vars.PUBLIC_GITEA_OWNER }} + PUBLIC_GITEA_REPO: ${{ vars.PUBLIC_GITEA_REPO }} run: | set -euo pipefail - if [[ -f /tmp/deploy.env ]] && grep -q '^SKIP_DEPLOY=1' /tmp/deploy.env; then - echo "ℹ️ skipped" - exit 0 - fi + source /tmp/deploy.env + [[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; } - TS="$(date -u +%Y%m%d-%H%M%S)" - echo "TS=$TS" + TS="${TS:-$(date -u +%Y%m%d-%H%M%S)}" - # backup image tag (best effort) - docker image inspect archicratie-web:green >/dev/null 2>&1 && \ - docker image tag archicratie-web:green "archicratie-web:green.BAK.${TS}" || true + rollback() { + echo "⚠️ rollback green -> previous image tag (best effort)" + docker image tag "archicratie-web:green.BAK.${TS}" archicratie-web:green || true + docker compose up -d --force-recreate web_green || true + } - # IMPORTANT: args de build (live) - export PUBLIC_SITE="$LIVE_PUBLIC_SITE" - - # on évite les conflits - docker rm -f archicratie-web-green >/dev/null 2>&1 || true - - # build + up set +e - docker compose -f docker-compose.yml build web_green - BUILD_RC=$? - set -e - [[ "$BUILD_RC" -eq 0 ]] || { echo "❌ build live failed"; exit 1; } + docker compose build --no-cache web_green + docker compose up -d --force-recreate web_green - docker compose -f docker-compose.yml up -d --force-recreate web_green + 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 - # smoke live (8082) - echo "== smoke live (8082) ==" - set +e - curl -fsS -o /dev/null "http://127.0.0.1:8082/para-index.json" - A1=$? - curl -fsS -o /dev/null "http://127.0.0.1:8082/annotations-index.json" - A2=$? CANON="$(curl -fsS "http://127.0.0.1:8082/archicrat-ia/chapitre-1/" | grep -oE 'rel="canonical" href="[^"]+"' | head -n1 || true)" - echo "canonical: $CANON" - echo "$CANON" | grep -q "$LIVE_PUBLIC_SITE" - A3=$? - set -e + echo "canonical(green)=$CANON" + echo "$CANON" | grep -q 'https://archicratie\.trans-hands\.synology\.me/' || { + echo "❌ live canonical mismatch"; rollback; exit 4; + } - if [[ "$A1" -ne 0 || "$A2" -ne 0 || "$A3" -ne 0 ]]; then - echo "❌ live smoke failed => rollback" - - # rollback image tag if backup exists - if docker image inspect "archicratie-web:green.BAK.${TS}" >/dev/null 2>&1; then - docker image tag "archicratie-web:green.BAK.${TS}" archicratie-web:green - docker rm -f archicratie-web-green >/dev/null 2>&1 || true - docker compose -f docker-compose.yml up -d --force-recreate --no-build web_green - echo "✅ rollback applied" - else - echo "⚠️ no backup image tag found => cannot rollback automatically" - fi - - exit 1 - fi - - echo "✅ live OK" \ No newline at end of file + echo "✅ live OK" + set -e \ No newline at end of file