From cb28021cac602678572a9227f337226e469aec42 Mon Sep 17 00:00:00 2001 From: Eric Dahl Date: Wed, 23 Sep 2026 10:14:15 -0400 Subject: [PATCH] fix: schema-drift check compared against the wrong migrations table and stale Neon DB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent bugs made the schema-drift workflow always fail: 1. collect.ts read migration history from drizzle.__drizzle_migrations, which only exists when 'drizzle-kit migrate' runs directly against a DB. Production applies migrations via the app's own applyCommittedMigrations() (app/lib/db/migrations.ts), which tracks them in public.__clawproxy_migrations instead. The check was comparing against a table that never existed in prod. 2. DRIFT_CHECK_DATABASE_URL (the GitHub secret) was configured against the old Neon database before PR #88 moved auth to Coolify's internal Postgres. Neon stopped receiving writes once that PR shipped, so the check was diffing against a frozen, pre-cutover snapshot -- it would never see the new auth_* tables regardless of what actually merged. Fixes: - collect.ts now reads public.__clawproxy_migrations (matches production's real migration tracking). - Added scripts/schema-drift/apply-migrations.ts + a new db:drift:seed-expected script that seeds the CI throwaway DB using the app's actual migrator, not drizzle-kit migrate, so both sides of the diff use the same mechanism. - Rotated DRIFT_CHECK_DATABASE_URL to a new least-privilege read-only role (clawproxy_drift_ro, SELECT-only) on the current Coolify clawproxy-postgres instance. Verified locally end-to-end against real production: ✓ production matches the migrated schema (50 columns, 21 indexes, 6 migrations) --- .github/workflows/schema-drift.yml | 2 +- package.json | 1 + scripts/schema-drift/apply-migrations.ts | 30 ++++++++++++++++++++++++ scripts/schema-drift/collect.ts | 6 ++++- 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 scripts/schema-drift/apply-migrations.ts diff --git a/.github/workflows/schema-drift.yml b/.github/workflows/schema-drift.yml index 7ccb7b0..43e4121 100644 --- a/.github/workflows/schema-drift.yml +++ b/.github/workflows/schema-drift.yml @@ -38,7 +38,7 @@ jobs: - name: Build the expected schema from committed migrations env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres - run: yarn db:migrate + run: yarn db:drift:seed-expected - name: Compare production against it env: EXPECTED_DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres diff --git a/package.json b/package.json index b5cb229..5b45ee5 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:drift": "tsx scripts/schema-drift/check.ts", + "db:drift:seed-expected": "tsx scripts/schema-drift/apply-migrations.ts", "db:push": "drizzle-kit push" }, "dependencies": { diff --git a/scripts/schema-drift/apply-migrations.ts b/scripts/schema-drift/apply-migrations.ts new file mode 100644 index 0000000..a6de431 --- /dev/null +++ b/scripts/schema-drift/apply-migrations.ts @@ -0,0 +1,30 @@ +/** + * Build the "expected" schema for drift comparison using the exact same migration + * mechanism production uses (app/lib/db/migrations.ts -> public.__clawproxy_migrations), + * rather than `drizzle-kit migrate` which tracks in a different table + * (drizzle.__drizzle_migrations) that production never creates. + */ +import postgres from 'postgres'; + +import { applyCommittedMigrations } from '../../app/lib/db/migrations'; + +async function main() { + const url = process.env.DATABASE_URL; + if (!url) { + console.error('DATABASE_URL is required'); + process.exit(2); + } + + const sql = postgres(url, { max: 1 }); + try { + await applyCommittedMigrations(sql); + console.log('[schema-drift] applied committed migrations to the expected database'); + } finally { + await sql.end({ timeout: 5 }); + } +} + +main().catch((error) => { + console.error('[schema-drift] failed to apply migrations:', error); + process.exit(2); +}); diff --git a/scripts/schema-drift/collect.ts b/scripts/schema-drift/collect.ts index 3cb5e30..e35ad38 100644 --- a/scripts/schema-drift/collect.ts +++ b/scripts/schema-drift/collect.ts @@ -38,8 +38,12 @@ export async function collectSnapshot(databaseUrl: string): Promise` - SELECT hash FROM drizzle.__drizzle_migrations ORDER BY created_at, id + SELECT hash FROM public.__clawproxy_migrations ORDER BY applied_at, name `; return {