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