Files
paliad/internal/db/testdata/README.md
mAi c901293c9c
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
feat(cicd): Slice A — pre-deploy gate + role-split migration smoke
Adds .gitea/workflows/test.yaml that gates every push on `go build`,
`bun run build`, `go vet`, the migration coordination check, and the
role-split end-to-end migration smoke. On push to main + green, calls
Dokploy's compose.deploy API and polls /health/ready until 200.

t-paliad-282 / m/paliad#114. Design: docs/design-cicd-pre-deploy-gate-2026-05-25.md
(inventor shift on mai/cronus/inventor-ci-cd-pre).

Catches all three of today's outage classes:

  brunel (~13:20) slot collision     -> TestMigrations_NoDuplicateSlot
  hermes (~16:05) dropped-col refs   -> TestBootSmoke
  mig 129 (~14:56) 42501 ownership   -> TestMigrations_EndToEndAsAppRole

Snapshot approach. internal/db/testdata/prod-snapshot.sql is a pg_dump
of youpc-supabase paliad schema + applied_migrations rows. CI restores
this into a fresh `supabase/postgres:15.8.1.060` (same image, same role
topology as prod) and runs ApplyMigrations as the `postgres` role
(which is NOT a superuser on supabase/postgres, matching prod). Existing
migrations are skipped (already in applied_migrations); only NEW migs
from the PR run end-to-end. This sidesteps the fresh-DB idempotence
debt in some historical migrations (mig 037 missing pg_trgm, mig 051
inner COMMIT) — those are tracked separately and don't block the gate.

Sub-changes:

- internal/handlers/handlers.go — new /health/ready endpoint distinct
  from /healthz. /healthz stays liveness (process alive, no DB); /ready
  is readiness (DB pool pings within 2 s). Returns 503 when svc or pool
  is nil (DB-less deploys are intentionally not-ready). svc.Pool added
  to handlers.Services, wired in cmd/server/main.go.

- internal/db/migrate_test.go — TestMigrations_NoDuplicateSlot (pure
  unit, catches brunel) and TestMigrations_EndToEndAsAppRole (snapshot-
  gated, catches the 42501 class).

- cmd/server/main_smoke_test.go — TestBootSmoke now also asserts
  /health/ready returns 503 with a nil svc. New TestHealthReady_Live
  asserts 200 against a live pool.

- internal/db/migrations/024_rename_department_columns.up.sql and
  027_rename_to_partner_units.up.sql — ALTER INDEX / ALTER POLICY
  exception handlers now catch undefined_object OR undefined_table OR
  duplicate_object. Old handler only caught undefined_object; Postgres
  raises undefined_table when source object never existed, and
  duplicate_object when destination already exists. The expanded
  handlers make these migrations truly idempotent across all plausible
  starting states.

- Makefile — verify-mig-app, test-frontend, refresh-snapshot targets.
  refresh-snapshot pg_dumps youpc-supabase prod (needs PALIAD_PROD_DATABASE_URL),
  strips pg16 \restrict commands for pg15 restore compat, and filters
  applied_migrations rows to this branch's max on-disk version.

- internal/db/testdata/README.md — explains the snapshot's purpose,
  refresh procedure, and how to verify locally.

- docs/cicd-runner-setup-2026-05-25.md — one-time admin steps for
  registering a Gitea Actions runner on mriver and wiring DOKPLOY_TOKEN
  as a repo secret. Documents soft-launch plan per m's Q11.4 (keep
  Dokploy's autoDeploy=true webhook alive for one week, disable after
  the workflow has gated 5 successful deploys).

Build clean. Full go test ./internal/... ./cmd/... green without
TEST_DATABASE_URL. With TEST_DATABASE_URL + TEST_APP_DATABASE_URL set
to a supabase/postgres scratch + snapshot restored:
TestMigrations_NoDuplicateSlot, TestMigrations_EndToEndAsAppRole,
TestBootSmoke, TestHealthReady_Live all pass. Live-DB service tests in
internal/services/* fail under supabase/postgres 15.8 with a 42P08
parameter-binding error (unrelated to Slice A — tracked as a follow-up).
2026-05-25 17:42:06 +02:00

3.4 KiB

internal/db/testdata/ — CI snapshot

prod-snapshot.sql

Schema-only pg_dump of paliad's prod DB (youpc-supabase paliad schema) plus the rows of paliad.applied_migrations that match this branch's on-disk migration set.

Purpose. Lets CI's migration smoke (.gitea/workflows/test.yaml) restore a Postgres scratch DB to "paliad at HEAD-of-snapshot" without having to replay 131 migrations from scratch. ApplyMigrations on the restored DB sees the applied set and only runs whatever NEW migrations this PR adds — exactly the integration shape we want to test, and the same shape prod sees on every deploy.

Why a snapshot at all. Running ApplyMigrations from scratch against a fresh supabase/postgres:15.8.1.060 surfaces multiple fresh-DB idempotence bugs in historical migrations (raw COMMIT; in mig 051, missing CREATE EXTENSION pg_trgm for mig 037, ALTER POLICY exception-handler gaps in mig 024/027 — the last is fixed in this PR). Fixing them all is a separate cleanup. The snapshot sidesteps them by starting CI from a state where every historical migration is already applied as it was in prod.

Schema scope. --schema=paliad only. Auth schema comes baked into supabase/postgres; CI's setup step installs pg_trgm before restoring.

Ownership. --no-owner --no-privileges keeps the dump portable across role topologies (CI's supabase_admin / postgres / authenticated / anon don't have to match prod's exact role layout). The role-split smoke relies on postgres being a non-superuser, which is true on supabase/postgres by default.

Refresh. Run make refresh-snapshot with PALIAD_PROD_DATABASE_URL set to a Postgres URL with pg_dump rights on youpc-supabase. The target appends data rows for paliad.applied_migrations, strips \restrict / \unrestrict commands (pg 16 dump → pg 15 restore), and filters out applied-migrations rows for versions beyond the branch's local max. The CI workflow consumes the resulting file verbatim.

Verify a refresh. Boot a local scratch:

docker run -d --rm --name paliad-snap \
  -e POSTGRES_PASSWORD=ci -e POSTGRES_DB=paliad_scratch \
  -p 15433:5432 supabase/postgres:15.8.1.060
sleep 5
docker exec -e PGPASSWORD=ci paliad-snap psql -h localhost -U supabase_admin -d paliad_scratch \
  -c "GRANT CREATE ON DATABASE paliad_scratch TO postgres;" \
  -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
cat internal/db/testdata/prod-snapshot.sql | docker exec -i -e PGPASSWORD=ci paliad-snap \
  psql -h localhost -U postgres -d paliad_scratch -v ON_ERROR_STOP=1
TEST_DATABASE_URL="postgres://postgres:ci@localhost:15433/paliad_scratch?sslmode=disable" \
TEST_APP_DATABASE_URL="postgres://postgres:ci@localhost:15433/paliad_scratch?sslmode=disable" \
  go test -count=1 -run 'TestMigrations|TestBootSmoke|TestHealthReady_Live' ./internal/db/ ./cmd/server/
docker stop paliad-snap

All four named tests must pass. If any fails after a refresh, investigate before merging — usually because a new migration was added to prod that this branch doesn't have on disk yet.

Why is the snapshot not gzipped? Small enough (~200 KB) that the diff stays human-readable in git diff reviews. If it crosses ~1 MB, gzip + decompress-on-restore in CI.

Privacy. Schema-only dump, no row data from any paliad table (except paliad.applied_migrations, which contains migration filenames + checksums — public info already in the repo).