247 lines
9.4 KiB
YAML
247 lines
9.4 KiB
YAML
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:
|
||
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
|
||
container:
|
||
image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm
|
||
|
||
steps:
|
||
- name: Tools sanity
|
||
run: |
|
||
set -euo pipefail
|
||
git --version
|
||
node --version
|
||
npm --version
|
||
|
||
- name: Checkout (push or workflow_dispatch, no external actions)
|
||
env:
|
||
EVENT_JSON: /var/run/act/workflow/event.json
|
||
run: |
|
||
set -euo pipefail
|
||
test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }
|
||
|
||
node --input-type=module <<'NODE'
|
||
import fs from "node:fs";
|
||
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";
|
||
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, "'\\''") + "'";
|
||
fs.writeFileSync("/tmp/deploy.env", [
|
||
`REPO_URL=${shq(cloneUrl)}`,
|
||
`DEFAULT_BRANCH=${shq(defaultBranch)}`,
|
||
`SHA=${shq(sha)}`
|
||
].join("\n") + "\n");
|
||
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"
|
||
|
||
if [[ -n "${SHA:-}" ]]; then
|
||
git fetch --depth 1 origin "$SHA"
|
||
git -c advice.detachedHead=false checkout -q FETCH_HEAD
|
||
else
|
||
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
|
||
|
||
FORCE="${INPUT_FORCE:-0}"
|
||
if [[ "$FORCE" == "1" ]]; then
|
||
echo "✅ force=1 -> bypass gate -> deploy allowed"
|
||
echo "GO=1" >> /tmp/deploy.env
|
||
exit 0
|
||
fi
|
||
|
||
CHANGED="$(git show --name-only --pretty="" "$SHA" | sed '/^$/d' || true)"
|
||
echo "== changed files =="
|
||
echo "$CHANGED" | sed -n '1,240p'
|
||
|
||
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
|
||
|
||
- name: Install docker client + docker compose plugin (v2)
|
||
run: |
|
||
set -euo pipefail
|
||
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
|
||
|
||
# 🔥 KEY FIX: reuse existing compose project name if containers already exist
|
||
PROJ="$(docker inspect archicratie-web-blue --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)"
|
||
if [[ -z "${PROJ:-}" ]]; then
|
||
PROJ="$(docker inspect archicratie-web-green --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true)"
|
||
fi
|
||
if [[ -z "${PROJ:-}" ]]; then PROJ="archicratie-web"; fi
|
||
echo "COMPOSE_PROJECT_NAME='$PROJ'" >> /tmp/deploy.env
|
||
echo "✅ Using COMPOSE_PROJECT_NAME=$PROJ"
|
||
|
||
- 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; }
|
||
PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}"
|
||
|
||
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 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
|
||
|
||
CANON="$(curl -fsS "http://127.0.0.1:8081/archicrat-ia/chapitre-1/" | 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;
|
||
}
|
||
|
||
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
|
||
source /tmp/deploy.env
|
||
[[ "${GO:-0}" == "1" ]] || { echo "ℹ️ skipped"; exit 0; }
|
||
PROJ="${COMPOSE_PROJECT_NAME:-archicratie-web}"
|
||
TS="${TS:-$(date -u +%Y%m%d-%H%M%S)}"
|
||
|
||
rollback() {
|
||
echo "⚠️ rollback green -> previous image tag (best effort)"
|
||
docker image tag "archicratie-web:green.BAK.${TS}" archicratie-web:green || true
|
||
docker rm -f archicratie-web-green || true
|
||
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 rm -f archicratie-web-green || true
|
||
docker compose -p "$PROJ" -f docker-compose.yml up -d --force-recreate --remove-orphans 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
|
||
|
||
CANON="$(curl -fsS "http://127.0.0.1:8082/archicrat-ia/chapitre-1/" | 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;
|
||
}
|
||
|
||
echo "✅ live OK"
|
||
set -e |