fix(ci): never let an incomplete review body overwrite a posted verdict (#8292) - #8342
fix(ci): never let an incomplete review body overwrite a posted verdict (#8292)#8342bolichen97 wants to merge 1 commit into
Conversation
…ct (#8292) The Post/update review comment step upserts one marker-keyed comment and unconditionally PATCHed it, so a run that produced no completed verdict replaced a posted [BLOCK-MERGE] verdict with a short 'review incomplete' body. The REST comments API exposes no edit history, so the buried verdict survived only in GraphQL userContentEdits. The step now captures the existing comment's body in the same query that finds its id, and refuses exactly one transition: a marker-absent (incomplete) body never overwrites a marker-present verdict. It keeps the existing verdict and prepends one dated stale-verdict notice (with the /ai-review override escape hatch), replacing any notice a previous incomplete run left. The notice cleanup is line-anchored and bounded to the head window where the step's own notice sits, so model-authored verdict prose that quotes the notice markers can never start a delete into the verdict. Override, blocked, and clear kinds still replace the comment as before, and the fail-closed 'Gate on findings' step is untouched. Comment scratch files now live under $RUNNER_TEMP (always set on GitHub runners, /tmp fallback) so the step's bash body is hermetically testable. Four contract cases are locked in by pytest running the real step script with a stubbed gh whose finder branch executes the step's real --jq filter over an array fixture, so filter drift (dropped @JSON, lost author guard) fails the tests instead of hiding. Closes #8292
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Root-cause fix at the exact upsert seam that lost the verdict, with the one risky part (sed surgery on model prose) bounded and mutation-tested. [DESIGN-REVIEWED] 7d769e5 |
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: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All evidence gathered. Composing the review. First-Principles-Verdict: CONCERNS The guard is right, but the stale-notice merge machinery is oversized — claude-review.yml already solves the identical problem by skipping the PATCH — and a third sibling is undeclared. What this change shipsIntent: stop a failed review run's "incomplete" body from erasing a posted blocking verdict — a FIX (reported defect: PR #8184 comment revisions).
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 7d769e5 |
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: |
|
🤖 Kiro Crew Auto-Pipeline [operator: bolichen97#bb3ad1ca]: Per-finding response to the First Principles CONCERNS verdict on 1. Undeclared unfixed sibling ( 2. "The delete option was never weighed" / Subtraction: replace the notice merge with claude-review.yml's skip-the-PATCH — DECLINED, with the trade-off now stated in the PR body. The skip shape does preserve the verdict, but it hides the other fact a reader needs on exactly this failure: that a newer run for the current head produced no verdict. The stale comment would keep saying "updated in place on each push" under an old sha with nothing marking it stale — readable as current. Issue #8292 names the dated notice (or a separate comment) as the preferred form for precisely this reason, and this PR implements the issue: the notice dates the failure, names the failed head sha, points at the job logs, and carries the 3. CRLF normalization undeclared — ADOPTED. The PR body's change list now declares it: GitHub stores comment bodies with CRLF, and the guard's grep and line-anchored cleanup must see the bytes the step wrote. No push was made for this response; the verdict head is unchanged. |
Problem / Motivation
The
Post/update review commentstep in.github/workflows/codex-review.ymlpublishes the GPT review verdict by PATCHing a single comment in place, keyed by the hidden<!-- codex-ai-review -->marker. The step defaultskind="incomplete"and unconditionally PATCHes, so a run that fails to producecodex-review-output.mdreplaces a posted[BLOCK-MERGE]verdict with a short "review incomplete" body. The REST comments API exposes no edit history, so the buried verdict survives only in GraphQLuserContentEdits— invisible everywhere a normal reader or tool looks. Verified by the reporter on PR #8184 comment 5528347174: 3 of its 10 revisions were incomplete bodies, each replacing a marker-present blocking verdict, including a security finding.Why it matters
A blocking security finding silently vanishing from the one comment reviewers read defeats the purpose of the review lane. Merge safety was never at risk (the
Gate on findingsstep fails closed on the output file, not the comment), but a human scanning the PR sees "review incomplete" where a red verdict used to be, and nothing says a verdict ever existed.What changed (motivation → approach → change)
Symptom: incomplete bodies overwrite completed verdicts. Root cause: the upsert PATCHes unconditionally, never looking at what it is replacing. Change, scoped to the
Post/update review commentstep only:{id, body} | @json— one compact line per match, sohead -n1still selects a whole record; author guard unchanged). Captured before any PATCH decision, so there is no find/fetch race. The fetched body is CRLF-normalized (tr -d '\r') before matching, because GitHub stores comment bodies with CRLF line endings and the guard's grep and line-anchored cleanup must see the bytes the step originally wrote.incompletekind AND the existing body contains[GPT-REVIEWED](tested withgrep -Fqagainst a file, never in a shell string), the step does NOT replace the verdict. It re-posts the existing verdict with one dated stale-verdict notice prepended (with the/ai-review overrideescape hatch), replacing any notice a previous incomplete run left so notices never stack. Invariant: a verdict that exists never becomes invisible.override/blocked/clearkinds still replace the comment exactly as before. TheGate on findingsstep is byte-identical: fail-closed on incomplete stays, and a preserved stale verdict is SHA-scoped so no consumer reads it as fresh (sha_matches()rejects the old SHA)./tmpto"${RUNNER_TEMP:-/tmp}"(always set on GitHub runners; behavior-identical there) so the step's bash body is hermetically testable.docs/ci/ci-and-reviews.mddocuments the new upsert semantics, scoped to the same-repo GPT lane.Known remaining gaps (out of scope per the issue): two fork lanes carry the same defect class —
fork-gpt-review.yml'sPost/update summary commentandfork-opus-review.yml's comment upsert both PATCH an incomplete body over a posted verdict unconditionally. Both are tracked in follow-up #8344. (claude-review.ymlis already safe: it posts nothing on incomplete. That skip shape was considered and not chosen here because the issue names the dated notice as the preferred form — see the First Principles disposition on this PR.)Tests
New
TestGptVerdictVisibilityclass intest/test_ai_review_workflows.pyruns the real step bash (extracted via the file's existing_step_scripthelper) with a stubbedghwhose finder branch executes the step's real--jqfilter through realjqover an array fixture — so filter drift (dropped@json, lost author guard) fails the tests instead of hiding:[BLOCK-MERGE]survivesGate on findingsuntouchedMutation-verified: disabling the guard condition, dropping the notice-dedup, reverting to the unanchored sed, and dropping the author guard each make the suite fail. Full backend suite: 84,514 passed; fail-set byte-identical to a clean-main control on the same host (all host-env classes).
Manual verification
Three-case transcript from running the step's bash body with stubbed
gh(sha1234…5678= new head whose run failed;aaaa…aaaa= old completed verdict):CASE (a): incomplete run over existing BLOCKING verdict → verdict preserved + dated notice
gh-stub: PATCH repos/example/repo/issues/comments/123
Preserved GPT 5.6 verdict in comment #123; prepended incomplete-run notice for 1234567890abcdef1234567890abcdef12345678
CASE (c): incomplete run, no existing comment → created (as before)
gh-stub: CREATE comment
Created new GPT 5.6 comment
Related Issues
Closes #8292
Pattern harvest
Rule candidate: review-prompt — Pattern: "in-place upsert of a status artifact must compare old and new state class before replacing; a lower-information state never overwrites a higher-information one". Second and third instances of the class:
fork-gpt-review.ymlandfork-opus-review.ymlcarry the same unconditional PATCH (follow-up #8344);claude-review.ymlalready avoids it by posting nothing on incomplete.Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Why no screenshot: CI workflow + backend test + doc change only; no UI surface is touched.