Skip to content
Active
Tools
Open Source
Aug 2026

merge-gate

The policy engine that decides which of ~100 open pull requests may merge without a human. Shape is computed from the diff, never eyeballed — and it refuses to arm auto-merge on anything, ever.

49 passing
Tests
~70
Repos governed
Zero
Arms granted
3 constants
Policy surface
merge-gate project visual

The constraint

A fleet of ~70 repos produces more pull requests than one person can read — mostly dependency bumps, mostly safe, occasionally not. Reviewing by eye does not scale, and the obvious shortcut is worse than the problem: GitHub's auto-merge grants standing permission to merge future content based on a judgement about past content.

The approach

Classify every PR by the shape of its diff, and merge only the shapes that cannot carry behaviour: lockfiles, tests, data artifacts, whitelisted CI fixes, and dependency bumps whose every version delta is patch or minor. Content never auto-merges. The gate merges a green PR immediately against the exact SHA it judged, and disables any standing arm it finds.

Process
  1. 1
    Measure before merging

    The classifier runs against the live fleet and prints its decision per PR, so the policy can be audited before it is trusted.

  2. 2
    Never arm auto-merge

    A PR armed as a minor bump was force-pushed to a major and merged 130 seconds later. No polling cadence catches that window, so arming was removed entirely rather than made faster.

  3. 3
    Judge the head you merge

    Merges pass --match-head-commit with the judged SHA; GitHub refuses server-side if the branch moved. A pending PR waits for the next sweep — that latency is the price of never merging unjudged content.

  4. 4
    Put the policy in three constants

    AUTO_MERGE_BUMP_KINDS, ZERO_MAJOR_MINOR_IS_BREAKING and TRUST_SHA_PINNED_ACTION_BUMPS sit at the top of the file. Changing what merges is a one-line, reviewable decision.

Outcomes

  • 49 tests covering the classifier and the merge decision, with DECISIONS.md recording why each rule exists
  • Version deltas read from the PR body, not the title — a group bump titled '21 updates' names no versions but its body tabulates the majors that must block it
  • A group is as risky as its riskiest member: bump_kind() takes the max
  • requirements*.txt treated as a manifest, not a lockfile — the bug that let pytest 7→9 and cryptography 48→50 through
What I'd do next
  • Publish the classification history so drift in the fleet's PR mix is visible over time.
  • Let a repo carry its own overrides without forking the constants.
Details

Extracted the fleet's merge policy into a standalone public engine with 49 tests and a DECISIONS.md that records the reasoning behind each rule.

Proved a tempting optimisation wrong with measurement: grouping Dependabot PRs to cut noise makes them strictly less mergeable here, because one 0.x minor or SHA-pinned action in a group demotes the whole PR.