Mainfast Migrations
Two PRs each add a migration on top of the same main. Each is green alone. Merge the first and the second is broken, but nothing re-runs its checks, so it merges and the next deploy fails. Mainfast Migrations checks a PR against main as it is now, and re-checks every open PR when main moves.
What it catches
- Alembic: multiple-heads, missing-parent, duplicate-revision.
- Django: multiple-leaves, missing-dependency, duplicate-number.
- Prisma: duplicate-timestamp and out-of-order (an error by default).
- Rails: duplicate-version, duplicate-name, and out-of-order (a warning by default).
- Flyway: duplicate-version and out-of-order (a warning by default, since some teams run outOfOrder=true).
- Drizzle: journal-fork, duplicate-idx, sql-not-in-journal, entry-without-sql, snapshot-fork and more.
- When main moves, every open PR is re-checked; stale ones get a failing commit status, a comment and a label. Every finding has a one-line fix.
- Problems main already had before the PR are listed separately, not blamed on it.
What you see
A real PR comment from a private test repository, where a branch forked the Alembic history.
Install
name: Mainfast Migrations
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
pull-requests: write # sticky comment + stale label
statuses: write # the blocking mainfast/migrations commit status
jobs:
migrations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
fetch-depth: 0
- uses: mainfast/migrations@v0After the first run, add mainfast/migrations under
Settings > Branches > main > Require status checks to pass
(or the ruleset equivalent).migrations:
frameworks: auto # or [alembic, django, prisma, rails, flyway, drizzle]
ignore: [legacy/**]
outOfOrder: # per framework; or one value for all: error | warning | off
rails: error
prisma: off
label: "mainfast: migrations stale"npx mainfast-migrations check --main origin/main --head HEAD # exit 1 on errors
npx mainfast-migrations check --format mdStep by step, with what to check after each part: installation manual. Everything else: the README.
Permissions
- contents: read: fetch main and refs/pull/<n>/head. Without it the check cannot run.
- pull-requests: write: the sticky PR comment and the stale label. Without it they are skipped with a warning; summary and annotations still carry the result.
- statuses: write: the mainfast/migrations commit status. Without it there is no status, so nothing blocks the merge.
- Only the commit status blocks a merge, and only once branch protection or a ruleset requires it. The comment and label only tell people.
- On fork PRs the token is read-only: comment, label and status are skipped with a warning and the job summary carries the result.
- The status is on by default; status: false turns it off (comment and label only, then nothing blocks).
Privacy
- It reads git with ls-tree and cat-file on refs only: no checkout of PR code is run and nothing is written to the working tree.
- The only calls are to the GitHub API with your GITHUB_TOKEN (open PRs, comment, label, status). Nothing is sent to a third party; with a licence key, only the key and the organisation id go to Polar to check it.
- The config is read from main, so a PR cannot switch its own check off.
- Branch names, file and revision names and PR titles are escaped before they reach a comment or summary, and @mentions in them are neutralised.
Limits
- Django cross-app dependencies are not validated.
- Alembic depends_on is ignored; with branch_labels present, multiple-heads drops to a warning.
- Flyway repeatable (R__), undo (U) and Java migrations are ignored.
- Rails schema.rb version and Prisma migration_lock.toml are not inspected.
- extends: in .mainfast.yml is accepted but not applied yet.
- A content conflict inside one migration file is not its job; Mainfast Intact covers what a merge dropped.
- The merge-base must be reachable, so check out with fetch-depth: 0.
More in Mainfast
Install Mainfast Migrations
Fail a PR whose migrations would fork the history once merged into main.
