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).
132 lines
8.4 KiB
SQL
132 lines
8.4 KiB
SQL
-- t-paliad-070: Rename departments → partner_units across the schema, drop
|
|
-- the legacy users.dezernat free-text column, and add the
|
|
-- partner_unit_events audit table.
|
|
--
|
|
-- Order of operations matters because constraint names are owned by their
|
|
-- owning table; we rename the table first, then the columns/constraints/
|
|
-- policies that postgres did not auto-rename.
|
|
--
|
|
-- Idempotent renames (DO $$ EXCEPTION WHEN undefined_object $$) are used
|
|
-- for constraint/index/policy steps so re-runs after a partial apply on
|
|
-- freshly-provisioned DBs (e.g. test DBs that may have run earlier
|
|
-- migrations under different names) do not abort the chain.
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 1. Best-effort second seed of department_members from the legacy
|
|
-- users.dezernat free-text field. Mirror of migration 019 — re-run before
|
|
-- DROP COLUMN to capture any drift since 019 ran. Idempotent via
|
|
-- ON CONFLICT DO NOTHING.
|
|
-- ---------------------------------------------------------------------------
|
|
INSERT INTO paliad.departments (id, name, lead_user_id, office, created_at, updated_at)
|
|
SELECT gen_random_uuid(),
|
|
btrim(u.dezernat),
|
|
NULL,
|
|
MIN(u.office),
|
|
now(),
|
|
now()
|
|
FROM paliad.users u
|
|
WHERE u.dezernat IS NOT NULL
|
|
AND btrim(u.dezernat) <> ''
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM paliad.departments d2 WHERE d2.name = btrim(u.dezernat)
|
|
)
|
|
GROUP BY btrim(u.dezernat)
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO paliad.department_members (department_id, user_id, created_at)
|
|
SELECT d.id, u.id, now()
|
|
FROM paliad.users u
|
|
JOIN paliad.departments d
|
|
ON d.name = btrim(u.dezernat)
|
|
WHERE u.dezernat IS NOT NULL
|
|
AND btrim(u.dezernat) <> ''
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 2. Drop the legacy free-text column. The structured side
|
|
-- (department_members) is the source of truth from here on.
|
|
-- ---------------------------------------------------------------------------
|
|
ALTER TABLE paliad.users DROP COLUMN IF EXISTS dezernat;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 3. Rename tables.
|
|
-- ---------------------------------------------------------------------------
|
|
ALTER TABLE paliad.departments RENAME TO partner_units;
|
|
ALTER TABLE paliad.department_members RENAME TO partner_unit_members;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 4. Rename junction column.
|
|
-- ---------------------------------------------------------------------------
|
|
ALTER TABLE paliad.partner_unit_members RENAME COLUMN department_id TO partner_unit_id;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 5. Rename constraints. Postgres auto-renames the underlying index for
|
|
-- pkey/uniq constraints; standalone indexes are renamed in step 6.
|
|
-- ---------------------------------------------------------------------------
|
|
DO $$ BEGIN ALTER TABLE paliad.partner_units RENAME CONSTRAINT departments_pkey TO partner_units_pkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
DO $$ BEGIN ALTER TABLE paliad.partner_units RENAME CONSTRAINT departments_lead_user_id_fkey TO partner_units_lead_user_id_fkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
DO $$ BEGIN ALTER TABLE paliad.partner_units RENAME CONSTRAINT departments_office_check TO partner_units_office_check; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
DO $$ BEGIN ALTER TABLE paliad.partner_unit_members RENAME CONSTRAINT department_members_pkey TO partner_unit_members_pkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
DO $$ BEGIN ALTER TABLE paliad.partner_unit_members RENAME CONSTRAINT department_members_department_id_fkey TO partner_unit_members_partner_unit_id_fkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
DO $$ BEGIN ALTER TABLE paliad.partner_unit_members RENAME CONSTRAINT department_members_user_id_fkey TO partner_unit_members_user_id_fkey; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 6. Rename non-pkey indexes.
|
|
-- ---------------------------------------------------------------------------
|
|
DO $$ BEGIN ALTER INDEX paliad.departments_office_idx RENAME TO partner_units_office_idx; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
DO $$ BEGIN ALTER INDEX paliad.departments_lead_idx RENAME TO partner_units_lead_idx; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
DO $$ BEGIN ALTER INDEX paliad.department_members_user_idx RENAME TO partner_unit_members_user_idx; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 7. Rename RLS policies.
|
|
-- ---------------------------------------------------------------------------
|
|
DO $$ BEGIN ALTER POLICY departments_select ON paliad.partner_units RENAME TO partner_units_select; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
DO $$ BEGIN ALTER POLICY departments_write ON paliad.partner_units RENAME TO partner_units_write; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
DO $$ BEGIN ALTER POLICY department_members_select ON paliad.partner_unit_members RENAME TO partner_unit_members_select; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
DO $$ BEGIN ALTER POLICY department_members_write ON paliad.partner_unit_members RENAME TO partner_unit_members_write; EXCEPTION WHEN undefined_object OR undefined_table OR duplicate_object THEN NULL; END $$;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 8. Audit table for partner-unit events. Mutations on partner_units +
|
|
-- partner_unit_members emit one row each, written in the same tx by
|
|
-- PartnerUnitService. The viewer in audit_service.go unions this source
|
|
-- in alongside project_events / caldav_sync_log / reminder_log.
|
|
--
|
|
-- partner_unit_id is nullable + ON DELETE SET NULL so the historical
|
|
-- 'deleted' event survives the cascade that removes the unit row.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE paliad.partner_unit_events (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
partner_unit_id uuid NULL REFERENCES paliad.partner_units(id) ON DELETE SET NULL,
|
|
actor_id uuid NOT NULL REFERENCES auth.users(id),
|
|
event_type text NOT NULL CHECK (event_type IN (
|
|
'created', 'updated', 'deleted', 'member_added', 'member_removed'
|
|
)),
|
|
-- Snapshot of the unit's name at event time so deleted units still show
|
|
-- a human-readable label in the audit timeline (partner_unit_id is NULL
|
|
-- on deleted, so we can't JOIN through to partner_units.name).
|
|
unit_name text NOT NULL,
|
|
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX partner_unit_events_unit_idx ON paliad.partner_unit_events(partner_unit_id, created_at DESC);
|
|
CREATE INDEX partner_unit_events_actor_idx ON paliad.partner_unit_events(actor_id, created_at DESC);
|
|
CREATE INDEX partner_unit_events_time_idx ON paliad.partner_unit_events(created_at DESC);
|
|
|
|
-- RLS: read access matches /api/partner-units (any authenticated user);
|
|
-- writes only by global_admin (defence-in-depth — the service already
|
|
-- gates with requireAdmin).
|
|
ALTER TABLE paliad.partner_unit_events ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY partner_unit_events_select ON paliad.partner_unit_events
|
|
FOR SELECT USING (auth.uid() IS NOT NULL);
|
|
|
|
CREATE POLICY partner_unit_events_write ON paliad.partner_unit_events
|
|
FOR INSERT WITH CHECK (
|
|
EXISTS (
|
|
SELECT 1 FROM paliad.users u
|
|
WHERE u.id = auth.uid()
|
|
AND u.global_role = 'global_admin'
|
|
)
|
|
);
|