Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ uv run --no-project --script scripts/pr_review.py cleanup 452 --repo ptr727/Proj

`1` is a GraphQL read that failed and ended the run, and `2` is argparse's own usage error.

`wait` exits `30` with `status=CHECKS_PENDING` on an attested held head when a required check is still settling at the timeout, one running long included. It does the same while no required one has posted, while a check the base branch's rules require has not registered, or while the merge reads `UNKNOWN`. A merge reading `CLEAN`, `UNSTABLE`, or `HAS_HOOKS` ends that poll. A failed check ends it on the first read, unless a newer run of the same workflow on the head has not finished, which the digest names `CHECK SUPERSEDED`. `--min-rounds N` counts what `status` prints as `rounds=`, every round on every head, refusals included, so N is the `rounds=` a `status` read before a re-request.
`wait` exits `30` with `status=CHECKS_PENDING` on an attested held head while its checks can still move the merge at the timeout. It reads them in this order. A pull request no longer open holds nothing, and a merge reading `UNKNOWN` holds. A `BLOCKED` merge ends the poll on the first read where a required check is stuck in a shape waiting does not clear. A check running long or gated is not such a shape. A failed one is the exception while a newer run of its workflow on the head has not finished, which the digest names `CHECK SUPERSEDED`. Next, a workflow run on the head that has not completed holds. A run's later jobs enter the rollup only as their `needs:` finish, so a green rollup proves nothing while one is open. The status line names those runs. Next, a merge reading `CLEAN`, `UNSTABLE`, or `HAS_HOOKS` ends the poll. Otherwise a required check still settling holds, one running long included, and so does any settling check while no required one has posted. A `BLOCKED` merge also holds while a check the base branch's rules require has not registered. An empty rollup holds under `BLOCKED`, `BEHIND`, or `DRAFT`. `--min-rounds N` counts what `status` prints as `rounds=`, every round on every head, refusals included, so N is the `rounds=` a `status` read before a re-request.

`wait` exits `40` when no Copilot review covers the head and the reviewer has said why in another shape. A plain comment newer than its newest review carries no commit, so it satisfies no coverage check whatever it says, and its body prints whole because its wording is the only thing separating a refusal from a remark. A comment older than the newest review is spent. Every connection reads the newest `WINDOW` nodes rather than the reviewer's own, so `window_blind` guards both sides: blind on comments reads as `answered_outside_review=unknown`, and blind on reviews reports nothing and lets the wait keep polling. A request that returns success and adds neither a pending reviewer nor a review-request event also exits `40`, as `status=REQUEST_NOT_RECORDED`, as does a reviewer writing under a renamed login, `status=REVIEWER_LOGIN_DRIFT`.

Expand All @@ -230,7 +230,7 @@ The digest reports the completed head review's effective effort as `lite`, `bala

`status` and `wait` exit `50` where the round covering the head opens on a verdict other than the clean one while it states no open finding, lists none, opened no thread, and collapsed nothing, and no thread is open. Its headline prints under `VERDICT WITH NO COUNTED FINDING`, since it is the one place that round names what it flags. The code repeats on that head after the answer, since nothing here reads one, and where a check on that head has not concluded, `wait` names a bounded `gh pr checks --watch` to watch it with. A clean verdict on the same terms prints its headline under `CLEAN VERDICT HEADLINE`, advisory and gating nothing.

