Repository navigation
Review loop: fix dropped forced reviews, stale labels, REVIEW_STATE gaps, and id reuse - #21948
Conversation
Sentinel — merge gate status (preview)Warning
|
| Gate | What it needs | |
|---|---|---|
| ✅ | G1 review-ran | Nothing to do: review current at e36639644. |
| ✅ | G2 findings-answered | Nothing to do: every finding answered. |
| 🔴 | G3 right-approver | Needs approval from a member of any review team (pulumi/docs-blog-review, pulumi/docs-guild, pulumi/docs-marketing-review, pulumi/docs-tools) or a repository administrator — no qualifying human approval at this head (bot approvals never count). |
| ➖ | G4 infra-evidence | Doesn't apply to this PR: no changed path affects the deploy. |
| ➖ | G5 oversized-ack | Doesn't apply to this PR: not oversized. |
Break glass: a member of a routing team (.github/review-routing.yml teams:) can apply review:waived, which is logged.
Evaluated at head e36639644.
|
Your site preview for commit e366396 is ready! 🎉 http://www-testing-pulumi-docs-origin-pr-21948-e3663964.s3-website.us-west-2.amazonaws.com |
Reviewer's guide v5 — not for the authorTip Check the PR author: nothing here is yours. Your to-do list is the "Author action guide" comment. Approval needed from: @pulumi/docs-tools — any member's approval satisfies the merge gate. Note What this PR changes:
The blocking risk is a workflow or script regression that drops, mislabels, or mis-gates reviews on every PR. The infra criteria and the fact-check pipeline ran; the only extracted claims came from test fixtures. Review confidence:
|
Author action guide v5 — nothing blocks mergeNote Nothing here needs an answer from you. A human reviewer still approves the merge. This PR hardens the v3 review loop (forced-review dispatch, label reconciliation, REVIEW_STATE handling, finding-id reuse); the review checked the workflow and script changes against the infra criteria. Full evidence: verification trail, investigation log, review history. Review v5 · updated 2026-09-28T22:31:43Z · head commit e366396 Please don't edit or delete this comment; it's the review's record. |
|
@claude pushed daddce5: Generated by Claude Code |
|
Independent adversarial review of head Confirmed
Plausible: fix or justify in the PR body
Checked and fine:
Generated by Claude Code |
- Triage: merge the paginated file list via --slurpfile (new with-pr-files.sh). `jq --argjson` over ~1,000 files overflowed MAX_ARG_STRLEN, so triage went green without review:oversized. - #new-review ids: the redispatch job forwards prior_high_water, and a forced run that fails after its card was cleared carries the mark on its failure notice (<!-- REVIEW_HIGH_WATER n -->), which the composer and claude-new read back. - Reconcile cron: actions: write and a Sentinel dispatch after each restamp or un-stale. - apply-update: prune only file-looking change bullets inside "What this PR changes"; narrow _OPEN_WORK_RE to asks addressed to the author. - Sentinel: review:oversized is the label again, not size, so approved PRs don't flip; triage re-dispatches the Sentinel when it moves review:oversized or review:trivial (#21936's race). - restamp-base-merge: re-read the card before PATCH; a patchless file must keep its blob sha. - cleanup-review-leftovers: age notices against the card's composition stamp and the notice's own edit time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cni84G1WNvPbAZ5BwqaLXJ
…gacy card picks - claude-new.yml: trust the same bots as the other lanes (workprentice's #new-review was silently dropped), read permissions with gh api, and say so on the PR when a request is declined. A sanity test keeps the four allowlists identical. - apply-update.py: concede records not-applicable, and every refresh backfills a disposition for any resolved row that lacks one. - compose-review.py: finding ids continue above the previous review's high_water instead of restarting at F1. - pinned-comment.sh: a v3 author card beats a legacy 1/1 monolith; new prune-legacy subcommand. - sentinel.py: read legacy pages posted by pulumi-bot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GtKzf3eR8238vqCLcc1qZt
…ws, fix Sentinel PR resolution - restamp-base-merge.py: a base-only merge with an unchanged diff moves the v3 card's head carriers instead of staling the review (mark-stale and the reconcile cron both use it). - review-label-reconcile.yml: read the v3 card (the legacy monolith on #21066 re-staled it every two hours), and add the missing un-stale pass. - sentinel.py: G2 no longer calls a stale card 'every finding answered'; G1 stops advising a push that refreshes nothing; reads retry a 5xx. - review-sentinel.yml: trust workflow_run.pull_requests only for a pull_request run at that head (every other run evaluated #2), and refuse to publish a check-run with no head SHA. - Both lanes delete earlier 'Review errored' notices once a review lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GtKzf3eR8238vqCLcc1qZt
- normalize-v3-draft.py: drop a model-written F? detail block (#21892), restore a deleted empty 🚨/❓ section and a deleted composed finding (#21885). Both captured drafts are now regression fixtures. - apply-update.py: map the card's own bucket words ('blocking') to the closed set (#21761); treat resolve/concede on an already-resolved id as a no-op (concurrent refresh, #21614) and drop actions on ids the card never had (#21785) instead of failing the refresh. - claude-update.yml: run the deterministic publish chain from the default branch, not the PR head (#21908's own half-finished validator refused master's cards); list the add buckets in the prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GtKzf3eR8238vqCLcc1qZt
…cleanup script - apply-update.py: a refresh drops 'What this PR changes' bullets for files that left the PR, settles review-confidence notes that point at resolved findings or an empty list, and removes a summary naming open work under 'nothing blocks merge' (#21801, #21798, #21790). - One definition of oversized: triage-classify.is_oversized, which the Sentinel now computes from the PR instead of racing triage's label (#21936). Triage reads the paginated file list and changedFiles, so the 150-file axis and the '1,156 files' count work. - review-sentinel.yml: name the job so it can't pass for the gate. - cleanup-review-leftovers.py: dry-run sweep for the stale labels, errored notices, legacy pages and stray status comment already on PRs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GtKzf3eR8238vqCLcc1qZt
The compose step runs the PR head's compose-review.py, so a branch cut before the flag existed would reject it and error its review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GtKzf3eR8238vqCLcc1qZt
This PR's own v1 brief showed a header-only⚠️ table under 'Check the⚠️ items, then approve': the model filed both rows off, and the '_Not your area?_' hint the composer puts beside rows kept the section from reading as empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GtKzf3eR8238vqCLcc1qZt
- Triage: merge the paginated file list via --slurpfile (new with-pr-files.sh). `jq --argjson` over ~1,000 files overflowed MAX_ARG_STRLEN, so triage went green without review:oversized. - #new-review ids: the redispatch job forwards prior_high_water, and a forced run that fails after its card was cleared carries the mark on its failure notice (<!-- REVIEW_HIGH_WATER n -->), which the composer and claude-new read back. - Reconcile cron: actions: write and a Sentinel dispatch after each restamp or un-stale. - apply-update: prune only file-looking change bullets inside "What this PR changes"; narrow _OPEN_WORK_RE to asks addressed to the author. - Sentinel: review:oversized is the label again, not size, so approved PRs don't flip; triage re-dispatches the Sentinel when it moves review:oversized or review:trivial (#21936's race). - restamp-base-merge: re-read the card before PATCH; a patchless file must keep its blob sha. - cleanup-review-leftovers: age notices against the card's composition stamp and the notice's own edit time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cni84G1WNvPbAZ5BwqaLXJ
…ames Follow-up to the verification pass on 6b32a93: - Read REVIEW_HIGH_WATER only from CLAUDE_PROGRESS failure notices. Cards and triage prose can quote PR text, so a quoted marker could inflate ids. - _OPEN_WORK_RE no longer matches bare "you need/must/have to", which a docs PR's summary uses about the page's reader. - A scoped package lead (`@pulumi/aws`) isn't a path, so it's not pruned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cni84G1WNvPbAZ5BwqaLXJ
a3adc39 to
b743c3b
Compare
- Drop the Sentinel job's display name. /pr-review filters the job by "sentinel", so the rename made every stamp preflight wait on it. - On a push, flag a not-yet-labelled PR as oversized by the line axis only. The paginated file count made the 150-file axis reachable, which would have flipped open PRs already reviewed under the normal gates. The classifier reports `oversized_by_lines` for this. - apply-update: keep site-URL and renamed-file change bullets (the file list now carries previous_filename), and drop a summary only when it asks the author about a counted set of items and isn't negated. - Page the PR file listings 100 at a time. - mark-stale's restamp step no-ops on a base branch that predates the script (stacked PRs) instead of erroring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cni84G1WNvPbAZ5BwqaLXJ
|
@claude I rebased onto master (b743c3b) and pushed e366396 with the fixes from a blast-radius review of the effects on unrelated PRs. Each new test fails on b743c3b.
Plausible but not changed:
Generated by Claude Code |
Proposed changes
This PR fixes the review-loop rough edges from the 2026-09-28 survey of open PRs. I root-caused each item from run logs, debug artifacts, and the PR timelines before fixing it. Every fix has a regression test, and two of the tests replay captured drafts from failed production runs.
make test-review-pipelineandmake lintboth pass.Session: https://claude.ai/code/session_01GtKzf3eR8238vqCLcc1qZt
#new-reviewnever landedclaude-new.ymlhad no trusted-bot allowlist, although the other three lanes had one. The collaborator API has nothing on a GitHub App, so run 36421951663 logged✗ User workprentice[bot] has accessand ended green having done nothing. The unquoted[bot]in thecurlURL is also a glob, so the permission came back empty. The "Pre-merge Review skipped" at 12:27:23 was the unrelated triage-chained run skipping onreview:oversized.gh api, and posted a reply on the PR when a request is declined.test_trusted_bot_allowlist_is_identical_in_every_access_checkfails if the four copies drift.#update-reviewedits the cards in place, far up the timeline, so the only visible signal is a 🚀 on the request.v3-section-order,bucket-split-faithful). v3 has no repair step.normalize-v3-draft.pyrules: rule 8 restores a deleted 🚨/❓ section in its empty form, and rule 9 re-appends a deleted composed finding verbatim from the evidence base.outstanding-issues. G2 scored the stale card ("every finding answered") while G1 rejected it. G1's "push to refresh" advice was wrong for a clean review.#update-review/#new-review.WORKPRENTICE.mdtells the bot to comment#update-reviewafter pushing a reviewer's change. I didn't add an auto-refresh for clean reviews: the unattended lane can't raise new findings, so it would only re-stamp a substantive push as reviewed, at the cost of a model call per push.test_g2_does_not_call_a_stale_card_answeredreview:stalenever clears after a merge from mastermark-stalestaled a base-only merge, and nothing ever un-staled a label.restamp-base-merge.pyhandles pushes where every commit since the reviewed head is a merge and the PR's +/- lines are unchanged (the same testcollect.pyalready uses). It moves the v3 card's head markers and nothing else, so the composition stamp and the stale-publish guard are unaffected. It re-reads the card right before writing and backs off if a refresh landed meanwhile, and fails closed on a large or binary file whose blob changed.mark-staleand the reconcile cron use it (on a stacked PR whose base predates the script, the step no-ops to the old stale transition), and the cron gains an un-stale pass: a current card gets its terminal label back from count-buckets. The cron re-dispatches the Sentinel after either repair.test_restamp_base_merge.py(8 cases, including a conflict resolution that changes the PR's lines, a card republished mid-decision, and a changed patchless file)#update-reviewrequests didn't stickCLAUDE_REVIEW 1/comment, which was a legacy monolith from August (run 36206993300).pinned-comment.sh last-reviewed-shatied the two1/1comments the same way.list_pinned_commentsnow returns only the v3 author card when one exists, and the cron reads through it. Newprune-legacysubcommand, which the v3 publish calls, so the two surfaces never coexist.clearstill sweeps everything.test_pinned_comment.pycases{}; 21801 F1, 21799 F2, 21798 F3, 21796 F1, 21793 F2, 21828 F2)concedewrote no disposition, so the ✅concede:annotation was the only record, while the card's note says an id absent from the block is OPEN. Sentinel G2, the worklist, and sla-sweep read row position, so they agreed with each other but not with the card, the README, or review-admin.concedenow recordsnot-applicable(actorupdate-lane, noteconceded: <reason>). That keeps the scraper's "conceded" classification and triggers no closed-set or schema change. Every refresh backfills a disposition for any ✅ row that lacks one, so older cards heal on their next update. The README now states the rule.test_concede_records_…,test_resolved_rows_without_a_disposition_are_backfilled…v3_may_demote). The sixth was a concurrent-refresh race ("F3 is already resolved"). #21892: the model wrote a#### F? · Do thisblock. #21083:PULUMI_BOT_TOKENhit its rate limit.F?detail block.apply-updatetreatsresolve/concedeon an already-resolved id as a no-op.bucket: "blocking". (2) It acted on an id it invented for an untrackedvalidate-pinned.pyagainst cards composed on master..review-tools/), so the PR under review also no longer supplies the scripts that run with the bot token and AWS credentials.test_add_bucket_uses_the_cards_words,test_an_invented_id_is_dropped…,test_resolve_and_concede_on_a_resolved_finding_are_no_ops422 No commit found for SHA:workflow_run.pull_requestswith[2, 71]for runs on master, so everyworkflow_run-triggered Sentinel evaluated #2 (and posted a status comment there). Run 35938827723 was a single HTTP 500. In both cases an emptyhead_shareached the check-run POST.RUN_PRis trusted only for apull_requestrun at that PR's head. The lanes already poke the Sentinel with the right number. Reads retry a 5xx twice (writes never retry). The publish step refuses an empty SHA with a clear error.test_a_transient_5xx_on_a_read_is_retriedpulumi-bot, because the v2 update lane runs onPULUMI_BOT_TOKEN, but G2 read onlygithub-actions[bot]pages.github-actions[bot]-only.test_a_legacy_page_the_v2_update_lane_posted_as_pulumi_bot_is_read--pr-filesfrom the paginated API, renamed files' old paths included); replaces confidence notes that point at settled findings or an emptytest_brief_drift_after_a_rework…,test_a_summary_naming_open_work…,test_change_bullets_that_do_not_name_a_file_are_kept,test_a_summary_that_reports_settled_checks_is_keptcompose_v3always started at F1. Worse,record-evidence.merge_dispositionsthen grafted the old F3'sfixedrecord onto an unrelated new F3.high_water, read from the live card.#new-reviewreads it before clearing the cards and passes it through asprior_high_water; the redispatch job forwards it. A forced run that errors or times out before publishing writes<!-- REVIEW_HIGH_WATER n -->on its failure notice, and the next review reads the larger of the card and those bot notices.test_a_rereview_continues_ids_above_the_prior_high_water,test_redispatch_forwards_every_new_review_input,test_failure_notices_carry_the_prior_high_water,test_review_state.py/authors/grid") survives. Rewriting it needs a model pass, which the update lane doesn't do.#new-review.gh pr view --json filesreturns one 100-file GraphQL page. That also made the classifier's 150-file oversized axis unreachable and cut domain/routing classification off at 100 paths.with-pr-files.sh(temp file +--slurpfile, never argv: a ~1,000-file list overflows the 128 KiB argument cap), and the comment useschangedFiles.pr_file_counttakes the larger of the two.test_oversized_threshold(1,156changedFilesbehind a 100-file page);test_with_pr_files.py(1,200 files through the helper and on into the classifier)review:oversizedwhile G5 says "not oversized"openedevaluation beat triage, and a label written withGITHUB_TOKENfires no event to re-run it.review:oversizedor movesreview:trivial. Now that the 150-file axis is reachable, a push only newly flags a PR by the line axis, as before; the file axis applies on open / ready / reopen. So no open PR that was reviewed under the normal gates gets flipped (G1/G2 skipped, G5 newly red) by this change.test_oversized_is_the_label_not_the_size,test_triage_re_evaluates_the_sentinel_when_it_moves_a_label_the_gate_reads,test_a_push_never_newly_flags_a_pr_as_oversized_by_file_countsentineland always succeeds./pr-review's_is_sentinel(act.py,analyze.py) count it as an ordinary pending check, stalling every stamp preflight; reverted, with a test pinning the default name.test_the_sentinel_job_keeps_its_default_nameAdversarial review follow-up (6b32a93). An independent review of
daddce5found five confirmed bugs and three plausible ones. All are fixed, each with a test that fails on the old code; the rows above reflect the fixes, and the PR comment answering that review has the per-item detail.Blast-radius review (e366396, after rebasing onto master). A second independent pass looked for effects on unrelated PRs. It confirmed five: the Sentinel job rename (7d), a push newly flagging >150-file open PRs as oversized (7c), brief repairs dropping renamed-file and site-URL bullets and two correct summaries (6a), 30-per-page file listings, and a red step on stacked PRs whose base predates the restamp script (3a). All five are fixed with tests that fail on the previous head.
Not fixed, and why:
PULUMI_BOT_TOKENrate limit hit inside claude-code-action's startup. It's a quota problem on a shared token, not a pipeline bug.One-time cleanup (I didn't touch anyone's PR).
scripts/review-v3/cleanup-review-leftovers.py --repo pulumi/docs --extra-pr 2is a dry run that lists what the old loop left behind:Add
--applyto perform the writes.Shared-code note:
triage-classify.py(one additive output key,oversized_by_lines) andapply-update.py(new optional--pr-files) are used outside this lane only through the functions changed above. I didn't touch the content-review, glow-up, or digest code.Related issues (optional)
#21936, #21760, #21885, #21840, #21673, #21066, #21790, #21614, #21892, #21083, #21908, #21761, #21785, #21210, #21487, #21149, #21889, #21801, #21798, #21920, #21787
🤖 Generated with Claude Code
https://claude.ai/code/session_01GtKzf3eR8238vqCLcc1qZt