diff --git a/.gitea/workflows/prod-deploy.yml b/.gitea/workflows/prod-deploy.yml new file mode 100644 index 0000000..79f525e --- /dev/null +++ b/.gitea/workflows/prod-deploy.yml @@ -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-` — 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 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..875dd85 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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-` 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" diff --git a/deploy/go-live.md b/deploy/go-live.md new file mode 100644 index 0000000..a54be05 --- /dev/null +++ b/deploy/go-live.md @@ -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-` (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`).