Skip to content

Hold a Held Wait While a Head's Workflow Run Has Not Completed - #3470

Merged
ptr727 merged 13 commits into
developfrom
feature/3451-held-wait-runs
Oct 25, 2026
Merged

ptr727 merged 13 commits into
developfrom
feature/3451-held-wait-runs

Conversation

@ptr727

@ptr727 ptr727 commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

A pull request into a non-default branch is "held": an attested local pass covers its head, and pr_review.py wait polls the head's checks instead of a Copilot round. Run right after a push, that poll exited 0 while CI was still running. A job behind needs: enters the rollup only once its dependencies finish, so the merge read CLEAN and 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) in scripts/pr_review_checks.py names every workflow run whose suite is not COMPLETED. 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_open reads its conditions in this order:
    • a pull request no longer open releases
    • a merge reading UNKNOWN holds
    • a BLOCKED merge with a required check stuck in a shape waiting does not clear ends the poll at 44
    • an open run holds
    • only then does a CLEAN, UNSTABLE, or HAS_HOOKS merge release
  • A check run WAITING past --check-grace is its own GATED shape. It gets a CHECK WAITING ON ITS ENVIRONMENT line saying to approve the job or let its timer pass, since a re-run meets the same rule. WAITING leaves NOT_STARTED.
  • Where workflowRun is unreadable, an Actions suite stands in, named suite <id>, through unkeyed_actions_suite, which check_nodes shares. The head's suites query now selects app{ slug } for this.
  • The status=CHECKS_PENDING line names the open runs. The wait help and the scripts/README.md held-wait and exit-44 paragraphs describe the new reading, in the code's order.

Closes on promotion: #3451

Decisions

  • Every open run holds, with no exception. Earlier revisions released a run waiting on an environment, then one with a starved job. Review showed each release reopened the early exit 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 exits 30, with the digest's GATED or CHECK NOT PICKED UP line naming what holds it.
  • A gated or long-running required check does not end a BLOCKED poll at 44. A timer clears by itself and an approval may land, so on a held head these hold to 30. On the Copilot path a required gated check still exits 44, as a long-running one already did.
  • A required starved check under BLOCKED still exits 44 at once, as before.
  • No shared walker with check_nodes. Its suite walk is threaded through per-node normalization, so runs_open reads the suites itself. The one condition the two share, whether a suite is an Actions suite left unkeyed, is one predicate.
  • A job-less Actions suite may hold in degraded mode. With workflowRun unreadable, 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 introduced finding they raised is fixed in the commits above, apart from these:

  • introduced, fixed in 8acf0511: the wait help's exit-30 line read as a complete list of holds. It now points to the holds status=CHECKS_PENDING names.
  • 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, wait deliberately exits 0 with CHECKS 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 exit 0 where the required checks were never read.
  • pre-existing: the help's claim that exit 0 means the head's required checks settled is false under a DIRTY or BEHIND merge whose required checks never ran. GitHub runs no pull_request workflow on a conflicted pull request, and held_checks_open holds an unregistered required context only under BLOCKED. 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 to 30 in 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 exit 0 under CLEAN. Nothing on the head exists yet for any reader to see. Every develop ruleset in this fleet requires the aggregator, so there the merge reads BLOCKED and required_unmet holds.
  • pre-existing: sentences over 25 words that this change only rewrapped or left in place: three at scripts/README.md line 233, scripts/pr_review.py lines 120 and 123, and the 49 = help sentence.

Verification

  • python -m unittest discover -s tests (under uvx --with-requirements tests/requirements.txt): 2,940 tests OK, 7 skipped, at e4295145. The commits after it change only the wait help text, and the test_pr_review and test_cli_usage suites pass on each.
  • Each rule was mutation-tested, and dropping it fails a named test:
    • the run hold, and its place ahead of CLEAN
    • the closed-PR release
    • the stuck-required release ahead of the hold
    • the gated exemption from 44
    • the COMPLETED skip
    • the no-workflow-run exclusion
    • the rollup-suite read
    • the Actions fallback for a rollup suite, for a job-less head suite, and its other-app exclusion
    • the GATED shape
  • On a live pull request, Split pr_review.py's Check and Round Readers Into Leaf Modules #3462, a wait using an earlier revision of this change (f6837bd9) held for 781 seconds until every run on the head completed, then exited 0. The same wait without it had returned while two checks were still running.
  • The extended suites query was read live with status against this repository.
  • ruff check, ruff format --check, mypy ., and prose_lint.py --diff: clean.

🤖 Generated with Claude Code

ptr727 and others added 3 commits October 25, 2026 06:58
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>
@ptr727 ptr727 added the script A defect in hub tooling label Oct 25, 2026
Copilot AI lite review requested due to automatic review settings October 25, 2026 14:29
@ptr727 ptr727 added the script A defect in hub tooling label Oct 25, 2026
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ptr727

ptr727 commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head c8a94811f5fa8bc75d3066dd1ea53f05bb98f96f, the content this pull request carries at that commit against develop, and it recorded 7 findings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread scripts/pr_review_checks.py Resolved
Comment thread scripts/pr_review.py Resolved
Comment thread scripts/pr_review_checks.py Resolved
Comment thread scripts/pr_review_checks.py Resolved
@codecov

codecov Bot commented Oct 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (develop@c75a730). Learn more about missing BASE report.

Additional details and impacted files
@@            Coverage Diff             @@
##             develop    #3470   +/-   ##
==========================================
  Coverage           ?   80.98%           
==========================================
  Files              ?       25           
  Lines              ?     8895           
  Branches           ?        0           
==========================================
  Hits               ?     7204           
  Misses             ?     1691           
  Partials           ?        0           
Flag Coverage Δ
python-3.13 80.98% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ptr727 and others added 6 commits October 25, 2026 07:35
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>
@ptr727

ptr727 commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 702f7035ad6192e937f92412106af6d680887322, the content this pull request carries at that commit against develop, and it recorded 8 findings.

ptr727 and others added 3 commits October 25, 2026 08:31
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>
@ptr727

ptr727 commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head d1dbc640b1805a851b9e73ab9171d2a17b12abaf, the content this pull request carries at that commit against develop, and it recorded 7 findings.

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>
@ptr727

ptr727 commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 3545ba4654c451b213e623ad970763c2fbd22d2c, the content this pull request carries at that commit against develop, and it recorded 7 findings.

ptr727 added a commit that referenced this pull request Oct 25, 2026
## 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>
ptr727 added a commit that referenced this pull request Oct 25, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

script A defect in hub tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants