merge: an absent tick is not a passing tick - #35
Merged
Merged
Conversation
Step 5 could merge a PR whose CI had not run. Two ways, both the same
shape — an absence of signal read as a positive answer. Hit while landing
ps-mutuus/mutuus-changelog#52.
1. `[]` was documented as "no CI gate exists — proceed". It is also what
GitHub returns for a minute or two after a force-push, while it
re-associates workflow runs with the new head. Observed on #52: rollup
`[]` and `gh pr checks` printing "no checks reported" while a run was
already queued for that exact head sha. `[]` now means re-query, and
"no CI" may only be concluded after `gh run list` positively shows no
run for the head — with the caveat, stated, that it sees Actions only.
The wait loop now requires `total > 0` before it can conclude nothing
is pending, so zero checks can never satisfy "all passed"; verified
against a scripted always-empty rollup, which now times out instead of
passing.
2. `.conclusion // .state` never consulted `.state`. jq's `//` falls
through on null and false; an in-progress check run has
`conclusion: ""`. Verified: `{"conclusion":"","state":null} | .conclusion
// .state` yields `""`, which lands in a bucket no rule mentioned and
reads as "not FAILURE, therefore fine".
The replacement branches on `__typename` rather than sniffing nulls. The
reason the null-sniffing shorthand is tempting and wrong: gh's export
(cli/cli `api/export_pr.go`) emits *disjoint* key sets — CheckRun has no
`state` key at all, StatusContext has no `status`/`conclusion` and names
itself via `context` — so `.state != null` is a type test, not a
completion test. That matters for the fix as first drafted, which used
`.state != null` for "done": a legacy status context that is still running
is `state: "PENDING"`, so a pending external CI would have counted as
passed. Terminal for a StatusContext is SUCCESS/FAILURE/ERROR only;
PENDING and EXPECTED are not. Enum values taken from GraphQL
introspection, not memory: StatusState, CheckStatusState (COMPLETED is
the only terminal one of six) and CheckConclusionState (the failing set
is wider than FAILURE — ACTION_REQUIRED, STARTUP_FAILURE and STALE were
missing).
An unknown `__typename` falls into the else branch with no state and
lands in `pending`. Unknown fails closed.
Verified: the 5a filter extracted verbatim from the doc and run against
cli/cli#14334 (37 rows, all green); against fixtures for in-progress
check runs, a pending legacy context, mixed failures, SKIPPED, empty and
null rollups; the 5b cross-check run in both directions on cli/cli trunk
(0 for a foreign sha, 20 for the real head) so it is known to be able to
answer negative and positive. Not observed: a live StatusContext row —
that half is derived from gh's export and the enum, and is flagged as
such under Limits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Step 5 of the
mergeskill could merge a PR whose CI had not run. Two bugs, one shape: an absence of signal read as a positive answer. Both hit by hand while landing ps-mutuus/mutuus-changelog#52.Bug 1 —
[]documented as "no CI gate exists — proceed"[]is also what GitHub returns for a minute or two after a force-push, while it re-associates workflow runs with the new head. Observed on #52 right after the step-4 push: rollup[],gh pr checks 52printingno checks reported on the 'claude/gh-path-helper' branch, andgh run listshowing aPR checksrun already queued for that exact head sha.Now:
[]means re-query. "No CI" may only be concluded after a positive check that no run exists for the head sha — with the caveat, stated in the skill, thatgh run listsees GitHub Actions only, so an external provider posting commit statuses appears in neither source until it registers. The wait loop requirestotal > 0before it can conclude nothing is pending, so zero checks can never satisfy "all passed".Bug 2 —
.conclusion // .statenever consulted.statejq's
//falls through onnullandfalse; an in-progress check run hasconclusion: "". So.statewas dead code, and a pending check landed in an empty-string bucket no rule mentioned — easy to read as "not FAILURE, therefore fine".The replacement branches on
__typenamerather than sniffing nulls, because the null-sniffing shorthand is a type test by accident:gh's export (cli/cliapi/export_pr.go) emits disjoint key sets — aCheckRunhas nostatekey at all, aStatusContexthas nostatus/conclusionand names itself viacontext.The half you asked me to verify — the legacy status context
The
doneclause as proposed in the report (.status == "COMPLETED" or .state != null) has the same bug one level down. A legacy status context that is still running isstate: "PENDING"— non-null — so a pending external CI would have counted as passed. Reproduced against a fixture; the committed filter treatsPENDING/EXPECTEDas pending.Enum values came from GraphQL introspection rather than memory, which also caught a third gap: the failing conclusions are wider than the proposed list —
ACTION_REQUIRED,STARTUP_FAILUREandSTALEwere missing.StatusStateCheckStatusStateCheckConclusionStateAn unrecognised
__typenamefalls into the else branch with no state and lands inpending. Unknown fails closed.Verified
{"failed":[],"pending":[],"total":37}.conclusion:"",status:IN_PROGRESS) → pending; aPENDINGlegacy status context → pending; mixed QUEUED/FAILURE/SKIPPED/ERROR → correct buckets; empty rollup,nullrollup, unknown typename → fail closed.//on"":{"conclusion":"","state":null} | .conclusion // .state→"". Confirmed on jq 1.8.2.0for a foreign sha,20for the real head — so it is known to be able to return a negative and a positive, the disciplinescripts/gh_path.pyencodes for a different API.Not verified
A live
StatusContextrow was never observed. That half is derived fromgh's export shape and theStatusStateenum, and is flagged under Limits in the skill so the next reader knows to check the raw rows once on a repo whose CI posts commit statuses.gh pr checksexit8remains documented-but-unobserved, unchanged.README updated in the same commit.