459d636ffb
A long-lived branch can add a migration whose timestamped filename sorts BEFORE migrations already applied in prod (#234's 20260627T130000-ai-chat-runs merged after 20260704T120000-client-metrics was live). Kysely's migrator with the default ordered setting then rejects the applied set as "corrupted migrations" (no longer a prefix of the sorted list), throws, and the app crash-loops on boot — exactly incident #361 (502s for ~11 min after a develop deploy). #119 and #120 (June branches) are the next such threats. Two levels, both: 1. CI migration-order gate (a new `migration-order` job in test.yml, PR-only): fails the PR when an added migration sorts at/before the newest migration on the base branch, with an actionable message to rename it to a current timestamp before merge. This is the primary defense — makes back-dating impossible to merge accidentally. 2. `allowUnorderedMigrations: true` on BOTH Migrators (migration.service.ts startup auto-migrate + migrate.ts CLI): the runtime safety net — Kysely applies a not-yet-applied older migration instead of bricking startup, so a back-dated migration that bypasses the gate (manual push / hotfix branch) still boots. Trade-off documented inline: apply order across instances may differ from lexicographic, so migrations must stay independent (ours each create their own objects); the CI gate remains the primary line. Verified: allowUnorderedMigrations is a valid Kysely 0.28.17 Migrator option; server tsc clean; the gate script rejects a back-dated filename and passes a current one. No new deps, no migration, no runtime behavior change beyond the migrator resilience. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
34 lines
1.0 KiB
TypeScript
34 lines
1.0 KiB
TypeScript
import * as path from 'path';
|
|
import { promises as fs } from 'fs';
|
|
import { Kysely, Migrator, FileMigrationProvider } from 'kysely';
|
|
import { run } from 'kysely-migration-cli';
|
|
import * as dotenv from 'dotenv';
|
|
import { envPath, normalizePostgresUrl } from '../common/helpers';
|
|
import { PostgresJSDialect } from 'kysely-postgres-js';
|
|
import postgres from 'postgres';
|
|
|
|
dotenv.config({ path: envPath });
|
|
|
|
const migrationFolder = path.join(__dirname, './migrations');
|
|
|
|
const db = new Kysely<any>({
|
|
dialect: new PostgresJSDialect({
|
|
postgres: postgres(normalizePostgresUrl(process.env.DATABASE_URL)),
|
|
}),
|
|
});
|
|
|
|
const migrator = new Migrator({
|
|
db,
|
|
provider: new FileMigrationProvider({
|
|
fs,
|
|
path,
|
|
migrationFolder,
|
|
}),
|
|
// Match the startup auto-migrator (migration.service.ts): a back-dated
|
|
// migration from a long-lived branch must be applied, not rejected as
|
|
// "corrupted migrations" (incident #361). See that file for the full rationale.
|
|
allowUnorderedMigrations: true,
|
|
});
|
|
|
|
run(db, migrator, migrationFolder);
|