Mainfast

Mainfast Intact

Free, MIT

When you merge main into a feature branch, a conflict resolved by taking ours, or a hand-merge that loses a line, reverts main's work without a trace. GitHub's PR diff never shows what the merge commit itself did. Mainfast Intact re-runs each merge and reports what it dropped.

What it catches

  • dropped: a hunk main added (or removed) since the merge base that the merge commit no longer carries, with the main commit that introduced it.
  • partially-dropped: part of such a hunk is gone.
  • evil: the merge commit contains changes found in neither parent and outside any conflict.
  • Lockfiles are checked by default: taking the branch's lockfile wholesale can undo a dependency bump, often a security fix.
  • Dropped changes are matched by tokens, not lines, so a line that gained a trailing comment is fine but a value changed back is not.
  • Default levels: dropped (main) is an error, partially-dropped and evil are warnings, dropping the branch's own change is a notice.

What you see

A real check run from a private test repository, where a merge commit dropped a change from main.

Check run: one error, a merge commit dropped a line changed on main.

Install

GitHub Actions workflowyaml
name: Mainfast Intact
on:
  pull_request:
  push:
    branches-ignore: [main]
permissions:
  contents: read
  checks: write
jobs:
  intact:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
        with:
          fetch-depth: 0
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - uses: mainfast/intact@v0
CLI, from a clone of the suite repositorysh
git clone https://github.com/mainfast/mainfast
# from the cloned suite root:
npm ci && npm run build -w intact
ln -s "$PWD/intact/dist/cli.cjs" ~/.local/bin/mainfast
mainfast check                       # every merge commit in <main>..HEAD
mainfast check --range origin/main..feature
mainfast check --json --fail-on error
Optional configyaml
version: 1
intact:
  ignore:                      # gitignore-style globs
    - "dist/"
  severity:                    # first match wins: error, warning, notice, off
    "docs/**": notice
    "migrations/**": error

Step by step, with what to check after each part: installation manual. Everything else: the README.

Permissions

  • Needs only GITHUB_TOKEN, with contents: read and checks: write.
  • With checks: write it posts a check run with annotations. Without it (fork PRs) it writes the job summary and annotations instead.
  • The Action reads .mainfast.yml from the base of the change, so a pull request cannot ignore its own dropped change. The CLI reads your working tree.
  • The CLI needs Node 20+ and git. The Action unshallows a shallow checkout automatically; fetch-depth: 0 is faster.
  • Reading a private shared config with extends needs a config-token input; GITHUB_TOKEN cannot read other private repositories.

Privacy

  • It analyses git history on the runner (or your machine for the CLI) using git remerge-diff, merge-tree and blame.
  • The Action calls only the GitHub API with your GITHUB_TOKEN. No code or findings are sent to a third party; with a licence key, only the key and the organisation id go to Polar to check it.
  • The plan notice counts active contributors through the GitHub API with your token, once per run.

Limits

  • Octopus merges (3+ parents) are reported as unsupported and skipped.
  • It is a heuristic, not proof: token matching can miss a drop, and can flag a resolution that rewrote main's change in different words.
  • Criss-cross merges measure from the first merge base, so expect extra notes there.
  • Semantic fix-ups inside a merge commit, such as updating a renamed call site, are evil merges by definition and reported as warnings.
  • Binary files are only checked for being reverted to the merge-base blob.
  • Rename detection uses git's default 50% similarity. Paths containing newlines or tabs are not supported.
  • Only the merge commit is judged; a later commit that changes main's line again is ordinary history.

Install Mainfast Intact

Prove that a merge commit did not silently drop changes from main.