CI/CD: pre-deploy test + migration gate so paliad.de stays online through failed deploys (inventor) #114
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?
m's report (2026-05-25 16:55)
Observed pain (today's session)
2x outages today both from migration failures at container boot:
paliad.applied_migrationsclaiming version=123 with the wrong name; cronus's actual 123_backups merged in parallel; migrator bailed on name mismatch; container crashlooped ~30 min.125_cross_cutting_filter_legal_sourcereferenced columns (is_mandatory,is_optional,condition_flag) dropped in mig 091; container crashlooped through two hotfix iterations.In both cases,
go build ./...was clean at merge time — the failure was migration-content. Container then went into Docker restart loop, Traefik served default 404. Site fully offline until head debugged.Constraint envelope from m
Phase: inventor design (READ-ONLY)
Inventor → coder gate per project CLAUDE.md.
Open design questions
Q1 — Where do tests run?
A. Gitea Actions (gitea has built-in CI runners). Runner on mriver or mlake. Pre-merge to main: every push fires
.gitea/workflows/test.yamlwhich runsgo build,go test ./internal/...,cd frontend && bun run build. Deploy webhook fires ONLY on green.B. Custom proxy on mriver: receives the gitea push webhook, runs tests in a transient docker container (using the same Dockerfile as prod), forwards to Dokploy only on green. More work to maintain.
C. Dokploy pre-deploy hook: if Dokploy supports a pre-deploy script (per skill docs — verify), run tests inline before container swap. Tightest integration but Dokploy may not expose this.
(R) = A — Gitea Actions. The runner is the cheapest infra; gitea's workflow file is in the repo; pipeline status visible in PR view. mriver hosts the runner since it has the worker pool resources.
Q2 — Where does the migration get tested?
Go unit tests don't catch migration failures because they run against a clean schema. A real test for migration safety has to:
Options:
A. Migration smoke test in the CI job: pull the prod schema dump nightly into a runner-local Postgres, then on every push, spin up an ephemeral copy, apply all migrations through the new one, run a smoke query.
B. Dry-run migration mode: Go binary has a flag that runs migrations against a target DB-URL inside a transaction and rolls back. Run as a CI step against a clone DB.
C. Defer to prod with auto-rollback: container's startup migrator rolls back on failure + flags the deploy as broken; old container stays running. Dokploy / Docker Swarm support this via health checks + zero-downtime swap.
(R) = A + C: belt-and-suspenders. A catches column-mismatch + ownership traps before they ever reach prod. C handles the rare case where production data shape differs from the snapshot.
Q3 — Blue/green or canary deploy for the container itself?
Docker Swarm (Dokploy's runtime) supports rolling updates with health checks. Current setup: container starts → fails health check → swarm doesn't replace the running one. Verify this is actually configured — if
update_config.failure_action: pauseis set in the compose, today's crashloops would NOT have taken the site offline.Looking at the symptom (Traefik default 404), it seems Swarm DID replace the old container with the failing one. The
restart: alwaysorrestart_policy.condition: anymay be too aggressive.(R) = inventor researches the current compose's
deploy:block (orupdate_config:) and proposes a config change that makes container-swap health-gated.Q4 — How do test workers (existing mai workers) fit in?
m mentioned "we need test workers included." Two readings:
go testbefore pushing, AND a dedicated mai-test worker runs a broader smoke suite on every main update.(R) = both. Per-worker pre-push tests stay (already-required convention — see Hard rules in every issue brief). The CI runner is a SAFETY NET that catches what individual workers might skip + catches integration issues between worker branches.
A dedicated
mai-testshift (per the existingmai-testskill) can also kick off post-merge as a checker that runs the broader smoke + integration suite + reports back to gitea.Q5 — Migration coordination (the ROOT CAUSE of today's first outage)
A process-level fix that's cheaper than infra:
paliad.applied_migrationsslot availability BEFORE writing the file.if any migration file's slot already exists in applied_migrations with a different name → FAIL the build.(R) = yes, head's slot-reservation behavior is already in flight (today's session). Codify as a Go check in CI + a heads-up to head when filing a task that touches deadline_rules / submission_drafts / projects.
Q6 — Existing prod traffic during deploy
Dokploy + Swarm should keep the previous container serving traffic during the new container's startup window. If the new container fails health checks, the old one keeps serving. Verify + document. If broken: this is the BIGGEST single win — fix
update_config.failure_action: rollbackin the compose.Deliverable
docs/design-cicd-pre-deploy-gate-2026-05-25.mdon branchmai/<inventor>/cicd-design. Sections:Hard rules
mai instruct head. Defaults to (R) recommendations.deploy:block live (ssh mlake docker stack configor read the compose fromm/paliad/docker-compose.yml).mgit.msbls.deGitea version + runners installed).When done
Push design doc +
mai report completedwith "DESIGN READY FOR REVIEW". Inventor stays parked. Head gates coder shift.Out of scope
B1 — dormant test gate made runnable (shift-1, patton/lead)
Branch
mai/patton/b1-make-the-dormant-test(6 commits, pushed). This is the test-side slice of #114.Delivered (all locally green):
make db-test-up→ supabase/postgres:15.8 + schema snapshot + PII-free reference seed + matview populate, printsTEST_DATABASE_URL.docker-compose.test.yml+scripts/db-test-setup.sh(shared by dev + CI).make db-test-down,make refresh-reference-seed.internal/db/testdata/reference-seed.sql= 19 PII-free catalogs (courts, offices, proceeding_types, procedural_events, scenario_flag_catalog, submission_bases/blocks, …), sourced from prod. FixesTestSearchCourtsDB(41 courts), the condition-expr validators, all catalog readers, and the mig-177-class FK-insert gap inverify-migrations. Per-test hermetic seeding for PII-linked rows..gitea/workflows/ci.yml): build +verify-migrations+verify-mig-app+make test+bun test. Does NOT block the deploy webhook. Fatal core = migration gate (green). Full live suite runs visible but non-fatal (continue-on-error) — see residual below.Load-bearing finding: the audit's "~55 dormant tests pass instantly" was empirically false — they never ran, so they carried latent bugs. Fixed the big shared classes (42P08 reused-
$1-as-text ×17 files, droppedusers.rolecolumn, pin project_teams NOT-NULL role, approvalusers.professionfixture ×16 tests, matview/FK/reopen fixtures). ~34 of ~55 now pass for real; 550 service asserts green. Also caught + fixed a genuine latent service bug:LookupEventsselected ambiguous bareidin a JOIN (42702) — exactly what the gate is for.Residual (21 red, made VISIBLE not skipped → B1b): ~13 dead tests vs dropped
paliad.deadline_rules(mig 140) → t-paliad-dead-migration-tests (audit undercounted "2"); 3 data-expectation drift → B4; ~7 genuine bugs/isolation → B1b. Full triage indocs/design-cicd-b1-migration-gate-2026-07-24.md§6.Runner status: workflow fires on push (run #220 — CI wiring confirmed) but concluded
failure: there is no act_runner on mriver (thecicd-runner-setup-2026-05-25.md§2 registration was never actually done); the job landed on the shared mlake shell-executor which lacks go/bun/docker/checkout. Going green on-runner needs the one-time docker-capable runner registration (head/m infra step, documented). Local proof:eval $(make -s db-test-up | tail -2); make verify-migrations verify-mig-app→ green;bun test→ 256 green.Stage 2 (blocking the deploy) is a separate commit — head + m decide, only once reliably green.
paliad's CI executed for the first time — and it is green
Branch
mai/knuth/give-paliad-its-own(t-paliad-ci-runner-own-stack). Not merged; head's call.Runs 640 and 642 both
success. Before today: 274 runs, 274 failures, zero successes.Root cause
Gitea Actions registration tokens are repo-scoped, and a runner only claims jobs for the repo it registered against.
m/paliadhad none. Every run was created, claimed by nobody, marked failed. The workflow YAML was never at fault — an earlier theory blamedactions/checkout@v4and was wrong.The diagnostic is
started_at, not the status badge: a build that fails has a start time; a run no runner claimed does not, and its job-log endpoint answers{"errors":["job not started"]}.Measured, so nobody re-derives it:
m/mGreenhas a repo-scoped runner and goes green;m/mGeohas none and never starts. The user-level runnermdock-nativeserves neither.What shipped
infra/gitea-act-runner/— act_runner 0.2.13 +docker:29-dind, paliad's own Dokploy stack (paliad-act-runner, projectpatholo), deliberately not a third service onmgit-actrunner-lthrhz, which serves yoUPC/youpc.org's working CI. Runnermlake-paliad, Gitea id 13. The file is a reviewable mirror; the authoritative copy is the Dokploy raw compose, and the README carries the deploy, edit and rotation recipes.runs-on: paliad-ci, notself-hosted. Run 637 was claimed bymdock-native, which also advertisesself-hosted, and died onCannot find: node in PATH— it is a host executor with no container. Two runners racing means half of paliad's runs fail for reasons unrelated to paliad. A label only paliad's runner carries settles it.catthehacker/ubuntu:act-22.04carries node 24.18, docker 29.6.1, compose 5.3.1, git 2.54, make, jq, gcc — but not go, bun or psql. A custom image would need a registry mlake does not have, a rebuild owner nobody would be, and a second place pinning the Go version.actions/setup-go@v5withgo-version-file: go.modkeepsgo.modthe only Go pin, so the runner cannot drift from a developer's toolchain. Verified: the job resolvesgo1.24.0, exactlygo.mod. Same pattern is already green on this Gitea inyoUPC/youpc.org.make db-test-up, both commented where they occur: dind speaks plaintext on the stack-private network (the job needs a docker client and the TLS certs cannot reach it), and job containers run in dind's network namespace — which is what makes this workflow's hardcodedlocalhost:15455DSNs correct. The latter pins runner capacity to 1; two concurrent jobs would collide on that port.The gate ran for real
Not a green-by-skip. From run 640's log:
ci-known-failing.txttolerated 0 entries — the ~22 reds that list was built for are gone, so §5's "known gap: live-DB service tests don't run in CI" indocs/cicd-runner-setup-2026-05-25.mdis closed. Whole job: ~2.5 min.Still open, deliberately not done here
verify-migrations,verify-mig-app, both collision checks andci-test-gate.shnever having fired. That window stays unverified, and it is why all six migration-number collisions were caught by humans reading branches by hand.m/mAistill has zero runners, and only escapes visibly failing because it has no workflow files. Any repo underm/that adds one hits this identically.go vetgates that were correctly deferred (a gate on a CI that cannot run looks like coverage and is worse than none) become addable.Commits
0434027— own Dokploy act_runner stack + workflow toolchain6cbdbea— targetpaliad-ci, notself-hostedef3bda3— correct CLAUDE.md and the 2026-05-25 runner doc, which said this was impossibleSweep verdict (2026-07-29): PARTLY.
Stage 1 exists and — since 2026-07-28 — genuinely runs:
.gitea/workflows/ci.ymlon runnermlake-paliad, carrying the migration dry-run, both collision checks, boot smoke, the full Go suite and the frontend build. Before that day every run was created and claimed by nobody (274 of 274 failed unstarted), so treat the 2026-07-24 → 2026-07-28 window as unverified.Stage 2 — the part this title asks for, gating the deploy — is not wired, deliberately. ci.yml's header: "It does NOT block the Dokploy deploy webhook — there is deliberately no deploy job here." So paliad.de is told about a bad deploy, not protected from one.
Full sweep:
docs/findings-issue-sweep-2026-07-29.md(commit4c39886).