fix(ci): bind fork AI-review readiness reads to PR+attempt external_id (#9302) - #9318
Conversation
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All facts I need are verified by reading: the trigger wiring ( First-Principles-Verdict: PASS Verify on a live override rerun that a fresh check-run POST gets a higher id than the stale row — the one platform premise the collapse rests on, unverifiable from the repo. Not justified as shipped
Both are derived, not speculative: What this change shipsIntent: stop a fork PR's merge gate from accepting an AI-review verdict computed for a different PR or a previous attempt — a FIX (linked issue #9302, replicating the merged #9214 pattern).
Sibling count: grep [FIRST-PRINCIPLES-REVIEWED] 383f674 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS The rerun-race guard is event-scoped, so the stale-verdict window it names stays open to every readiness evaluation the rerun itself didn't trigger. WatchThe Suggestions
[DESIGN-REVIEWED] 383f674 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
43d28c5 to
8f7f29b
Compare
8f7f29b to
f4a7ef4
Compare
|
F1 (
self-added: yes |
|
Running the checker directly against a clean |
f4a7ef4 to
37d50d5
Compare
|
Item 5 (
self-added: no |
37d50d5 to
cc642a2
Compare
#9302) The five Stage-2 fork AI-review lanes (Opus, GPT, Design, UX, First Principles) had their verdict read from the head SHA's check-runs by name only, so a sibling PR sharing the head SHA, or a stale row from a previous rerun attempt, could answer for the wrong PR/attempt (fail-open). Replicate the two-dimensional binding PR #9214 established for the Internal Content Scan lane, generalized to the five sibling lanes: - Each lane stamps external_id=<lane>-pr-$PR-$WR_RUN_ID-$WR_RUN_ATTEMPT at both the open and fallback POST. - pr-readiness.yml derives the expected id from Fast Gate's newest run for the head (bound to this PR by head repository + head branch, so a sibling PR's trigger run can never be selected) and collapses every check-run matching that id to the newest by CHECK-RUN id (distinct per POST, monotonically increasing, independent of what external_id a row carries). This closes a human-override-rerun gap: a rerun of a lane directly (without Fast Gate re-running) leaves the trigger-bound id unchanged, so the stale failed attempt and the fresh rerun share one external_id; the check-run-id collapse still resolves to the fresh row. Trigger-not-run-yet reads pending (holds the merge). - A second race on that same rerun: pr-readiness.yml also triggers on the rerun's own `workflow_run: in_progress` event, which fires the instant the rerun starts and can race its "Open check-run" step -- reading check-runs at that exact moment would still see only the OLD completed verdict. Readiness recognizes when its own evaluation was triggered by that lane's in_progress event and reads pending directly, without querying check-runs. - The two live consumers of the old id (each lane's finalize stranded -run sweep and ai-review-human-override.yml) switch from exact match to a PR-dimension prefix match so they still span attempts. - Fallback POST stamps the id when the PR is known; when unresolved it stamps nothing and is documented as intentionally pending-on-invisible (fail-closed). Per-lane tests in test/test_pr_readiness_evaluate.py (shared _ForkLaneVerdictBinding base class) cover the sibling-PR, stale-attempt, sibling-trigger-run, own-row, human-override-rerun, and rerun-starting race cases; test/test_ai_review_workflows.py pins updated with negative guards on the old exact match; docs/ci/ci-and-reviews.md updated. Co-authored-by: Kiro Crew <noreply@kiro.dev>
cc642a2 to
383f674
Compare
|
F1 (
self-added: yes |
Fixes #9302.
Problem
The five Stage-2 fork AI-review lanes (Opus, GPT, Design, UX, First Principles) read their verdict from the head SHA's check-runs by name only. Two open PRs can share a head SHA, so a sibling PR's check-run could answer for the wrong PR (a race that fails open or closed depending on the sibling's verdict). A rerun on an unchanged head also left the previous attempt's completed row in place, so a PR-scoped-but-attempt-blind read would accept a verdict computed against an older ruleset. The fallback POST, which omits
external_idwhen the PR number cannot be resolved, read as a pass by name.Fix
Replicate the two-dimensional binding that PR #9214 (commit
a4e6944e3) established for theInternal Content Scanlane, applied to the five sibling lanes:external_id=<lane>-pr-$PR-$WR_RUN_ID-$WR_RUN_ATTEMPTat both the open and fallback POST (env gainsWR_RUN_ID/WR_RUN_ATTEMPTfromgithub.event.workflow_run.id/.run_attempt).pr-readiness.ymlfour-fieldcheckrun:specs (<Name>|<Name>|<prefix>-pr-|fast-gate.yml); the reader resolves Fast Gate's newest run for the head, filtered to THIS PR's (head repository, head branch) so a sibling PR's trigger run on the same head SHA can never be selected, buildswant=<prefix>-pr-<PR>-<runid>-<attempt>, and filters check-runs to that exact id. Trigger-not-run-yet reads pending, holding the merge.ai-review-human-override.yml) switch from exact match to a PR-dimension prefix match (startswith("<lane>-pr-$PR-"), trailing hyphen so-pr-9!=-pr-99) so they still span attempts.Audit findings
Note on the reference
PR #9214 (commit
a4e6944e3) merged intomainwhile this branch was rebasing and is now an ancestor of HEAD, so theInternal Content Scanlane's own binding (itscheckrun:spec, itsTestForkScanVerdictIsBoundToItsPullRequesttest class, and its(head repository, head branch)trigger-run filter) already exists in the working tree unmodified. This PR generalizes that exact proven pattern -- including the trigger-run repo/branch filter, which #9214 added to close a sibling-PR collision that the original commit43d28c515had not yet accounted for -- to the five sibling review lanes, and adds a shared_ForkLaneVerdictBindingtest base class (subclassed per lane) rather than restating the four per-lane test methods five times.Testing
.github/workflows/*.ymlparse viayaml.safe_load.test_pr_readiness_evaluate.py,test_ai_review_workflows.py, plus the touched-file suitestest_acp_client.py,test_bench_kb_retrieval.py,test_conductor_agent.py,test_hooks_coverage.py,test_knowledge_search_upgrade.py,test_security_posture.py,test_session_pool.py): 1657 passed, 6 failed. The 6 failures are all intest_acp_client.py's tool-results/session-resume tests, unrelated to this PR's diff (workflow YAML + readiness test files) -- confirmed by running the identical 6 tests against a cleanorigin/maincheckout, where they fail identically with the same assertion.scripts/check_black_formatting.pyexit 0;isort --check-onlyandflake8clean on the touched test files;scripts/docs_lint.py"All documentation checks passed".ai-review-human-override.ymland all fivefork-*-review.ymlfinalize steps).Pattern harvest
Not generalizable: this replicates an already-landed, already-reviewed binding pattern (PR #9214) onto five sibling lanes rather than introducing a new defect class.
Files changed (10)
.github/workflows/pr-readiness.yml-- four-field fork specs + attempt-binding reader for the five sibling lanes, sharing the (head repository, head branch) trigger-run filterInternal Content Scanalready uses..github/workflows/fork-{opus,gpt,design,ux,first-principles}-review.yml-- two-dimensional open/fallback stamp; prefix-match stranded-run sweep..github/workflows/ai-review-human-override.yml-- prefix-match on the second consumer.test/test_pr_readiness_evaluate.py-- shared_ForkLaneVerdictBindingbase class + five per-lane subclasses (sibling / stale-attempt / sibling-trigger-run / own-row).test/test_ai_review_workflows.py-- pins updated with negative guards on the old exact match.docs/ci/ci-and-reviews.md-- fork readiness read documented as external_id-bound (PR + run + attempt).