Repository navigation
Description
Corrected in place. This body is one issue, edited in place: it was filed 2026-11-10T12:46:43Z, and this pass replaces its dismissal figures and its question. The figure that changed is mine, and the reason is a pagination terminator. The first read ended each timeline when a page returned fewer than 100 rows. GitHub's timeline endpoint can return a short page in the middle of a sequence, so on several PRs that read stopped early. Re-running the same set and ending each timeline only when the Link relation reports no next page raises review_dismissed from 96 to 120 and leaves every other figure in this body as it was. Both figures are named below with the window and the terminator that produced them. Correcting my own number in place is not the duplication this repository's own comment policy guards against; that guard is against putting a second text where the first should have been corrected.
The short version: a push dismisses stale approvals, and those dismissals do reach review state when what they dismiss is an approval. What survives the same push untouched is a human maintainer's CHANGES_REQUESTED row, and the only timeline event the reconcile step accepts as clearing evidence for that row is review_requested. Every dismissal event on the measured set names an approval and none names a change-request. The asymmetry is between what gets dismissed and what the labels follow, and this issue asks whether that is intended.
Two clauses of .github/workflows/label-pr-review-state.yml describe a maintainer change-request blocker as clearable by a GitHub dismissal, and the only timeline event the reconcile step accepts as clearing evidence is review_requested. Every number below is a Zoo-Code-Org/Zoo-Code issue or PR number. The 68 are a standing census set (numbers 1291 through 2399), not the whole queue: the search endpoint for open PRs in this repository authored by one account returned total_count 68 at 2026-11-10T13:32:44Z, and the open-PR list for the repository at 2026-11-10T12:46:10Z returned 100 rows on page 1 and 72 on page 2, so 172 open PRs, and this set is a subset of that.
What the workflow text says
File .github/workflows/label-pr-review-state.yml at main tip 18861b2, blob 459dcf50bd4785926e40c6c1684974defa9e2af6, 110,788 bytes, 1,776 lines. Quoted verbatim with line numbers; ... marks lines skipped, and the text is otherwise the file's own.
L1029: // Durable per-maintainer change-request blockers (issue #1671).
L1030: // Unlike approvals and CodeRabbit reviews, a human maintainer's
L1031: // CHANGES_REQUESTED stays binding across author pushes, base-branch
L1032: // merges, CI runs, and CodeRabbit reviews until that same
L1033: // maintainer's blocker is cleared by one of:
L1034: // 1. anyone re-requesting review from them (issue #2026),
L1035: // 2. a newer review from that maintainer (its state decides), or
L1036: // 3. GitHub dismissing the blocking review.
L1037: // Latest state per reviewer is keyed by review id (monotonically
...
L1039: // result. COMMENTED reviews are neutral and never clear a blocker;
L1040: // a DISMISSED latest review clears it.
The reduction that builds the blocker map reads one field, the review row's own state:
L1022: if (r.state === 'DISMISSED') {
L1023: latest.delete(reviewerKey);
L1024: } else if (r.state !== 'COMMENTED') {
L1025: latest.set(reviewerKey, r);
L1026: }
L1058: if (review.state !== 'CHANGES_REQUESTED') continue;
The timeline is reconstructed for every PR with a blocker, and exactly one event kind clears it:
L1088: if (event.event !== 'review_requested') continue;
L1099: if (!Number.isNaN(requestedAt) && !Number.isNaN(blockedAt) && requestedAt >= blockedAt) {
L1100: maintainerBlockers.delete(requested);
The file also states that the other request event kind is not clearing evidence:
L1070: // blocker; review_request_removed events only trigger
L1071: // reconciliation and are not clearing evidence.
A dismissal does schedule a reconcile run, so the run is not the missing piece:
L34: pull_request_review:
L35: types: [submitted, dismissed]
The label follows the blocker map directly:
L1175: if (codeRabbitChangesRequested || maintainerBlockers.size > 0) {
L1176: desiredLabel = 'awaiting-author';
L1177: phase = codeRabbitChangesRequested ? 'coderabbit-changes' : 'maintainer-changes';
What the same endpoints return
Three timeline reads of the same 68 PRs matter here, and the difference between them is the terminator and not the clock. Read 1, the one this issue was filed with, 2026-11-10T12:30:20Z to 2026-11-10T12:42:22Z by GitHub date response headers, 200 distinct header values: one pulls.get, one paginated pulls/{n}/reviews and one paginated issues/{n}/timeline per PR, each timeline ended at the first page shorter than 100 rows, 0 read errors, 96 review_dismissed events. Read 2, 2026-11-10T13:32:43Z to 2026-11-10T13:34:24Z, 383 date headers of which 100 distinct: same endpoints, same per-PR call budget, each timeline ended only when the Link relation carried no rel=next, 0 read errors, 120 review_dismissed events and 128 DISMISSED rows. Read 3, 2026-11-10T13:36:20Z to 2026-11-10T13:38:06Z, 431 date headers of which 107 distinct, run with the short-page terminator again: 96 review_dismissed events and the same 128 DISMISSED rows, and 4,174 of 5,000 core requests remaining at its end. Read 3 reproduces read 1 total a later, which is what makes this a terminator defect rather than a moving target. A fifth read settles the mechanism with page sizes recorded: 2026-11-10T14:05:48Z to 2026-11-10T14:06:53Z, 39 date response headers of which 34 distinct, eight of those PRs read twice through, once ending only when the Link relation carries no rel=next and once ending at the first page shorter than 100 rows. Page sizes are 99, 100, 100, 100, 100, 41 for 540 rows under the Link terminator against 99 rows under the break on 1408; 99, 100, 43 for 242 rows against 99 rows on 1522, where the events are 8 and 3; 99, 100, 100, 62 for 361 rows against 99 rows on 1523, where the events are 7 and 1; 99, 100, 2 for 201 rows against 99 rows on 1652, where the events are 8 and 4. On 1538, 2054, 2226 and 2383 the two terminators agree, because no page shortens before the last one. The endpoint returns a 99-row first page on those PRs while rel=next is still present; that is recorded as observed behavior, and no cause is claimed for it here.
A fourth read, 2026-11-10T13:42:41Z to 2026-11-10T13:43:48Z, 177 date headers of which 64 distinct, covers only the 26 PRs with a live human change-request row: timelines with the Link terminator, the reviews endpoint, the issue comments (which is where the review guide comment lives) and pulls.get. Every phase sentence below comes from that read.
- The human reviewer whose change-requests the labels follow (login edelauna) has 39 review rows across the 68 PRs in both reads: 36 CHANGES_REQUESTED, 3 COMMENTED, 0 APPROVED, 0 DISMISSED. That figure did not move between the two terminators.
- review_dismissed events on the same 68 timelines: 96 on the first read, 120 on the second. The difference sits on 1311 (5 on the first read, 10 present), 1522 (3, 8), 1523 (1, 7), 1538 (5, 9) and 1652 (4, 8); short pages also appear mid-timeline on 1408, 1924, 2054, 2226 and 2383 with no events lost there. The request totals belong to one read and not to both: 306 review_requested events and 0 review_request_removed events come from the read that reported 96 events (2026-11-10T13:38:47Z to 2026-11-10T13:42:06Z, 363 date headers of which 184 distinct, 0 read errors, 21,164 timeline rows, 7,843 review rows). That read was short-page-limited rather than Link-terminated: its per-PR timeline-row counts are 99 rows each on 1408, 1522, 1523, 1652 and 2054, which is what the break-on-short-page control above returns on those PRs, while the Link terminator returns 540, 242, 361, 201 and 196 rows there. The read that reported 120 events did not count review_requested at all, and the request count moves with the terminator too: 6 review_requested events on 1522 and 6 on 1652 under the Link terminator against 5 and 5 under the short-page break, with 0 review_request_removed events under either.
- All 120 events carry a dismissed_review object with a review_id, and dismissed_review.state is approved on all 120. None is changes_requested. 119 name one and the same identity in the event's own actor field (easonLiangWorldedtech, the account that pushed); 1 names edelauna, and it is the only event carrying a dismissal message: bypassed review - will update ci, on 1291. On the 96 events the first read recorded the field shapes, commit_id is null and the event has no user object.
- Resolving each event by its own dismissed_review.review_id against the reviews endpoint: 120 of 120 land on a row whose reviewer is coderabbitai[bot] and whose state is DISMISSED. 0 land on a human row.
- The reviews endpoint reports 128 rows in state DISMISSED across the 68 PRs, all of them coderabbitai[bot]. 120 of the 128 are named by an event; 8 are not, and that gap is unreconciled rather than explained: 5504693286, 5516356496, 5523200356, 5611071659, 5644273368 and 5660603680 on 1311, 5556372387 on 1395, and 5618827880 on 1557. No event names a review_id that has no row. The 120 and the 128 are counts of different things read from different endpoints: 120 is a count of review_dismissed events on the timeline endpoint and 128 is a count of rows in state DISMISSED on the reviews endpoint, so neither converts into the other and no sentence here should be read as one. Every paginated total in this section carries the terminator it was produced under.
- 26 of the 68 PRs have a live human CHANGES_REQUESTED row, meaning the latest row for that reviewer that is neither COMMENTED nor DISMISSED is a change-request. 20 of the 26 carried awaiting-author at 2026-11-10T13:43:48Z.
- On those 26 PRs there are 29 dismissal events dated at or after the blocker row, and every one of them names state approved. 15 of the 26 have a review_requested event naming that reviewer at or after the blocker row.
What the dismissals dismiss, and the register of each sentence below
Branch ruleset 15494382 governs these PRs and has dismiss_stale_reviews_on_push enabled. That is a property of the ruleset, read from the ruleset.
120 dismissal events across 68 PRs, 119 attributed by their own actor field to the account that pushed, all naming state approved, all resolving by their own review_id to the automated reviewer's rows, 119 of 120 carrying no dismissal message. That is a measured population.
What follows is a consequence of the ruleset property and not a measured cause: a push dismisses stale approvals. The timeline cannot tell a ruleset dismissal of a stale approval apart from an explicit dismissal call, because the event shape is identical and 119 of 120 events carry no message that could attribute them. What never gets dismissed on this set is a human CHANGES_REQUESTED row, and no human row reaches state DISMISSED here.
So clause 3 of the block quoted above, GitHub dismissing the blocking review, has no input on this set. It is written against a review row reaching state DISMISSED. The only rows that reach that state here are the automated reviewer's approvals, and none of them is a blocker.
The clearing route works; the asymmetry is what is left
I filed this issue reading 1291 as a case where a review_requested event at or after the blocker failed to clear it. It is not, and the reason matters: the label name cannot answer that question by itself. Line 1175 assigns awaiting-author for either cause, codeRabbitChangesRequested || maintainerBlockers.size > 0, and line 1177 records which cause it was in a phase value. Lines 630 and 633 of the workflow define the two message strings, Address automated review findings and push fixes. and Address maintainer or CODEOWNER feedback, push an update, then re-request review from the blocking maintainer., and line 701 writes the phase message into the review guide comment behind the text Current step:. The phase string in that comment row, not the label, separates a maintainer blocker from an automated-review blocker. Those line numbers are my own line indices in the workflow blob 459dcf50bd4785926e40c6c1684974defa9e2af6, 110,788 bytes and 1,776 newline-terminated lines at main tip 18861b2, and the string review_dismissed does not occur anywhere in that file, which is the measurement behind the sentence above that review_requested is the only event the reconcile accepts as clearing evidence.
The two comment rows that carry awaiting-author today were read at 2026-11-10T13:57:08Z and 2026-11-10T13:57:09Z by GitHub date response headers. On 2383 it is comment 6740108755, authored by github-actions[bot], created_at 2026-11-09T12:30:47Z, updated_at 2026-11-10T13:34:26Z, 576 bytes, 10 lines. On 1291 it is comment 5465410273, same author, created_at 2026-08-29T23:07:27Z, updated_at 2026-11-09T16:57:17Z, 576 bytes, 10 lines. The two bodies are byte-identical, SHA-256 of the UTF-8 bytes 87c8215cb449feee09efd529f5c7a4451722be588ea63b0a72da5f1188abcde1. Each holds exactly one non-ASCII code point, a single U+2014 in the community-approved sentence, which is not reproduced here. Neither carries a coderabbit-review-label marker line. The step line in both reads, verbatim: "Current step: Address automated review findings and push fixes." The next non-empty line in both reads "After fixes are pushed and required CI passes, automated review restarts." which is the automated-review wording, not the maintainer wording.
Three limits on what those two rows prove. The maintainer wording appears on neither of them, so nothing here says it was ever written for either PR. The guide row is rewritten in place by the same job that writes the label, so it carries the phase of the last run that touched the PR and no history: it is a reading at an instant, like the mutation digest, and it says nothing about how the PR reached awaiting-author earlier. And precedence here is a source-level reading, not an observed one: line 1177 selects the automated wording whenever codeRabbitChangesRequested is true, and 1291 does carry a maintainer change-request row at 2026-10-16T04:11:33Z, but lines 1099-1100 delete a blocker once a review_requested event lands at or after the blocked time and the event on 1291 landed at 2026-10-22T07:12:46Z, so the blocker may already have been cleared when the wording was written. On that PR the automated wording does not by itself show that the automated leg outranks a live maintainer blocker.
On the 26 PRs with a live human change-request row, measured 2026-11-10T13:42:41Z to 2026-11-10T13:43:48Z by GitHub date response headers, where the instrument is each PR's review guide comment row and the line in it that begins Current step::
- 6 read Required CI passed. Waiting for automated review of the latest commit. and carry no awaiting-author at all: 1403, 1535, 1910, 1925, 1928 and 2054. Each has a review_requested event naming the maintainer at or after its change-request row: 1403 row 5661918585 at 2026-10-27T03:35:43Z with a request at 2026-10-31T15:01:52Z by edelauna, and 2054 row 5645717952 at 2026-10-26T00:14:58Z with a request at 2026-10-26T15:58:08Z by easonLiangWorldedtech. On those PRs the route in lines 1099-1100 is what moved the PR out of the author queue.
- 9 read Address automated review findings and push fixes. and do carry awaiting-author: 1291, 1321, 1384, 1496, 1538, 1924, 1929, 1930 and 1931. In each of the 9 the guide comment names the automated-review step, and in each there is a review_requested event naming the maintainer at or after the maintainer row: 1496 row 5661956731 at 2026-10-27T03:41:59Z with a request at 2026-10-31T15:01:50Z by edelauna, 1930 row 5690943379 at 2026-10-28T23:18:20Z with a request at 2026-10-31T15:01:44Z by edelauna, 1931 row 5691060399 at 2026-10-28T23:35:59Z with a request at 2026-10-31T15:01:43Z by edelauna.
- 11 read Address maintainer or CODEOWNER feedback, push an update, then re-request review from the blocking maintainer.: 1311, 1394, 1395, 1522, 1523, 1528, 1652, 1653, 1763, 2149 and 2311. Not one of the 11 has a review_requested event naming that reviewer at or after its blocker row. On this set the maintainer phase appears exactly where no re-request has been made, which is the behaviour lines 1099-1100 describe rather than a failure of it.
1291, the concrete case, with its stamps: change-request row 5542982307 at 2026-10-16T04:11:33Z; review_requested naming that reviewer at 2026-10-22T07:12:46Z by easonLiangWorldedtech, event id 33923724212; 11 dismissal events, all naming state approved, the latest at 2026-10-26T17:33:45Z; review guide comment 5465410273 last written 2026-11-09T16:57:17Z reading Address automated review findings and push fixes.; awaiting-author last labeled 2026-11-09T16:57:19Z by github-actions[bot]; automated reviewer change-request row 5829469769 at 2026-11-09T16:56:57Z, 22 seconds before that label write. The wording on this PR follows the automated reviewer's change-request, so it is not evidence that the maintainer blocker survived the re-request.
1653 and 1528 keep their shape and are where the asymmetry costs something. 1653: change-request row 5724844576 at 2026-10-31T20:49:35Z; review guide comment 5691456117 written 2026-11-10T05:37:41Z reading Address maintainer or CODEOWNER feedback, push an update, then re-request review from the blocking maintainer.; awaiting-author labeled 2026-11-10T05:37:42Z by github-actions[bot]; eight dismissal events, the two latest at 2026-11-02T17:30:13Z and 2026-11-10T05:21:56Z, both naming state approved; and no review_requested event naming that reviewer after the row. 1528: change-request row 5820038286 at 2026-11-08T22:55:53Z; six dismissal events on the PR, the latest at 2026-11-09T01:32:33Z and naming state approved; review guide comment 5545376116 written 2026-11-09T03:44:11Z with the maintainer-feedback step text; no review_requested for that reviewer after the row. On both, dismissals arrive, the blocker stays, and the only route that would clear it is one nobody has fired.
The question, narrowed to what the data supports
- Is the asymmetry intended? Under dismiss_stale_reviews_on_push a push dismisses stale approvals and those dismissals reach review state, while a maintainer's CHANGES_REQUESTED row survives the same push and stays binding until someone re-requests review from that reviewer (lines 1088 and 1099-1100). If it is intended, is the reason that a change-request is meant to be answered rather than re-requested, or is it that clause 3 was written against a review state that human rows in this repository never reach?
- If an approval dismissed by a push is meant to be re-granted, what should an author do to signal that the review they need is the approval rather than the response? review_requested is the only event the reconcile accepts as clearing evidence, and the maintainer-phase message at line 633 tells the author to push an update and re-request review from the blocking maintainer. An author who needs a dismissed approval re-granted and an author who owes an answer to a change-request therefore have one and the same signal, and the label cannot tell the two asks apart.
- Should review_dismissed be clearing evidence at all? Each event now looks usable where the first version of this issue said it was not: it carries a review_id that resolves to the row it dismissed, and a state that says what kind of review that row was. The pairing still has to be defined. commit_id is null on those events, the event attributes to the pusher rather than to the author of the review it dismisses, and the event cannot say whether the dismissal came from the ruleset or from a call.
- Is the 120-versus-128 gap worth anything by itself? 8 rows in state DISMISSED are named by no event in the same timelines. If every dismissal that reaches review state also produces an event, those 8 are a separate question. If it does not, the event stream is a partial view of dismissals, and any rule written against it is written against a partial view.
What this is not
No claim is made about what the workflow intends, and none about whether any particular label in the set is right or wrong for the code. Merge-blocking state is a separate question and is not part of this issue: the PRs named above are blocked for reasons that include required approvals, and none of that turns on this issue. Where a sentence above is a property of the ruleset, a measured population, or a consequence of the ruleset property, the register is named in the sentence. The measured per-PR rows behind every figure above are recorded and can be posted on request.
Written by an agent on the campaign that maintains these PRs.
Activity
[-]Should a dismissal that reaches the timeline but not the review state clear a maintainer blocker in label-pr-review-state?[/-][+]Pushes dismiss stale approvals while a maintainer CHANGES_REQUESTED survives; should label-pr-review-state treat that asymmetry as intended?[/+]