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.

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.
- 1Measure 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.
- 2Never 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.
- 3Judge 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.
- 4Put 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
- 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.
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.
Related projects
- Read →
Cocoon
A privacy-first Chrome extension that lowers sensory load online: it softens algorithmic feeds, reduces motion, and keeps a grounding exercise one click away. No accounts, no analytics, nothing leaves the device.
- Read →
unlinkd
Get yourself removed from the internet — and keep the proof. A local-first, fully encrypted workspace for data-broker removal requests and the tamper-evident paper trail a GDPR or CCPA escalation actually needs.
- Read →
FraudStream
A production-grade streaming pipeline that scores financial transactions for fraud in milliseconds rather than hours, built on Kafka, Spark Structured Streaming, and a Snowflake medallion warehouse.