migrolint
v0.1.0
Published
Framework-agnostic linter for database migration folders — catch duplicate version numbers, sequence gaps, and missing down-migrations before CI does. Zero dependencies.
Maintainers
Readme
migrolint
A framework-agnostic linter for your database migrations folder. It catches
the boring, expensive mistakes that pass locally and only blow up when CI
actually runs the migrations — duplicate version numbers, sequence gaps, and
up migrations with no matching down. No database connection, no framework
lock-in, zero dependencies.
npx migrolint # auto-detects ./migrations, db/migrations, ...
npx migrolint db/migrations --strictThe problem
Two developers branch off main, each adds 0007_add_index.sql, both merge.
Now your migrations folder has two migrations claiming version 0007. Your
runner picks one and silently skips the other — or aborts the whole deploy.
This is a known, recurring failure
in every sequence-numbered migration tool.
The existing linters (django-migration-linter, Flyway's own checks) are tied
to one framework or need a live database. If you use raw SQL with goose,
dbmate, golang-migrate, or a hand-rolled folder, there's nothing that just
looks at the filenames and tells you they're sane. That's migrolint.
What it checks
| Rule | Severity | Meaning |
|------|----------|---------|
| DUPE_NUM | error | two migrations share a version number (the merge-collision bug) |
| MISSING_DOWN | warning | an up migration has no matching down (only flagged if the project uses up/down splits) |
| SEQ_GAP | warning | a hole in an integer sequence — usually a deleted or un-merged migration |
| BAD_FORMAT | warning | a file whose name no known convention recognizes |
Naming conventions it understands
migrolint reads version numbers out of the filename, across the conventions people actually use:
| Convention | Example |
|------------|---------|
| Flyway | V1__init.sql, U1__undo.sql, R__refresh.sql, V1.1__patch.sql |
| golang-migrate / dbmate | 0001_create_users.up.sql + 0001_create_users.down.sql |
| goose / Rails | 20230101120000_create_users.sql (timestamp prefix) |
| minimalist | 1_init.sql, 2-add-index.sql |
Timestamp-style versions are recognized but exempt from SEQ_GAP (they're not
meant to be contiguous). Well-known non-migration files (schema.rb,
structure.sql, seeds.rb, …) and any non-migration extension are skipped.
Usage
migrolint # scan the first migrations dir it finds
migrolint db/migrations # scan a specific dir
migrolint app/migrations svc/migrations # scan several
migrolint --json # machine-readable, for tooling
migrolint --strict # warnings become errors (exit 1) — good for CI
migrolint --ext .sql,.py # override which extensions count
migrolint --ignore baseline.sql,seed.sqlAs a pre-commit / CI gate
# .git/hooks/pre-commit (or any CI step)
npx migrolint --strict || exit 1# GitHub Actions
- run: npx migrolint --strictExample output
migrolint db/migrations (14 files, 12 migrations)
✗ DUPE_NUM version 0007 used by 2 migrations:
0007_add_index.up.sql
0007_add_orders_fk.up.sql
⚠ MISSING_DOWN 0009_drop_legacy.up.sql — no matching .down file
⚠ SEQ_GAP missing version(s): 5
1 error, 2 warnings.Exit codes
| Code | Meaning |
|------|---------|
| 0 | clean (or only warnings, without --strict) |
| 1 | errors found — or warnings, when --strict is set |
| 2 | usage / IO error (no migrations dir, unreadable path) |
Also available for Python
Same checks, same flags: pip install migrolint
(source: migrolint-py). Both ports
read filenames identically, so a mixed-language team gets the same verdict.
License
MIT
