fix(ci): read the reviewed label live so a queued synchronize cannot leave a false green (BACKLOG #1423) - #783
fix(ci): read the reviewed label live so a queued synchronize cannot leave a false green (BACKLOG #1423)#783wshallwshall wants to merge 4 commits into
Conversation
The label-reading step took its verdict from the webhook payload, which is a snapshot frozen when the event fired. On PR 765 a `labeled` run created at 20:43:24Z started its job at 20:49:34Z and reported SUCCESS from that payload, four minutes after the queued `synchronize` run had removed the label. Nothing corrected the green: the removal runs under GITHUB_TOKEN, which raises no workflow run, so no `unlabeled` run ever fired. It now reads `gh pr view --json labels` at evaluation time and BLOCKS when that read fails. The `synchronize` removal and its unread-by-definition arm are unchanged, as is the job name that branch protection requires. The controls run the shipped shell against a `gh` stand-in that records its own argv, and the pre-fix form is run against the same planted fixture and observed PASSING, so the refusal is measured rather than asserted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (BACKLOG #1423) Records the PR 765 measurement end to end, names the mechanism, and states the limit rather than claiming closure: reading live narrows the window from minutes of queue delay to the seconds between the API call and the job finishing, so the operational rule survives the fix. For a BEHIND pull request, update-branch, wait for the `synchronize` run to COMPLETE, then label, then merge. #1417 filed the same defect on PR 724 and worked out the detection rule; this is a second instance plus the fix, and adds the fact that no `unlabeled` run fires to correct the green. Whether #1417 closes with it is the Lander's call. The erratum records #1422 as a hole: it was allocated from a coordinating session's own worktree and handed to a builder in another, so both ownership keys failed and the ledger gate refused the commit. Re-filed at #1423 unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cross-PR reconciliation: 783 and 784 fix the same defect in the same file, and they conflictPosted by a Builder dispatched to reconcile BACKLOG #1417 against PR 783. On arrival I found PR 784 The conflict is measured, not predicted
Both rewrite the same Rule by rule: what PR 783 closes of #1417#1417 numbers five rules. Rules (1) to (4) are a detection procedure for a reader; rule (5) is the
Rule (2) inverts under 783. Under the payload gate, the originating run's Rule (4): the residual is real and 783 states it in its own header. A Rule (5) is reachable under 783 alone, not merely unaddressed. 783 adds no head sha, no commit What each pull request has that the other lacksNeither is a subset of the other, which is why the land order matters beyond the rebase cost.
The residual nobody has recorded: #1417's row goes stale if 783 lands alone#1417's row on So if 783 lands first, someone has to update #1417's row separately, or #1417 keeps asserting an Neither pull request closes #1417, and that is right. 784 keeps the open banner and says "BUILT Two observations for the reviewers, not fixesI did not edit
Open question for the ConsoleThe claim on #1417 is held by a worktree with no live session in it, and |
Both sides append a new section at the same point in docs/BACKLOG.md, under different item numbers. Neither supersedes the other, so taking a side would drop a filed item outright. Both blocks are kept, in the ascending item order the file already uses. Verified with parse_items from scripts/docs/backlog_status_check.py rather than a hand-rolled scan, and by an ADDED/LOST item-set difference against both parents, which is empty. A count alone would not catch two blocks folding into one.
…22ba84254fb # Conflicts: # docs/BACKLOG.md
Label disclosure, required by the Manager playbookI did NOT apply the I am recording that because I commissioned this diff, and the playbook this seat works to requires the commissioning party to say so when it labels its own workers' work. The honest version here is the inverse: this label is not mine, and I cannot vouch for what read stands behind it. Every session on this machine pushes as one GitHub identity, so the actor field on the label event does not identify which session applied it. That is measured, not assumed. The owner has disabled the reviewer role and its tag requirement, so the label now satisfies a gate rather than asserting a review. Nothing here should be read as my attestation of this diff's scope. |
What this fixes
a reviewer has read thistook its verdict from the webhook payload, which is a snapshot frozen when the event fired. A run can sit queued for minutes, so the gate could report SUCCESS on a state that no longer existed.Measured on PR 765, 2026-09-03, head
ee2e7ec, from the runs, jobs and timeline APIs:33803677823created by asynchronizepushwshallwshallappliesreviewed33803911587created bylabeled-- its payload records the label as PRESENTsynchronizerun's job starts, four minutes after creationgithub-actions[bot]removes the label; that run concludes FAILURE at 20:45:16Zlabeledrun's job starts, six minutes after creationThe GITHUB_TOKEN question, answered
Confirmed, both from the docs and empirically. GitHub does not dispatch a workflow run from an event raised by the repository's own
GITHUB_TOKEN(workflow_dispatchandrepository_dispatchare the documented exceptions, and a label event is neither). The removal runsghundergithub.token, so itsunlabeledevent emits nothing.Empirical check on the same head:
actions/workflows/review-gate.yml/runs?head_sha=ee2e7ec...reports exactly two review-gate runs, the two above, and none for the 20:45:14Z removal. The workflow header's old claim thatunlabeledis "the opposite half" is corrected in place: it covers a person taking the label back, and nothing else.The fix
The step now runs
gh pr view "$NUMBER" --json labels --jq '[.labels[].name] | join(",")'and decides on the result. The four required properties hold:ghabsent, unauthenticated, rate-limited or erroring all BLOCK.synchronizeremoval is untouched.synchronizestill fails by definition, decided before any read.a reviewer has read this.The payload is still passed in as
PAYLOAD_LABELS, diagnostic only. When it and the live set disagree aboutreviewed, the log says so in words; nothing branches on it.What it does NOT do
It narrows the window, it does not close it to zero, and the item says so rather than claiming closure. The payload read cost minutes of queue delay; what remains is the gap between the API call and the job finishing, which is seconds. So the operational rule survives the fix: for a BEHIND pull request, update-branch, WAIT for the resulting
synchronizerun to COMPLETE, then label, then merge. Recorded indocs/CI.mdand the workflow header.Both test arms, and both pass
Added to
tests/test_merge_gate_controls.py. The step's own shell is lifted out of the workflow and run under Actions' flags against aghstand-in placed first on PATH, which records its own argv so a gate that stopped calling the API cannot be graded as though it had.reviewed, live set EMPTYopened/reopened/ready_for_review/labeled/unlabeledghwas calledghexits non-zero, payload saysreviewedreviewedsynchronizewith the label still presentThat last row is what makes the first one evidence rather than a claim: the shipped refusal is measured against a form known to accept. A one-armed suite would be satisfied by a gate that refuses everything, which would wedge every PR in the repository.
Checks run
Run here, all green:
ruff check .,ruff format --check .,mypy messagefoundry(strict, 267 files),pytest tests/test_merge_gate_controls.py tests/test_negative_controls.py tests/test_security_posture.py tests/test_required_contexts.py(68 passed), the seven doc-guard modules (131 passed), andbacklog_status_check.py,backlog_citation_check.py,dangling_citation_check.py,link_check.py.actionlintpassed in pre-commit.Not run here: the full pytest suite, and every hosted-runner-only leg (
windows-service-smoke, the SQL Server / Postgres store legs, the load legs). Those need reading after this process exits.OPEN QUESTIONS FOR THE CONSOLE
1. The item is filed at #1423, not #1422. The brief allocated #1422, but its
allocrecord names the coordinating session's own worktree and branch (compress-claude-md-c78f90/claude/subagent-backlog-manager-7bdfb7), both live. Ownership keys on the worktree with the branch as a fallback that is only reachable once the worktree is GONE, so both keys failed and the pre-commit ledger gate refused the commit -- correctly.alloc.ps1has no transfer verb by design, and--no-verifyis forbidden, so I took the remedy the Ledger erratum itself records for #1297: re-allocate in the committing tree and re-file. #1422 is now a permanent hole, recorded as such in the erratum. The title and body are unchanged. Allocate in the tree that will commit, not on a builder's behalf.2. #1417 is the same root cause and this pull request fixes it. #1417 filed the defect on PR 724, worked out the detection rule, and already named reading live as one of two remedies. This is a second independent instance plus the fix, and it adds the corrective-path finding. Whether #1417 closes alongside #1423 is the Lander's call, not mine -- I have not touched its banner.
3.
.github/required-contexts.txtis untouched. The job name is unchanged, so the live required set should not have moved. Worth reading branch protection to confirm rather than trusting that.I have not applied the
reviewedlabel and have not armed auto-merge.🤖 Generated with Claude Code