Repository navigation
Hold a Held Wait While a Head's Workflow Run Has Not Completed - #3470
Conversation
Right after a push, a CLEAN merge and an all-green rollup read as settled while most of a run's jobs had not registered, since a job behind needs: enters the rollup only once its dependencies finish. The held poll now holds on any workflow-run suite on the head that has not completed, whatever the merge reads, and the CHECKS_PENDING line names those runs. A run WAITING on an approval holds nothing, since no wait clears it, and a note names it instead. A stuck required check under BLOCKED still ends the poll at once. Closes on promotion: #3451 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where workflowRun was unreadable, every suite read as another app's, so the run hold switched off without saying so. An Actions suite in the rollup now stands in, the fallback check_nodes takes too, and each open entry is named as a run or a suite. A run with a job no runner picked up holds nothing, since no wait starts it and the digest already names the job. The WAITING note now names an environment's protection rule, which can be a timer as well as a reviewer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Releasing every open run once any check read NOT_PICKED_UP brought back the early exit 0: a job behind a concurrency group or a busy runner pool passes the five-minute grace and then starts. A starved job's run now holds like any other, and the wait ends at 30 with the digest's line for that job. The head's own suites now carry their app, so with workflowRun unreadable a fresh Actions suite with no job yet still holds. One predicate names an Actions suite left unkeyed, for check_nodes and runs_open alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
|
A recorded local strict-review pass covers head |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved findings could allow active workflow work to be released or missed, including active jobs in waiting suites and suites beyond the query limit.
4 open issues
What changed in this PR
This pull request updates held pull-request waits to remain pending while workflow runs on the head commit are incomplete, including runs whose jobs have not yet entered the check rollup.
Changes:
- Detects open workflow runs from head and rollup check suites.
- Adds degraded-mode fallback for unreadable workflow-run data.
- Updates wait status reporting, documentation, and regression coverage.
| File | Description |
|---|---|
tests/test_pr_review.py |
Adds regression coverage for incomplete, completed, waiting, degraded, and duplicate workflow runs. |
scripts/README.md |
Documents the updated wait behavior, status output, and exit conditions. |
scripts/pr_review.py |
Extends suite queries and reports open workflow runs during waits. |
scripts/pr_review_checks.py |
Detects open workflow runs and applies degraded-mode suite handling. |
Review effort: Lite
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #3470 +/- ##
==========================================
Coverage ? 80.98%
==========================================
Files ? 25
Lines ? 8895
Branches ? 0
==========================================
Hits ? 7204
Misses ? 1691
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
A check run WAITING past the grace read as NOT_PICKED_UP, so the digest told a reader to re-run a job a protection rule holds, beside the note saying its run waits on its environment. It now reads as GATED, with its own remedy line, and a gated required check under BLOCKED names its approval in the 44 line. A WAITING suite counts as gated only while none of its registered jobs is queued or running, so a gated deploy job does not release the run's other jobs. Another app's queued suite holds nothing where workflowRun is unreadable, now under a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every rule that let a run WAITING on its environment release the wait opened another way to exit 0 with jobs still to run: a job pending behind a concurrency group, or a dependent not yet registered. A gated run now holds like any other open run, and the wait ends at 30 with the digest's GATED line naming what holds it. A gated required check under BLOCKED holds rather than ending the poll at 44, since a timer clears by itself and an approval may land. WAITING leaves NOT_STARTED, which check_shape no longer reads it from, and the README lists GATED among the stuck shapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's held-wait paragraph now lists each release and hold in the order held_checks_open reads them, and the exit-44 paragraph says which path it describes, since a held head polls a long or gated check to 30 rather than ending at 44. The wait help names a gated check among the 44 shapes. Two tests are renamed to what they assert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wait help listed a required check still settling and a workflow run still open as though they were every hold on a held head. It now points to the holds status=CHECKS_PENDING names, with those two among them, and the exit-code block is rewrapped to its width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pull request merged or closed during the wait releases the held poll before any run is read, so the help's promise that every run on the head has completed now holds only while the pull request is open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"While it is open" left the pronoun to the held head or the pass, and scoped only the runs clause. The exit-0 entry now names an open pull request's held head once, covering its settled checks and its completed runs alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
Placing the pass on an open pull request's head left a held pull request merged or closed during the wait with no named cause for its exit 0. The pass is back on any held head, and only the completed-runs clause is scoped to an open pull request, which is where the run hold applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"On an open pull request" read as covering every exit 0, the Copilot path's included, where runs may still be going. The clause now names an open pull request's held head, the only place the run hold applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A held head a Copilot round covers, with no attestation, takes the review branch, where the run hold does not apply. The clause now names the attested held head, the one the hold reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
The run hold applies where an attested local pass covers the head, which also needs no Copilot refusal on it, so an attested head carrying a refusal could exit 0 with a run open. The clause now names coverage, the condition the code reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
## Summary Answers the finding the develop -> main promotion [#3472](#3472) raised on the `wait` help that [#3470](#3470) wrote. The help and `scripts/README.md` said a held head exits `0` with its required checks settled and every workflow run completed. Two paths in `held_checks_open` release without either: - A payload with no commit matching the head, `CHECKS UNREADABLE`, reads no run and no check. - A required check not yet registered holds only a `BLOCKED` merge whose base branch rules were read and whose rollup is not `CHECKS TRUNCATED`. An empty rollup holds only `BLOCKED`, `BEHIND`, or `DRAFT`. Short of those holds, a merge with some check posted, or a `DIRTY` one with none, releases with a required check unregistered. The help and the README's exit table and held-wait paragraph now say this. The exit code itself is unchanged and stays for [#3471](#3471) to decide, whose done-when allows either a new exit or help naming each exception. This change does the second without closing that decision. ## Verification - Full unit suite, the `OPERATIONS.md` "Local Verification" command: 2940 tests, OK, 7 skipped, on the first commit. The `test_pr_review*.py` set, 525 tests, OK, on the second. - `prose_lint.py --diff` against the merge base, with and without `--check sentence-length`: clean. - Three local adversarial passes. The first two found three gaps, the omitted `DRAFT` case, the unnamed rules-read and truncation conditions, and an incomplete list of releasing merges, each fixed here. The third found nothing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…nd Nine Fixes to Main (#3472) ## Summary Promotes the pull requests merged into develop since release 2.0.1166. - [#3444](#3444): keeps untracked files out of the pre-commit snippet's prose gate. - [#3446](#3446): exempts the BuildKit `check=` parser directive in the prose gate. - [#3448](#3448): compares intent staleness against the hub's own copy and names the moved canonical. - [#3450](#3450): says only the prose gate is diff-scoped in the commit hook. - [#3453](#3453): reclassifies HolidayLights as an operational config repository in the registry. - [#3455](#3455): classifies the remaining subprocess failures by status rather than by prose. - [#3464](#3464): splits the semicolon series in WORKFLOW.md's filesystem output-seam bullet. - [#3466](#3466): audits a caller's `workflow-model` input against the registry. - [#3469](#3469): emits one snapshot shape from the skills report's cannot-read paths. - [#3462](#3462): splits `pr_review.py`'s pure check and round readers into the leaf modules `pr_review_checks.py` and `pr_review_rounds.py`. - [#3470](#3470): holds a covered held `wait` while any workflow run on the head has not completed. - [#3473](#3473): answers this promotion's review. The `wait` help and `scripts/README.md` now say what a held head's exit `0` does not prove: a `CHECKS UNREADABLE` payload, the conditions the unregistered-required-check hold reads, and the reach of the completed-run read. The exit code itself stays for #3471. Closes #3370 Closes #3372 Closes #3371 Closes #3369 Closes #3378 Closes #3454 Closes #3332 Closes #1759 Closes #3451 ## Constituent coverage Each change this promotion carries had coverage on its own pull request into develop, at its merged head: | Pull request | Merged head | Coverage | | --- | --- | --- | | [#3444](#3444) | `a61d1e2a` | attested local pass at the merged head, base `develop` | | [#3446](#3446) | `0a0efea1` | attested local pass at the merged head, base `develop` | | [#3448](#3448) | `c2c71ea7` | attested local pass at the merged head, base `develop` | | [#3450](#3450) | `4ec37c34` | attested local pass at the merged head, base `develop` | | [#3453](#3453) | `f20a543c` | a Copilot round on the merged head, its one changed file, no open findings | | [#3455](#3455) | `85f0ce60` | attested local pass at the merged head, base `develop` | | [#3464](#3464) | `fcd48e2d` | attested local pass at the merged head, base `develop` | | [#3466](#3466) | `3426a19c` | attested local pass at the merged head, base `develop` | | [#3469](#3469) | `ea4b0bb7` | attested local pass at the merged head, base `develop` | | [#3462](#3462) | `236eceab` | attested local pass at the merged head, base `develop` | | [#3470](#3470) | `3545ba46` | attested local pass at the merged head, base `develop` | | [#3473](#3473) | `46010530` | attested local pass at the merged head, base `develop`, and a Copilot table on an earlier head naming exactly the changed files | 🤖 Generated with [Claude Code](https://claude.com/claude-code)



