Skip to content

v0.5.0 — A refusal that can never resolve now reaches a human

Choose a tag to compare

@raphaelcm raphaelcm released this 30 Jul 21:24
· 11 commits to main since this release
a7f486b

The gap this closes

The gate wakes on every CI completion and every trusted-bot comment, checks its conditions, and on a refusal writes one line to a log nobody reads. That's right for most refusals, which mean "not yet" — CI still running, review not posted — because the gate gets woken again.

Some refusals mean "never", and the gate couldn't tell the difference. It would refuse the same PR forever and tell nobody. A security update that can't land looks exactly like one nobody needed.

I audited every refusal the gate can emit against every notifier that exists. Two are terminal and reach no one:

  • An unparseable review verdict. The review job's assertion greps for the opening marker only, so a comment carrying the marker with truncated or malformed JSON inside passes it green — review SUCCESS, no label, no assignee — while the gate refuses that same comment forever. A truncated agent comment does exactly this.
  • A group PR touching a non-whitelisted path. The assertion skips group PRs by design, and autofix needs a CI failure while CI here is green. Three notifiers, and the PR falls between all of them.

Both now assign, label, and comment — once per PR, not once per wake-up.

Deliberately still silent

Escalation is an allow-list, so anything added later stays quiet by default. Being paged about a PR that fixes itself is what teaches you to ignore the label, and then you miss the real one.

refusal why it stays quiet
review verdict not posted yet the review job already labels, assigns, and goes red
reviewer said escalate / usage affected the reviewer labels the PR itself
CI still running transient by definition
CI cancelled, timed out, awaiting approval a new push re-runs CI and re-wakes the gate, so these usually resolve themselves. Known gap, on purpose
not a Dependabot PR, PR closed, empty diff anomalies — the gate is looking at something it shouldn't act on

One owner for a red build

When CI goes red, the autofix job and the gate wake from the same event and run in parallel. If both escalated you'd be paged about a build the fixer is already fixing. The installer now renders only the taken branch: autofix on → autofix owns it; --no-autofix → the gate does.

And autofix's side is deterministic now. Its no-edits path used to print "The fixer made no edits" and exit quietly, leaving the notification to the agent choosing to label the PR — which its prompt asks for but nothing enforces. An agent that errored, or simply didn't, left a red PR with nobody told. "Covered sometimes" is worse than not covered, because you learn to trust it.

A total outage this release nearly shipped

Refactoring the comment-posting helper added an opening quote and not the closing one. Everything after it parsed inside an unterminated string, which kills the whole auto-merge step — meaning no PR would ever merge again.

The full suite was green. CI shellchecked install.sh and the tests exercised gate.cjs, but nothing had ever parsed the shell those two conspire to generate.

test/workflow-shell.test.mjs closes that: every run: block in the rendered workflow is extracted and parsed with bash -n — the shell GitHub actually runs them with. It names the offending step and line. Mutation-checked against the exact bug that motivated it.

(If you write your own workflow shell, the lesson generalizes: check it with bash -n, not sh -n. On macOS /bin/sh is bash and they look identical; on Ubuntu it's dash, and every <<< fails.)

Upgrade

Re-run the installer. gate.cjs gains a code= output line; if you've hand-edited your installed gate or workflow, re-render rather than merging by hand.

Full changelog: v0.4.0...v0.5.0