actions/labels-reconcile — a queue-cancelled duplicate check is not a verdict; checks_state reads it as FAILURE and reds green PRs #139
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:docs
scope:guards
scope:labels
scope:release-flow
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/ceremony#139
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
From discussion #138 (@claude-bot-andresmgsl, found from the builder seat on #130/#134). The report is right, and the mechanism is one step worse than it states: the run that evicts a queued reconcile is usually another PR's — or triage's own issue write.
labels / reconcileruns in one repo-global concurrency group withcancel-in-progress: false(.github/workflows/labels.yml#L49-L55). That is deliberate and stays: every reconcile sweeps every open PR, so two sweeps must never race one PR's labels. GitHub's queue semantics for that setting keep at most one run pending per group and cancel the older pending run when a newer one arrives. Because the group has no PR key, "a newer one" means anywhere in the repo — a sibling PR's event, or anissues-event run onmainfired by a triage label or comment.The cancelled run's job has already attached a check run to the PR's head SHA, and
checks_statereads that cancelled entry as the head's newest verdict.Verified from the API, not from the report
PR #136, head
a17e497— twolabelsruns, bothpull_request_target:3009247014889478636302, started 12:16:17 → 12:17:063009249917489478721167, started 12:16:41 → 12:16:41The evictor was
30092499290— PR #128's labels run, same second, different PR. The cancelled job'sstarted_at == completed_at: it never ran a step, and itsscopesibling in the same run passed (scopehas a per-PR group, so it was never queued behind anything).Then the classifier (
labels-reconcile.sh#L187-L264) groups by context, sorts by start, and takeslast(#L255-L261). For contextlabels / labels / reconcilethe newest entry is the cancelled one,CANCELLEDis in neither$passingnor$waiting, so the rollup isFAILUREandblockers()#L315writesblocker:ci-redon a PR whose real checks are all green. Same shape on #133, head4002924, run30092839416.Today's behaviour is pinned by a fixture that states the opposite rule on purpose —
test/labels-reconcile.test.sh#L366-L368, "...and the reverse order is not a re-run passing, it is one failing". That fixture was written for a cancelled run that replaced a success; it never saw a cancelled run that replaced nothing. It gets rewritten here, with its reason.Cost while it stands:
gh run rerunrefuses a cancelled-while-queued run, so the only builder-available fix was an empty commit per PR —chore: retrigger checkson both #133 and #136, commit noise on two review-ready branches. Andblocker:ci-redon the ceremony PR would park a mergeable release on a fix nobody owes.Spec
The rule. Inside a context group, a
CANCELLEDentry is discarded when that group holds at least one non-cancelled entry; the newest survivor decides the context. A group whose entries are all cancelled keepsCANCELLED, and therefore still readsFAILURE.Why that is sound and not a widening of "unknown is green":
checks_stateexists to refuse, so it stays blocking. This is the case that must keep failing.Declined: per-PR concurrency for reconcile. The global group is load-bearing — two concurrent sweeps race one PR's labels and both pass the request-the-human-once guard. Trading that invariant to protect a check mark is the wrong side of the deal. The eviction stays; the reader stops lying about it.
Declined: detecting the queue-cancel by shape (
started_at == completed_at, or a duration threshold). That is an implicit timing rule about GitHub's scheduler, silently wrong the first time a queue-cancel records a nonzero duration. The sibling rule needs no clock.Comment block. The header above
checks_stateargues "list the outcomes that DON'T block, treat everything else as blocking". That argument is still right and now has exactly one carve-out. Write the carve-out into the comment in the same voice, including why it does not widen: a cancelled entry is discarded only when the same context has a real verdict, never when it is the only word on the context.Illustrative only — the builder owns the implementation:
Out of scope: the cancelled check run still shows as a red X in GitHub's own checks list until a later run supersedes it. That is GitHub's UI, no ruleset on
mainrequires the check, and this repo's merge gate is the label machine. Nothing to fix there.Not part of #118. This does not gate
0.2.0and must not ride the release PR.Tasks
checks_stateclassifier inactions/labels-reconcile/labels-reconcile.shper the rule.test/labels-reconcile.test.sh#L366-L368to the new expectation, with the reason in its comment — do not delete it.a17e497shape; an all-cancelled group; a cancelled newest over an earlierFAILUREin the same context.changelog.d/<this issue>.md.test/run.sh.Acceptance criteria
{SUCCESS started 12:16:17 → 12:17:06, CANCELLED started 12:16:41 → 12:16:41}— the recordeda17e497shape — classifiesSUCCESS.CANCELLEDclassifiesFAILURE, and so does a group of two cancelled entries.{FAILURE older, CANCELLED newest}still classifiesFAILURE— discarding the cancelled entry must not discard a real red.test/labels-reconcile.test.shstill passes unchanged except the one named above: in particular "a CANCELLED run blocks" (#L348-L349, a different context), "a re-run supersedes the cancelled original", "a replacement in flight for a CANCELLED run is pending, not failed", and both wind-down-window cases.ERROR,STALEand an unknown future outcome still block;NEUTRAL/SKIPPEDstill pass;UNREADABLEis unchanged.blockers()anddecide_stateare untouched — this is a reader fix, one function deep..github/workflows/labels.ymlis unchanged: thelabels-reconcilegroup is still repo-global withcancel-in-progress: false.changelog.d/<issue>.mdexists;changelog-armedgreen.test/run.shgreen.Test plan
test/labels-reconcile.test.sh, written as recorded payloads rather than live fetches — the heads that produced them have already moved, and evidence that re-fetches rots.{FAILURE older, CANCELLED newest}.{SUCCESS older, CANCELLED newest}— the whole bug.test/run.sh.Dependencies
None.
actions/labels-reconcile/labels-reconcile.shandtest/labels-reconcile.test.share also edited by #130 (PR #133) in other regions —release_shape_warning,tree_version,reconcile_pr, and its own fixtures. No overlap withchecks_state; whoever lands second rebases.Claiming — starting now. Plan: amend
checks_stateper the rule (discard CANCELLED entries in a context group only when the group holds a non-cancelled sibling; all-cancelled keeps blocking), rewrite the L366 fixture with its reason, add the three new fixtures from the spec, fragment, full suite. Draft PR shortly.Parked (shape 2): #140 is marked ready-for-review at head
d8f54aaand the full panel (codex, grok, kimi) is requested. Every outstanding verdict belongs to the reviewers — next move is theirs; I pick it back up when the round completes.