fix(ci): make the main-red alarm agree with the merge gate - #282
Merged
Conversation
The alarm and the auto-merge green-base guard disagreed about what "main is
green" means, and the alarm was the wrong one.
The guard reads the RUN's conclusion, which every job contributes to. The
alarm recomputed its own verdict from a hand-written needs: list that left out
`migrations` and `test`, and treated `cancelled` as a benign non-event.
On 2026-08-07 an Actions incident cancelled Migration Drift on main. The run
concluded `failure`, auto-merge refused every merge for ~14h, and the alarm —
blind to that job, and forgiving of cancellation anyway — concluded `success`
and filed nothing. The queue was stopped and the thing built to announce that
stayed silent.
Two changes, both narrowing the alarm to the question that actually matters
("is main blocking the queue?"): depend on every job, and count anything that
is not success-or-skipped as red. A cancelled job blocks PRs exactly as hard
as a failing one.
The needs: list is hand-maintained and has now drifted once, so it gets a test
rather than a promise: it asserts the list covers every job in the file, that
each one is wired into the verdict env, and that `cancelled` is not carved back
out. Mutation-checked against the pre-fix list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 7, 2026
#283) Three consecutive sweeps read #278 and #282 as MERGEABLE/CLEAN, announced the merge, and were refused: X Pull request #278 is not mergeable: the base branch policy prohibits the merge. There is no such policy. main has branch protection with every option disabled, no rulesets, no CODEOWNERS, no required checks and no required reviews. Throughout the same window the bot merged #225, #226, #279 and #280 without trouble — including #225/#226, which modify .github/workflows/, so this is not the token's workflows scope either. The refusal is gh's client-side precheck reading the mergeStateStatus that GITHUB_TOKEN sees, which is not the one a PAT sees — nothing this script can inspect its way around. So log both what we saw and delegate: native auto-merge is GitHub performing the merge itself, and does not go through that precheck. It is the escape hatch gh names in its own error message. Still one car per sweep — the fallback is followed by the same `break`, so at most one PR per sweep is handed over, and the green-base guard is unchanged. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 7, 2026
Diagnosis, now with the number that was missing before: the sweep logs its own
view at the merge site, and it reads
#278 direct merge refused (MERGEABLE/BLOCKED)
#282 direct merge refused (MERGEABLE/BLOCKED)
while a PAT reads MERGEABLE/CLEAN for the same two PRs at the same moment.
mergeStateStatus is computed per viewer, and `gh pr merge` refuses client-side
on BLOCKED. `--auto` goes through the identical precheck, so it was refused
too — that fallback never fired.
main has branch protection with every option disabled, no rulesets, no
CODEOWNERS, no required checks and no required reviews, so there is no rule
being enforced here. Meanwhile #224, #225 and #226 — all of which modify
.github/workflows/ — merged normally, so this is not the workflows scope.
The REST merge endpoint skips the precheck and enforces the branch's actual
protection instead. If GitHub does refuse, it returns non-2xx and the PR is
left for the next sweep exactly as before; nothing is forced past a real rule.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 7, 2026
…nges (#287) Merging a PR that edits .github/workflows/ writes those files to the base branch, and GitHub gates that on the `workflows` permission. The sweep never had it, so such a PR is reported to GITHUB_TOKEN as MERGEABLE/BLOCKED — while a PAT reads the identical PR as MERGEABLE/CLEAN, because mergeStateStatus is computed per viewer. Every merge path is refused identically: `gh pr merge`, `gh pr merge --auto`, and PUT /pulls/{n}/merge. That is what stranded #278 and #282 through eight sweeps while PRs touching nothing under .github/ merged normally throughout. Note this PR cannot merge itself — it is the very change it enables, so it needs one merge by hand. Everything after it is self-merging again. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # .github/workflows/ci.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The incident this comes from
main was red and silent for ~14 hours on 2026-08-07, holding 11 PRs.
An Actions incident cancelled
Migration Drift Checkon main. The runconcluded
failure, so auto-merge's green-base guard refused every merge —correctly. The main-red alarm, which exists to announce exactly this,
concluded
successand filed nothing.Why the alarm was wrong
The guard and the alarm answer the same question from two different sources:
needs:list + its own mappingThat list omitted
migrationsandtest, and the mapping treatedcancelledas a benign non-event. So the one job that actually blocked the queue was
invisible to the alarm twice over.
Replaying the real conclusions from run
31119402753:Changes
post-mainnow depends on every job in the file, and each result is fedinto the verdict.
successorskippedcounts as red, includingcancelled— a cancelled job on main blocks PRs exactly as hard as afailing one. (
skippedstays green; jobs here are conditional on event type.)Why a test and not just a longer list
The list is hand-maintained and has already drifted once.
main-red-verdict.test.tsasserts it covers every job in
ci.yml, that each is wired into theR_*envblock, and that
cancelledis not carved back out. It is mutation-checked:restoring the pre-fix list turns it red.
securityjob: this test will (by design) failuntil that job is added to
needs:too.🤖 Generated with Claude Code