Mainfast

Mainfast Migrations

Free, MIT

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.

PR comment: two Alembic heads if this PR merges, with the command to fix it.

Install

GitHub Actions workflowyaml
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@v0
Make it block mergestext
After the first run, add mainfast/migrations under
Settings > Branches > main > Require status checks to pass
(or the ruleset equivalent).
Optional configyaml
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"
CLIsh
npx mainfast-migrations check --main origin/main --head HEAD          # exit 1 on errors
npx mainfast-migrations check --format md

Step 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.

Install Mainfast Migrations

Fail a PR whose migrations would fork the history once merged into main.