Summary
A pull request into a non-default branch is "held": an attested local pass covers its head, and
pr_review.py waitpolls the head's checks instead of a Copilot round. Run right after a push, that poll exited0while CI was still running. A job behindneeds:enters the rollup only once its dependencies finish, so the merge readCLEANand every registered check was green while most of a run's jobs had not been created yet. A caller trusting the exit judged the pull request ready.The held poll now holds while any workflow run on the head has not completed:
runs_open(pr)inscripts/pr_review_checks.pynames every workflow run whose suite is notCOMPLETED. It reads the head commit's own suites and each rollup check run's suite, so a fresh head's run counts before most of its jobs exist. A suite with no workflow run, such as another app's that never reports, holds nothing.held_checks_openreads its conditions in this order:UNKNOWNholdsBLOCKEDmerge with a required check stuck in a shape waiting does not clear ends the poll at44CLEAN,UNSTABLE, orHAS_HOOKSmerge releaseWAITINGpast--check-graceis its ownGATEDshape. It gets aCHECK WAITING ON ITS ENVIRONMENTline saying to approve the job or let its timer pass, since a re-run meets the same rule.WAITINGleavesNOT_STARTED.workflowRunis unreadable, an Actions suite stands in, namedsuite <id>, throughunkeyed_actions_suite, whichcheck_nodesshares. The head's suites query now selectsapp{ slug }for this.status=CHECKS_PENDINGline names the open runs. Thewaithelp and thescripts/README.mdheld-wait and exit-44 paragraphs describe the new reading, in the code's order.Closes on promotion: #3451
Decisions
0: a job pending behind a concurrency group, a busy runner pool, or a dependent not yet registered. The cost is that a run waiting on an approval that never comes, or on a job no runner takes, polls to the timeout. It then exits30, with the digest'sGATEDorCHECK NOT PICKED UPline naming what holds it.BLOCKEDpoll at44. A timer clears by itself and an approval may land, so on a held head these hold to30. On the Copilot path a required gated check still exits44, as a long-running one already did.BLOCKEDstill exits44at once, as before.check_nodes. Its suite walk is threaded through per-node normalization, soruns_openreads the suites itself. The one condition the two share, whether a suite is an Actions suite left unkeyed, is one predicate.workflowRununreadable, an unfinished Actions suite holds even when it has no job yet, since right after a push it looks the same as one that never gets a job. I measured the check suites on the 8 most recent pull request heads here: every Actions suite carried jobs, and none sat unfinished. The suites left queued with no jobs belonged to other apps (azure-pipelines,claude,coderabbitai), which the predicate excludes and a test pins.Remaining Local-Review Findings
Each push had recorded frontier-tier passes, and every
introducedfinding they raised is fixed in the commits above, apart from these:introduced, fixed in8acf0511: thewaithelp's exit-30 line read as a complete list of holds. It now points to the holdsstatus=CHECKS_PENDINGnames.introduced, deferred to A Held Wait Exits 0 Under a DIRTY or BEHIND Merge Whose Required Checks Never Ran #3471: the exit-0 help's completed-runs clause now names coverage, the condition the code reads (3545ba46). One path still falls outside it: when the rollup's commit is not the head,waitdeliberately exits0withCHECKS UNREADABLE, and no run is read. That predates this change and a test pins it, but the help's new clause now overclaims there. It joins A Held Wait Exits 0 Under a DIRTY or BEHIND Merge Whose Required Checks Never Ran #3471, which tracks exit0where the required checks were never read.pre-existing: the help's claim that exit0means the head's required checks settled is false under aDIRTYorBEHINDmerge whose required checks never ran. GitHub runs nopull_requestworkflow on a conflicted pull request, andheld_checks_openholds an unregistered required context only underBLOCKED. These paths predate this change, so they are left to an issue of their own.introduced, conditional: the degraded-mode decision above. If GitHub ever leaves a job-less Actions suite queued on a head, the held wait would poll to30in that mode alone. The measurement above found none.style: one help string and two help-text lines are wrapped short. Declined, since no gate reads them and the file already carries longer lines.pre-existing: a head read before any check suite exists, on a base requiring no check, can still exit0underCLEAN. Nothing on the head exists yet for any reader to see. Every develop ruleset in this fleet requires the aggregator, so there the merge readsBLOCKEDandrequired_unmetholds.pre-existing: sentences over 25 words that this change only rewrapped or left in place: three atscripts/README.mdline 233,scripts/pr_review.pylines 120 and 123, and the49 =help sentence.Verification
python -m unittest discover -s tests(underuvx --with-requirements tests/requirements.txt): 2,940 tests OK, 7 skipped, ate4295145. The commits after it change only thewaithelp text, and thetest_pr_reviewandtest_cli_usagesuites pass on each.CLEAN44COMPLETEDskipGATEDshapef6837bd9) held for 781 seconds until every run on the head completed, then exited0. The same wait without it had returned while two checks were still running.statusagainst this repository.ruff check,ruff format --check,mypy ., andprose_lint.py --diff: clean.🤖 Generated with Claude Code