Add the release pipeline with a tag-based manual gate and Prod stack (#89)
All checks were successful
CD / Build and push images (push) Successful in 1m5s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m6s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m33s
CI / Build container images (push) Has been skipped
Release / Build release images and notes (push) Successful in 1m1s
Prod deploy / Deploy the released images to Prod (push) Successful in 14s
CI / Auth e2e pack (push) Successful in 5m31s
CI / Import/export fidelity gate (push) Successful in 47s
All checks were successful
CD / Build and push images (push) Successful in 1m5s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m6s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m33s
CI / Build container images (push) Has been skipped
Release / Build release images and notes (push) Successful in 1m1s
Prod deploy / Deploy the released images to Prod (push) Successful in 14s
CI / Auth e2e pack (push) Successful in 5m31s
CI / Import/export fidelity gate (push) Successful in 47s
Pushing vX.Y.Z builds the four semver images and publishes a Gitea release whose notes list the changes since the previous release with a migration call-out derived from the migrations diff (the repo is trunk-based — commit subjects stand in for PR titles). Deploying to Prod is a separate human act: pushing prod-vX.Y.Z-<suffix> — Gitea 1.22 has no environment approvals, so the tag push is the gate — verifies the release images exist, pins TAG in the Prod .env, restarts the stack, and waits for readiness; rollbacks are new suffix tags on the previous release. The Prod stage is provisioned on ONE (ports 8120-8122, secrets generated on the host, full backup profile); deploy/go-live.md carries the executed mechanics and the operator checklist that blocks the DNS switch (DNS, Caddy block, wizard/SMTP, legal texts, monitors, Prod drill, BASEL mirror). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
This commit is contained in:
parent
92c71c3f12
commit
28f05e270d
61
.gitea/workflows/prod-deploy.yml
Normal file
61
.gitea/workflows/prod-deploy.yml
Normal file
@ -0,0 +1,61 @@
|
||||
# Prod deploy behind the manual gate (issue #89): after reviewing a
|
||||
# published release vX.Y.Z, a human pushes `prod-vX.Y.Z-<suffix>` — that
|
||||
# tag push IS the approval (Gitea 1.22 has no environment approvals; move
|
||||
# to a real environment gate on 1.23+). The suffix keeps tags unique, so
|
||||
# re-deploys and rollbacks are just new tags pointing at the same release:
|
||||
# git tag prod-v0.1.0-initial && git push origin prod-v0.1.0-initial
|
||||
# git tag prod-v0.1.0-rollback1 && git push origin prod-v0.1.0-rollback1
|
||||
|
||||
name: Prod deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['prod-v*']
|
||||
|
||||
env:
|
||||
IMAGE_BASE: gitea.101010.cloud/stwaidele/dorfteich
|
||||
DEPLOY_HOST: one.101010.cloud
|
||||
|
||||
jobs:
|
||||
deploy-prod:
|
||||
name: Deploy the released images to Prod
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Resolve the release version from the tag
|
||||
run: |
|
||||
VERSION=$(echo "$GITHUB_REF_NAME" | grep -o 'v[0-9]\+\.[0-9]\+\.[0-9]\+' | head -n1)
|
||||
[ -n "$VERSION" ] || { echo "tag $GITHUB_REF_NAME carries no vX.Y.Z version" >&2; exit 1; }
|
||||
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
echo "deploying release $VERSION"
|
||||
|
||||
- name: Verify the release images exist (never deploy unbuilt tags)
|
||||
run: |
|
||||
printf '%s' "${{ secrets.REGISTRY_TOKEN }}" | tr -d '[:space:]' | docker login gitea.101010.cloud -u fable-5 --password-stdin
|
||||
for app in web api collab backup; do
|
||||
docker buildx imagetools inspect $IMAGE_BASE-$app:$VERSION > /dev/null
|
||||
done
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY_TEST }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
printf '%s\n' "${{ secrets.DEPLOY_HOST_KEY }}" > ~/.ssh/known_hosts
|
||||
|
||||
- name: Pin the version and restart the Prod stack
|
||||
run: |
|
||||
ssh deploy@$DEPLOY_HOST "cd /home/DOCKER/dorfteich-prod \
|
||||
&& sed -i 's/^TAG=.*/TAG=$VERSION/' .env \
|
||||
&& docker compose pull --quiet && docker compose up -d --remove-orphans \
|
||||
&& docker compose ps"
|
||||
|
||||
- name: Wait for Prod readiness
|
||||
run: |
|
||||
for i in $(seq 1 45); do
|
||||
body=$(ssh deploy@$DEPLOY_HOST 'curl -s http://127.0.0.1:8121/api/v1/readyz' || true)
|
||||
case "$body" in
|
||||
*'"database","status":"ok"'*) echo "readyz: $body"; exit 0 ;;
|
||||
esac
|
||||
sleep 4
|
||||
done
|
||||
echo "Prod did not become ready" >&2; exit 1
|
||||
65
.gitea/workflows/release.yml
Normal file
65
.gitea/workflows/release.yml
Normal file
@ -0,0 +1,65 @@
|
||||
# Release build (ADR 0014, issue #89): pushing a semver tag `vX.Y.Z` builds
|
||||
# and pushes the immutable release images and publishes a Gitea release
|
||||
# whose notes list the changes since the previous release, with a call-out
|
||||
# when the release contains database migrations (the `migration` marker the
|
||||
# update guide promises). Deploying to Prod is a SEPARATE, manual step:
|
||||
# after reviewing the release, push a `prod-vX.Y.Z-<suffix>` tag
|
||||
# (prod-deploy.yml) — that tag push is the manual approval gate, since
|
||||
# Gitea 1.22 has no environment approvals (revisit on 1.23+).
|
||||
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*.*.*']
|
||||
|
||||
env:
|
||||
IMAGE_BASE: gitea.101010.cloud/stwaidele/dorfteich
|
||||
|
||||
jobs:
|
||||
build-release:
|
||||
name: Build release images and notes
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Log in to the Gitea registry
|
||||
run: printf '%s' "${{ secrets.REGISTRY_TOKEN }}" | tr -d '[:space:]' | docker login gitea.101010.cloud -u fable-5 --password-stdin
|
||||
|
||||
- name: Build and push semver images
|
||||
run: |
|
||||
TAG=${GITHUB_REF_NAME}
|
||||
for app in web api collab backup; do
|
||||
docker build -f apps/$app/Dockerfile --build-arg APP_VERSION=$TAG \
|
||||
-t $IMAGE_BASE-$app:$TAG .
|
||||
docker push $IMAGE_BASE-$app:$TAG
|
||||
done
|
||||
|
||||
- name: Generate release notes and publish the release
|
||||
run: |
|
||||
TAG=${GITHUB_REF_NAME}
|
||||
PREV=$(git tag --list 'v*.*.*' --sort=-v:refname | grep -vx "$TAG" | head -n1 || true)
|
||||
RANGE=${PREV:+$PREV..}$TAG
|
||||
{
|
||||
echo "## Changes since ${PREV:-the beginning}"
|
||||
echo
|
||||
git log --no-merges --pretty='- %s' $RANGE
|
||||
echo
|
||||
if git diff --name-only ${PREV:-$(git hash-object -t tree /dev/null)} $TAG -- apps/api/prisma/migrations/ | grep -q .; then
|
||||
echo '> ⚠️ **migration** — this release applies database migrations automatically at api start. Downgrade window: one minor release (docs/self-hosting).'
|
||||
else
|
||||
echo '_No database migrations in this release._'
|
||||
fi
|
||||
} > notes.md
|
||||
TAG=$TAG docker run --rm -i -e TAG node:22.15-alpine node -e \
|
||||
'const fs=require("fs");const body=fs.readFileSync(0,"utf8");process.stdout.write(JSON.stringify({tag_name:process.env.TAG,name:process.env.TAG,body}))' \
|
||||
< notes.md > release.json
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${{ github.token }}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data @release.json \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases" \
|
||||
> /dev/null && echo "release $TAG published"
|
||||
61
deploy/go-live.md
Normal file
61
deploy/go-live.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Go-live checklist — dorfteich.online (issue #89)
|
||||
|
||||
The release pipeline and the Prod stack are ready; going live is the
|
||||
operator's call. Executed items carry their date; unchecked items block
|
||||
the DNS switch.
|
||||
|
||||
## Release mechanics (in place)
|
||||
|
||||
- [x] 2026-07-12 — **Release workflow**: pushing `vX.Y.Z` builds the four
|
||||
semver images and publishes a Gitea release whose notes list the
|
||||
changes since the previous release and call out database
|
||||
migrations. (Notes derive from commit subjects — the repo is
|
||||
trunk-based without PRs; the issue's "PR titles" have no source
|
||||
here, documented deviation.)
|
||||
- [x] 2026-07-12 — **Manual gate + Prod deploy**: pushing
|
||||
`prod-vX.Y.Z-<suffix>` (the human approval — Gitea 1.22 has no
|
||||
environment gates; revisit on 1.23+) verifies the release images
|
||||
exist, pins `TAG` in the Prod `.env`, pulls, restarts, and waits
|
||||
for readiness. Re-deploys/rollbacks are new suffix tags on the
|
||||
target release.
|
||||
- [x] 2026-07-12 — **Prod stack provisioned** on ONE
|
||||
(`/home/DOCKER/dorfteich-prod/`, ports 8120–8122, secrets generated
|
||||
on the host, full backup profile: 30 d retention, failure mail).
|
||||
The host decision "ONE" is the working default — recorded here; if
|
||||
the owner picks different iron at go-live, the stack directory
|
||||
moves per the restore runbook's relocation procedure.
|
||||
- [x] 2026-07-12 — **Test release walked the full gate**: `v0.1.0` built
|
||||
and published → `prod-v0.1.0-initial` deployed → readyz green.
|
||||
- [x] 2026-07-12 — **Rollback tested on Prod**: `v0.1.1` deployed, then
|
||||
`prod-v0.1.0-rollback1` returned the stack to `v0.1.0`, readyz
|
||||
green (one-release downgrade window per docs/self-hosting).
|
||||
|
||||
## Operator items (block the DNS switch)
|
||||
|
||||
- [ ] **Prod host decision confirmed** (working default: ONE, where
|
||||
Test/Int and the registry already live — one host, no BASEL yet).
|
||||
- [ ] **DNS**: point `dorfteich.online` at ONE (today it still points at
|
||||
the old VPS `188.245.116.44`).
|
||||
- [ ] **Caddy**: activate the prepared `dorfteich.online` block in
|
||||
`/etc/caddy/Caddyfile` on ONE (ports 8120–8122) after DNS,
|
||||
`systemctl reload caddy`, verify the Let's Encrypt certificate —
|
||||
this also closes the #88 item "ACME exercised on a real domain".
|
||||
- [ ] **First-run setup**: run the wizard on the fresh instance (or set
|
||||
the `SETUP_ADMIN_*` preseed in the Prod `.env` before first boot) —
|
||||
creates the Site Admin.
|
||||
- [ ] **Prod SMTP**: configure a production relay (wizard step or `.env`);
|
||||
the Prod `.env` ships without SMTP on purpose.
|
||||
- [ ] **Legal texts** (#82): paste the real dorfteich.online imprint and
|
||||
privacy policy in Administration → Legal pages.
|
||||
- [ ] **Monitors** (#85): create the Prod monitor set from
|
||||
`deploy/monitoring.md` in Uptime-Kuma, with alerting; verify one
|
||||
test alert fires.
|
||||
- [ ] **Backups verified on Prod** (#87): switch the drill's
|
||||
`DRILL_SOURCE_VOLUME` to `dorfteich-prod_backups` and run one
|
||||
on-demand drill (`drill-*` tag) green.
|
||||
- [ ] **Off-host mirror** (#84): blocked on the ONE→BASEL WireGuard
|
||||
tunnel (Handoff-Wireguard.md) — going live without it is a
|
||||
conscious, temporary risk acceptance (local 30 d backups only).
|
||||
- [ ] Optional hygiene: a dedicated `DEPLOY_SSH_KEY_PROD` secret (the
|
||||
workflows currently reuse the host-wide deploy key stored as
|
||||
`DEPLOY_SSH_KEY_TEST`).
|
||||
Loading…
Reference in New Issue
Block a user