`wait` exits `44` when the review loop has closed but a required check sits in a shape no wait clears. The digest carries `checks=N/M` beside the merge word and names the shape as `stuck=`, because `BLOCKED` alone is worn by a red check, a check nothing is running, an unresolved thread, and a missing approval alike. `NOT_POSTED` is a required status nothing has posted, `NOT_PICKED_UP` a check queued with no runner past `--check-grace`, `RUNNING_LONG` a check running past `--check-stall`, and `FAILED` a verdict. A check merely still running is none of these and exits `0`, since `wait` returns the moment coverage lands and the checks are nearly always still going then. `44` also requires that the stuck check be required, read from each rollup node's `isRequired`, and that the merge read `BLOCKED`. A rollup member of an unknown type is named on `CHECKS PARTIALLY UNREAD`, a rollup past `CHECKS_WINDOW` contexts on `CHECKS TRUNCATED`, and a payload with no commit matching the head on `CHECKS UNREADABLE`, rather than any of them reading as a clean `checks=0/0`. A `CheckRun`'s `PENDING` is dispatched and not begun while a `StatusContext`'s is under way, so the two are translated rather than renamed. `--check-grace` must be less than `--check-stall`, and a negative value for either is rejected by name.
`wait` exits `44` when the review loop has closed but a required check sits in a shape no wait clears. The digest carries `checks=N/M` beside the merge word and names the shape as `stuck=`, because `BLOCKED` alone is worn by a red check, a check nothing is running, an unresolved thread, and a missing approval alike. `NOT_POSTED` is a required status nothing has posted, `NOT_PICKED_UP` a check queued with no runner past `--check-grace`, `RUNNING_LONG` a check running past `--check-stall`, and `FAILED` a verdict. `GATED` is a check its environment's protection rule holds past `--check-grace`. On the Copilot path, a check merely still running is none of these and exits `0`. `wait` returns the moment the round lands, and the checks are nearly always still going then. A held head polls its checks instead. There a check running long or gated holds the poll to `30` rather than ending it at `44`. `44` also requires that the stuck check be required, read from each rollup node's `isRequired`, and that the merge read `BLOCKED`. A rollup member of an unknown type is named on `CHECKS PARTIALLY UNREAD`, a rollup past `CHECKS_WINDOW` contexts on `CHECKS TRUNCATED`, and a payload with no commit matching the head on `CHECKS UNREADABLE`, rather than any of them reading as a clean `checks=0/0`. A `CheckRun`'s `PENDING` is dispatched and not begun while a `StatusContext`'s is under way, so the two are translated rather than renamed. `--check-grace` must be less than `--check-stall`, and a negative value for either is rejected by name.

`comment` posts one answer in the pull request conversation. A suppressed finding has no thread, so this is the path that records its disposition. The command reads the pull request node ID in the same run and exposes no ID argument. It confirms the returned comment URL and exact body before it reports success. A missing target exits `65`, and an unconfirmed response exits `66`, after which the conversation is inspected before any retry. With `--fingerprint` the body is the acknowledgment of an unplaced set, posted with its marker beneath it.

Expand Down
52 changes: 31 additions & 21 deletions scripts/pr_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,23 +114,28 @@
carries any Copilot activity. The loop runs in-process, so a 45-minute wait costs one
agent turn, not 90.
Exit 0 = review present, or on a held head an attested local pass with its required
checks settled. 30 = still pending at the timeout, a required check still settling on
a held head printed as `status=CHECKS_PENDING`, or a round past --min-rounds not yet
landed. 40 = no Copilot review covers the head: the reviewer answered outside a formal
review, the request returned success and recorded nothing on the pull request, or the
reviewer writes under a login no query filters on. 41 = the newest round is a refusal
or a quota state, the kind printed in its status line and, for a refusal on this pull
request, in the digest's `refusal=`: a refusal on the
head, the pull request's newest review being a quota or error refusal on any head, or
the reviewer's newest activity elsewhere in the repository being a quota refusal
nothing answered since while this head carries none of its own. Proceed on the other
reviewers' coverage rather than retrying, and pass --ignore-quota-signal once the
quota is believed to have reset. 42 and 50 = the review landed and `status`'s readings
checks settled. Where an attested local pass covers an open pull request's held head,
every workflow run on it has completed.
30 = still pending at the timeout. On a held head it is any hold
`status=CHECKS_PENDING` names, a required check still settling and a workflow run still
open among them. It is also a round past --min-rounds not yet landed.
40 = no Copilot review covers the head: the reviewer
answered outside a formal review, the request returned success and recorded nothing on
the pull request, or the reviewer writes under a login no query filters on. 41 = the
newest round is a refusal or a quota state, the kind printed in its status line and, for
a refusal on this pull request, in the digest's `refusal=`: a refusal on the head, the
pull request's newest review being a quota or error refusal on any head, or the
reviewer's newest activity elsewhere in the repository being a quota refusal nothing
answered since while this head carries none of its own. Proceed on the other reviewers'
coverage rather than retrying, and pass --ignore-quota-signal once the quota is believed
to have reset. 42 and 50 = the review landed and `status`'s readings
apply to it. 44 = the review loop closed, the merge reads BLOCKED, and a required check
is in a shape no wait clears, queued with nothing acting on it, expected and never
posted, running far past what the job costs, or failed. 49 = no Copilot round covers
this head and none was requested, the pull request merging into a branch other than
the default with no attestation vouching for the head. 64 = the write scope could not
is in a shape no wait clears. It is queued with nothing acting on it, expected and
never posted, running far past the job's cost, held by its environment, or failed. On
a held head a check running long or held by its environment polls to 30 instead.
49 = no Copilot round covers this head and none was requested, the pull request
merging into a branch other than the default with no attestation vouching for the
head. 64 = the write scope could not
be established or excludes the target, checked before the auto-request or any poll.

