-
Notifications
You must be signed in to change notification settings - Fork 38
Enforce drizzle-kit generated migrations with a custom lint check #2430
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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}`); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+53
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win Print per-package regeneration commands in failures. Current output reports the violation but not the exact recovery command, which slows remediation. Proposed improvement if (violations.length > 0) {
console.log(`Drizzle migration checks failed (${violations.length}):\n`);
for (const violation of violations) {
console.log(`${violation.packageName}: ${violation.message}`);
}
+ console.log('\nSuggested fix commands:');
+ for (const pkg of new Set(violations.map((v) => v.packageName))) {
+ console.log(` bun --cwd ${pkg} drizzle-kit generate`);
+ }
process.exit(1);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| console.log('Drizzle migration checks passed.'); | ||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Auto-discover drizzle targets to avoid silent policy gaps.
Line 8 hard-codes
MIGRATION_TARGETS, so newpackages/*/drizzlefolders can bypass this check until someone updates this file.Proposed refactor
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 ALLOW_MANUAL_BY_PACKAGE = new Map<string, Set<string>>([ + ['packages/api', new Set(['0010_great_colleen_wing.sql'])], + ['packages/osm-db', new Set(['0000_extensions.sql'])], +]); + +const MIGRATION_TARGETS = readdirSync(join(ROOT, 'packages'), { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => `packages/${d.name}`) + .filter((name) => existsSync(join(ROOT, name, 'drizzle'))) + .map((name) => ({ + name, + allowManual: ALLOW_MANUAL_BY_PACKAGE.get(name) ?? new Set<string>(), + }));🤖 Prompt for AI Agents