Restore runbook automation and monthly drill #87
Labels
No Label
area:auth
area:docs
area:export
area:ops
area:storage
area:supply-chain
auth
backend
blocked
collab
deployment
docs
effort:L
effort:M
effort:S
frontend
plugins
qa
vs-nfd
vs-nfd:blocker
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: stwaidele/dorfteich#87
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Backups without practiced restores are hope, not strategy (ADR 0015: monthly automated drill).
Scope
Implement the drill workflow (scheduled Gitea Actions, monthly): fetch the latest Prod (or Test, pre-go-live) backup set, restore into a scratch compose project on the VPS (188.245.116.44), run sanity checks (migration status, row counts vs. status.json metadata, one fixture page render via API, one media file byte-check), report the outcome as a Gitea issue comment on a pinned 'Restore drills' issue, tear down; finalize
docs/operations/restore-runbook.mdwith the manual procedure (doubles as the Prod relocation procedure).Acceptance criteria
Technical notes
Dependencies
Depends on #83, #84.
Size: ~1.5 days
Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add
deanden), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.Implemented in
d95c18e+6e99cb3(both pipelines green; the drill itself ran green on demand and reported to the pinned log issue #98).Workflow
.gitea/workflows/drill.yml— monthly (1st, 04:17 UTC, after the nightly backups) plus on demand. Gitea 1.22 has no workflow-dispatch API/UI (arrives in 1.23), so the on-demand trigger is pushing adrill-*tag. Each run posts its outcome (success and failure,if: always()) as a comment on the pinned Restore drills issue #98 with the drill log.deploy/backup/drill.sh— plain-docker orchestration (no compose dependency in the runner):status.json → lastSuccess.backupId;dorfteich-drill-<timestamp>prefix: own network, own volumes, throwaway Postgres with a random password;restore.sh(the backup image'sdist/restore.js:pg_restore --clean --if-exists+ volume untar);:test) against the result and checks: readyzdatabase+migrationsok, ≥ 1 user and page (trash counts — restorability, not content policy; Test's e2e packs leave fixtures trashed), ≥ 1 rendered page inpage_content_cache, a public API request (legal endpoint: routing + db + HTML pipeline) answers 200, and one media file's bytes on the volume match itsattachments.size_bytesrow (skips with a note when the set has no attachments);trap— also on failure.Acceptance criteria
drill-verify-87→ run 201 → comment "Restore drill success" on #98 (set20260711-154136, 3 users, 12 pages, 12 rendered, public API 200).drill: FAILED — restore of 20260711-154136 did not complete, teardown still ran; the corrupted volume was removed afterwards (nothing committed, per the AC).restore.shwas executed live on Test during #83 (display-name corruption → restore → reverted, readyz green);drill.shadditionally ran manually on the host twice during development. Manual procedure + Prod-relocation notes indocs/operations/restore-runbook.md.:ro; everything else lives under the unique drill prefix; after every rundocker volume lsshows zero drill volumes (verified).Deviations from the issue text, deliberate: the stages live on ONE now (not the old VPS — the runner is on the same host, so the drill reaches the volumes directly); pre-go-live it drills Test (
DRILL_SOURCE_VOLUME), switch to Prod at go-live (#89). Fetching the set from the BASEL mirror stays with #84 (tunnel still down); "row counts vs. status.json metadata" became row counts + a size byte-check, since status.json records sizes, not row counts.