The Software GardenThe merge button is part of the system ← Contents
The Software Garden

The merge button is part of the system

Agents review, fix, merge and release. Every loop in this series ends at the same place: a merge.
What makes that merge safe isn’t the agent pressing the button. It’s what the platform refuses to merge.

Scroll ↓
Zero eyes needs a real gate

Auto-merge is only as safe as what the branch refuses.

In zero-eyes approval, agents review each other until the review is clean, then the PR merges. That assumes the merge itself checks something.

Auditing our seven SDK repos, we found that it didn’t always. main required CI. Some stable/* branches required nothing, so our merge agent could merge a converged PR into a stable branch while CI was red.

The agent did nothing wrong. It merged what the branch allowed it to merge.

🚧 main: required checks in the ruleset
CI must pass
main
🚪 stable/*: no required checks
CI runs… nobody waits
stable/*
 

Same agent, same PRs, same CI. Red squares are PRs with failing CI.

The enforcement ladder, one rung higher

“CI runs it” isn’t the same as “the merge waits for it”.

Rules that enforce themselves stops at “blocked in CI”. But a failing job only blocks anything if the branch’s ruleset requires it. Otherwise it’s a red mark next to a merged PR.

And a required check can still be hollow. One of our SDKs required a generation-drift job that had continue-on-error: true. It was always green, because it couldn’t fail.

A gate that fails at random is hollow too. People learn to re-run it, then to ignore it. That’s why there are no flaky tests: our PHP SDK’s “random” red turned out to be an uncached, unretried download.

★
Required by the rulesetThe platform refuses the merge. No agent, script or tired human can get past it without a bypass someone granted on purpose.
4
Runs in CITurns red. Whether anything waits for it depends on the rung above.
✖
Runs, but can’t failcontinue-on-error, || true, a step skipped by a condition. Looks like a gate. Isn’t.
A hollow required check
generation-drift:
  runs-on: ubuntu-latest
  continue-on-error: true  # reports success either way
  steps: …
Settings drift too

Stable branches are where gates rot. Repo settings are where fleets drift.

Teams protect main carefully. Release branches get whatever was set up the day they were cut, and nobody looks again.

Rulesets, auto-merge, the dependency bot, how releases are triggered: none of it lives in the repo, so none of it is derived or checked for drift. Seven SDKs generated from one spec had grown seven different safety setups.

The fix is the same as for code: pick the source of truth (here, the three most mature SDKs), audit every repo against it, and converge.

in placehanded to a humanmissing◆ reference repos
 

Simplified from our audit of the orchestration-cluster-api-* repos. “After” counts changes that are in open PRs.

The loop with no human in it

A vulnerability is published. A patch ships. Nobody touched it.

Renovate opens a fix(deps) PR. Required checks run, including a dependency review that blocks high and critical advisories. If everything is green, the PR auto-merges, and semantic-release turns the fix into a patch release on main and on every supported stable/* branch.

That’s the goal of zero eyes: hours from advisory to published patch. But it’s only safe because every gate in it is real. Take one out and watch what the same loop does.

📢
AdvisoryCVE published
🤖
Renovatefix(deps) PR
🚧
Required checksbuild, tests, dependency review
🔀
Auto-mergeonly when green
🏷️
semantic-releasefix → patch
📦
Publishednpm, PyPI, NuGet…
 

Two of our SDKs only released when someone pushed a tag by hand, and four had never had the dependency bot installed. For those, the loop simply stopped partway. That’s safe, but the fix never ships.

Humans hold the keys

At the admin boundary, hand off a change, not a request.

Rulesets, app installs and org-wide security settings need admin rights. That’s deliberate: an agent that can change the rules of the merge can merge anything.

So the agent does everything up to the permission line: the audit, the exact new ruleset, the PRs it depends on. Then it opens an issue a human can apply in one step and check in one look.

“Please improve branch protection” sits in a backlog. “Run this command, here’s why” gets done.

🔍
AuditCompare every repo’s rulesets and settings with the reference repos.
↓
🔧
Fix what it’s allowed toPRs for workflows and bot config. Settings changes on repos where it has admin.
↓
🔑
Permission lineAn issue with the exact change: which ruleset, which checks, and the command to apply it.
↓
🧑
A human applies itOne command, one review. The rules of the merge stay in human hands.
So

Don’t trust the merge. Make the platform refuse a bad one.

Require the checks that matter, on every branch you release from. Make sure each one can actually fail. Treat repo settings like code: one reference, audited and converged across the fleet. Then let the loop run all the way to a release, with humans holding the keys to the gates.