diff --git a/package.json b/package.json index ea65394910..f096795576 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "ios": "cd apps/expo && bun ios", "lefthook": "lefthook install", "lint": "biome check --write", - "lint:custom": "bun run scripts/lint/no-raw-typeof.ts && bun run scripts/lint/no-raw-regex.ts && bun run packages/env/scripts/no-raw-process-env.ts && bun run scripts/lint/no-duplicate-guards.ts && bun run scripts/lint/no-unauth-routes.ts", + "lint:custom": "bun run scripts/lint/no-raw-typeof.ts && bun run scripts/lint/no-raw-regex.ts && bun run packages/env/scripts/no-raw-process-env.ts && bun run scripts/lint/no-duplicate-guards.ts && bun run scripts/lint/no-unauth-routes.ts && bun run scripts/lint/check-drizzle-migrations.ts", "lint:strict": "biome check && bun run lint:custom", "lint-unsafe": "biome check --write --unsafe", "mcp": "bun run --cwd packages/mcp dev", diff --git a/scripts/check-all.ts b/scripts/check-all.ts index 8bba7e6e46..5df6de6a9b 100644 --- a/scripts/check-all.ts +++ b/scripts/check-all.ts @@ -82,6 +82,10 @@ const ALL_CHECKS: CheckDef[] = [ name: 'no-unauth-routes', script: join(ROOT, 'scripts', 'lint', 'no-unauth-routes.ts'), }, + { + name: 'check-drizzle-migrations', + script: join(ROOT, 'scripts', 'lint', 'check-drizzle-migrations.ts'), + }, { name: 'check-type-casts', script: join(ROOT, 'packages', 'checks', 'src', 'check-type-casts.ts'), diff --git a/scripts/lint/check-drizzle-migrations.ts b/scripts/lint/check-drizzle-migrations.ts new file mode 100644 index 0000000000..d4d5c1caa9 --- /dev/null +++ b/scripts/lint/check-drizzle-migrations.ts @@ -0,0 +1,60 @@ +#!/usr/bin/env bun + +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const ROOT = join(import.meta.dir, '..', '..'); + +const MIGRATION_TARGETS = [ + { name: 'packages/api', allowManual: new Set(['0010_great_colleen_wing.sql']) }, + { name: 'packages/osm-db', allowManual: new Set(['0000_extensions.sql']) }, +]; + +const DRIZZLE_FILE_PATTERN = /^\d{4}_[a-z0-9]+(?:_[a-z0-9]+)+\.sql$/; +const DRIZZLE_TEMPLATE_COMMENT = '-- Custom SQL migration file, put your code below! --'; + +interface Violation { + packageName: string; + message: string; +} + +function checkTarget(target: (typeof MIGRATION_TARGETS)[number], violations: Violation[]): void { + const drizzleDir = join(ROOT, target.name, 'drizzle'); + if (!existsSync(drizzleDir)) return; + + const sqlFiles = readdirSync(drizzleDir) + .filter((file) => file.endsWith('.sql')) + .sort(); + + for (const file of sqlFiles) { + if (target.allowManual.has(file)) continue; + + if (!DRIZZLE_FILE_PATTERN.test(file)) { + violations.push({ + packageName: target.name, + message: `${file}: migration name must match drizzle-kit format (NNNN_word_word.sql)`, + }); + } + + const content = readFileSync(join(drizzleDir, file), 'utf-8'); + if (content.includes(DRIZZLE_TEMPLATE_COMMENT)) { + violations.push({ + packageName: target.name, + message: `${file}: contains drizzle template comment; regenerate via drizzle-kit instead of hand-writing`, + }); + } + } +} + +const violations: Violation[] = []; +for (const target of MIGRATION_TARGETS) checkTarget(target, violations); + +if (violations.length > 0) { + console.log(`Drizzle migration checks failed (${violations.length}):\n`); + for (const violation of violations) { + console.log(`${violation.packageName}: ${violation.message}`); + } + process.exit(1); +} + +console.log('Drizzle migration checks passed.');