Compare commits
9 Commits
chore/depl
...
chore/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
| 4dec9e182b | |||
| 9b4584f70a | |||
| 7b64fb7401 | |||
|
|
57cb23ce8b | ||
| 708b87ff35 | |||
| 577cfd08e8 | |||
| de9edbe532 | |||
| 5e95dc9898 | |||
| 006fec7efd |
@@ -1,26 +1,28 @@
|
||||
name: Deploy (staging + live) — annotations
|
||||
name: Deploy staging+live (annotations)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force:
|
||||
description: "Force deploy even if gate would skip (1=yes, 0=no)"
|
||||
required: false
|
||||
default: "0"
|
||||
|
||||
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,196 +30,211 @@ 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)
|
||||
- name: Checkout (push or workflow_dispatch, no external actions)
|
||||
env:
|
||||
EVENT_JSON: /var/run/act/workflow/event.json
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export EVENT_JSON="/var/run/act/workflow/event.json"
|
||||
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
|
||||
|
||||
eval "$(node --input-type=module -e 'import fs from "node:fs";
|
||||
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON,"utf8"));
|
||||
const repo =
|
||||
ev?.repository?.clone_url ||
|
||||
(ev?.repository?.html_url ? (ev.repository.html_url.replace(/\/$/,"") + ".git") : "");
|
||||
const after =
|
||||
ev?.after ||
|
||||
ev?.pull_request?.head?.sha ||
|
||||
ev?.head_commit?.id ||
|
||||
ev?.sha || "";
|
||||
const before =
|
||||
ev?.before || "";
|
||||
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`);
|
||||
')"
|
||||
# Write /tmp/deploy.env with shell-safe quoting (NO eval)
|
||||
node --input-type=module <<'NODE'
|
||||
import fs from "node:fs";
|
||||
|
||||
echo "Repo URL: $REPO_URL"
|
||||
echo "BEFORE: $BEFORE"
|
||||
echo "AFTER: $AFTER"
|
||||
const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
|
||||
const repoObj = ev?.repository || {};
|
||||
|
||||
const cloneUrl =
|
||||
repoObj?.clone_url ||
|
||||
(repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");
|
||||
if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");
|
||||
|
||||
const defaultBranch = repoObj?.default_branch || "main";
|
||||
|
||||
// On push, act/Gitea usually provides GITHUB_SHA; on workflow_dispatch it may be empty.
|
||||
const sha =
|
||||
(process.env.GITHUB_SHA && String(process.env.GITHUB_SHA).trim()) ||
|
||||
ev?.after ||
|
||||
ev?.sha ||
|
||||
ev?.head_commit?.id ||
|
||||
ev?.pull_request?.head?.sha ||
|
||||
"";
|
||||
|
||||
const shq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
||||
|
||||
const out = [
|
||||
`REPO_URL=${shq(cloneUrl)}`,
|
||||
`DEFAULT_BRANCH=${shq(defaultBranch)}`,
|
||||
`SHA=${shq(sha)}`
|
||||
].join("\n") + "\n";
|
||||
|
||||
fs.writeFileSync("/tmp/deploy.env", out);
|
||||
process.stdout.write(out);
|
||||
NODE
|
||||
|
||||
source /tmp/deploy.env
|
||||
|
||||
echo "Repo URL: $REPO_URL"
|
||||
echo "Default branch: $DEFAULT_BRANCH"
|
||||
echo "SHA: ${SHA:-<empty>}"
|
||||
|
||||
rm -rf .git
|
||||
git init -q
|
||||
git remote add origin "$REPO_URL"
|
||||
|
||||
# fetch AFTER (+ BEFORE si dispo)
|
||||
git fetch --depth 50 origin "$AFTER"
|
||||
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
|
||||
if [[ -n "${SHA:-}" ]]; then
|
||||
git fetch --depth 1 origin "$SHA"
|
||||
git -c advice.detachedHead=false checkout -q FETCH_HEAD
|
||||
else
|
||||
# workflow_dispatch fallback: checkout latest of default branch
|
||||
git fetch --depth 1 origin "$DEFAULT_BRANCH"
|
||||
git -c advice.detachedHead=false checkout -q "origin/$DEFAULT_BRANCH"
|
||||
SHA="$(git rev-parse HEAD)"
|
||||
echo "SHA='$SHA'" >> /tmp/deploy.env
|
||||
echo "Resolved SHA: $SHA"
|
||||
fi
|
||||
|
||||
git log -1 --oneline
|
||||
|
||||
- name: Gate — auto deploy only on annotations/media changes
|
||||
env:
|
||||
INPUT_FORCE: ${{ inputs.force }}
|
||||
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"
|
||||
FORCE="${INPUT_FORCE:-0}"
|
||||
if [[ "$FORCE" == "1" ]]; then
|
||||
echo "✅ force=1 -> bypass gate -> deploy allowed"
|
||||
echo "GO=1" >> /tmp/deploy.env
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# liste des fichiers changés
|
||||
CHANGED="$(git diff --name-only "$BEFORE" "$AFTER" || true)"
|
||||
CHANGED="$(git show --name-only --pretty="" "$SHA" | sed '/^$/d' || true)"
|
||||
echo "== changed files =="
|
||||
echo "$CHANGED" | sed -n '1,240p'
|
||||
|
||||
# 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
|
||||
exit 0
|
||||
if echo "$CHANGED" | grep -qE '^(src/annotations/|public/media/)'; then
|
||||
echo "GO=1" >> /tmp/deploy.env
|
||||
echo "✅ deploy allowed (annotations/media change detected)"
|
||||
else
|
||||
echo "GO=0" >> /tmp/deploy.env
|
||||
echo "ℹ️ no annotations/media change -> skip deploy"
|
||||
fi
|
||||
|
||||
echo "✅ Allowed: annotations/media only"
|
||||
|
||||
- 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: Assert deploy files exist
|
||||
run: |
|
||||
set -euo pipefail
|
||||
source /tmp/deploy.env
|
||||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||||
|
||||
test -f docker-compose.yml
|
||||
test -f Dockerfile
|
||||
test -f nginx.conf
|
||||
echo "✅ deploy files 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; }
|
||||
|
||||
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
|
||||
|
||||
# IMPORTANT: forcer les args de build (staging)
|
||||
export PUBLIC_SITE="$STAGING_PUBLIC_SITE"
|
||||
|
||||
# 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 build --no-cache 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"
|
||||
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
|
||||
|
||||
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 -f docker-compose.yml 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 -f docker-compose.yml build --no-cache web_green
|
||||
docker compose -f docker-compose.yml up -d --force-recreate web_green
|
||||
|
||||
# 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=$?
|
||||
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
|
||||
|
||||
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"
|
||||
echo "✅ live OK"
|
||||
set -e
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 816 KiB |
@@ -19,3 +19,12 @@ paras:
|
||||
credit: ""
|
||||
ts: 2026-02-25T19:11:32.634Z
|
||||
fromIssue: 121
|
||||
p-11-67c14c09:
|
||||
media:
|
||||
- type: image
|
||||
src: /media/archicrat-ia/chapitre-4/p-11-67c14c09/Capture_d_e_cran_2026-02-16_a_13.07.35.png
|
||||
caption: "[Media] p-11-67c14c09 — Chapitre 4 — Histoire archicratique des
|
||||
révolutions industrielles"
|
||||
credit: ""
|
||||
ts: 2026-02-26T13:17:41.286Z
|
||||
fromIssue: 129
|
||||
|
||||
Reference in New Issue
Block a user