diff --git a/package.json b/package.json index 3f1ab2987..d8b0d9a96 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "check:rpc-exists": "node scripts/check-rpc-exists.mjs", "check:ai-models": "node scripts/check-ai-models.mjs", "check:mdx": "node scripts/check-mdx.mjs", - "verify": "npm run ci:docs && npm run check:accent-ink && npm run type-check && npm run check:sizes && npm run audit:routes && npm run lint && npm run check:duplication && npm run check:dead-fields && npm run check:schema-columns && npm run check:currency-units && npm run check:rpc-exists && npm run check:mdx && npm run test:unit -- --watchAll=false", + "verify": "npm run ci:docs && npm run check:accent-ink && npm run type-check && npm run check:sizes && npm run audit:routes && npm run lint && npm run check:duplication && npm run check:dead-fields && npm run check:migration-versions && npm run check:schema-columns && npm run check:currency-units && npm run check:rpc-exists && npm run check:mdx && npm run test:unit -- --watchAll=false", "audit:schema": "node scripts/db/audit-schema-drift.mjs", "audit:routes": "node scripts/audit-routes.mjs", "gen:types": "bash scripts/db/gen-types.sh", @@ -105,7 +105,8 @@ "test:e2e:matrix": "playwright test tests/e2e/workflow-matrix.spec.ts --project=chromium --reporter=line", "test:e2e:matrix:p0": "playwright test tests/e2e/workflow-matrix.spec.ts --project=chromium --grep @p0 --reporter=line", "db:audit": "node scripts/db-audit.mjs", - "eval:voice": "node scripts/eval-voice-routing.mjs" + "eval:voice": "node scripts/eval-voice-routing.mjs", + "check:migration-versions": "node scripts/check-migration-versions.mjs" }, "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.4", diff --git a/scripts/check-migration-versions.mjs b/scripts/check-migration-versions.mjs new file mode 100644 index 000000000..14eed1ba3 --- /dev/null +++ b/scripts/check-migration-versions.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node +/** + * One migration per version number. + * + * Supabase keys its ledger on the numeric prefix of the filename + * (supabase_migrations.schema_migrations.version, a PRIMARY KEY), not on the + * whole name. So two files that merely SHARE a prefix are the same migration as + * far as the ledger is concerned, and replaying them dies on: + * + * ERROR: duplicate key value violates unique constraint "schema_migrations_pkey" + * Key (version)=(20260826180000) already exists. + * + * That is not a theoretical clash. It happened on 2026-08-26: two sessions + * working in parallel each timestamped a migration 20260826180000 + * (mention_queue_all_mentions, resolve_username_history_rpc), main went red, + * and CD stopped deploying for the whole repo — including the deploy carrying + * the fix each of them was waiting on. + * + * git does not catch it, because neither file conflicts with the other: they + * have different names and different contents. Only the ledger notices, and by + * then it is on main. So this runs in CI, where a collision costs one red PR + * check instead of a blocked pipeline. + * + * Run: node scripts/check-migration-versions.mjs + */ + +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +const DIR = join(process.cwd(), 'supabase', 'migrations'); + +const byVersion = new Map(); +for (const name of readdirSync(DIR)) { + if (!name.endsWith('.sql')) continue; + const version = name.match(/^(\d+)/)?.[1]; + if (!version) { + console.error(`✗ ${name} has no leading version number — supabase cannot record it`); + process.exit(1); + } + if (!byVersion.has(version)) byVersion.set(version, []); + byVersion.get(version).push(name); +} + +const collisions = [...byVersion.entries()].filter(([, files]) => files.length > 1); + +if (collisions.length > 0) { + console.error('✗ migration version collision — supabase_migrations.schema_migrations.version is a PRIMARY KEY,'); + console.error(' so these files are the same migration to the ledger and a replay will abort:\n'); + for (const [version, files] of collisions) { + console.error(` ${version}:`); + for (const f of files) console.error(` ${f}`); + } + console.error('\n Fix: bump one filename to an unused timestamp. Nothing else needs to change.'); + process.exit(1); +} + +console.log(`✓ migration versions unique (${byVersion.size} migrations)`); diff --git a/supabase/migrations/20260826180000_resolve_username_history_rpc.sql b/supabase/migrations/20260826181500_resolve_username_history_rpc.sql similarity index 100% rename from supabase/migrations/20260826180000_resolve_username_history_rpc.sql rename to supabase/migrations/20260826181500_resolve_username_history_rpc.sql