Reading is the bulk of this and the writing commands are a trade rather than a free win. A
Expand Down Expand Up @@ -173,6 +178,7 @@
checks_stuck,
checks_tally,
held_checks_open,
runs_open,
)
from pr_review_rounds import (
ACK_MARKER,
Expand Down Expand Up @@ -320,7 +326,7 @@
checkSuite{ databaseId status app{ slug databaseId } workflowRun{ databaseId workflow{ databaseId } } } }
... on StatusContext{ context state createdAt isRequired(pullRequestNumber:$n) }
}}}
checkSuites(last:50){ nodes{ databaseId status workflowRun{ databaseId workflow{ databaseId } } } }
checkSuites(last:50){ nodes{ databaseId status app{ slug } workflowRun{ databaseId workflow{ databaseId } } } }
Comment thread
ptr727 marked this conversation as resolved.
Resolved
}}}
""".replace("__CHECKS_WINDOW__", str(CHECKS_WINDOW))
Q_HELD = """
Expand Down Expand Up @@ -1974,6 +1980,7 @@ def held_read() -> dict:
)
if verdict:
return verdict
runs = runs_open(final)
if covered and held_checks_open(
final,
checks,
Expand All @@ -1984,10 +1991,12 @@ def held_read() -> dict:
):
print(
"status=CHECKS_PENDING an attested local pass covers this head, and by the "
"timeout a check had not concluded, none had posted, a check the base branch "
"requires had not registered or came from another app, or the merge was still "
"UNKNOWN, so the merge is not ready yet: wait again, or read the checks above"
"timeout a check had not concluded, none had posted, a workflow run on the head "
"had not completed, a check the base branch requires had not registered or came "
"from another app, or the merge was still UNKNOWN, so the merge is not ready yet: "
"wait again, or read the checks above"
+ _unmet_note(required_for(final, owner, repo), checks)
+ (f" (not completed: {', '.join(runs)})" if runs else "")
)
return 30
# The rollup carries checks no ruleset requires, and CLEAN proves no gate is outstanding.
Expand All @@ -1998,7 +2007,8 @@ def held_read() -> dict:
"BLOCKED, and a required check is in a shape waiting does not clear, "
f"required stuck {', '.join(repr(g) for g in gates)}: read their lines in the "
"block above, since a starved check wants a re-run, an unposted one its poster, "
"a long one a judgment, and a failed one a fix. It need not be all that blocks "
"a gated one its approval or its timer, a long one a judgment, and a failed one a fix. It need "
"not be all that blocks "
"the merge, because BLOCKED is also worn by a thread or a missing approval"
)
return 44
Expand Down
Loading
Loading