mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #236] [MERGED] ci(cursor-review): post the review from a job that never checks out PR code #242
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#242
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/236
Author: @mattmillerai
Created: 8/27/2026
Status: ✅ Merged
Merged: 8/27/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-9569-split-post-review-job📝 Commits (2)
95d9cf8ci(cursor-review): post the review from a job that never checks out PR code6b08fb7fix(cursor-review): close the post-review gate and harden the isolation test (BE-9569)📊 Changes
10 files changed (+711 additions, -44 deletions)
View changed files
📝
.github/cursor-review/README.md(+15 -0)📝
.github/cursor-review/build-ledger.py(+8 -6)➕
.github/cursor-review/tests/test_workflow_job_isolation.py(+350 -0)📝
.github/workflow-pins/tests/test_check_workflow_pins.py(+5 -2)📝
.github/workflows/cursor-review.yml(+310 -22)📝
.github/workflows/test-cursor-review-scripts.yml(+5 -4)📝
CONTRIBUTING.md(+7 -4)📝
README.md(+1 -1)📝
SECURITY.md(+7 -4)📝
docs/callers/cursor-review.md(+3 -1)📄 Description
ELI-5
The judge model that writes our PR reviews used to run in the same job that held the review bot's private key. That judge reads the PR's diff — attacker-authored text — and Cursor's
--printmode can use write and shell tools, so a diff that talked the judge into rewriting a script on disk would have been rewriting a script that ran moments later with a GitHub App token in the environment. This moves the token mint and the review post into their own job on a fresh runner that never checks out the PR. Nothing about the review a user sees changes.What changed
consolidateandpost-revieware now two jobs, the same shapepr-size.ymlalready uses for itscommentjob andcursor-review-catalog-drift.ymluses for itsissues: writejob.consolidatekeeps thefetch-depth: 0PR checkout and the judge, and dropspull-requests: write— it iscontents: readonly now. Two new steps at the end write a small meta JSON (judge_status,consolidated_count,degraded,triggered_by, and the ledger download's outcome) and upload it alongsideconsolidated.json+judge-findings.jsonas thecursor-review-consolidatedartifact.post-review(new) does noactions/checkoutof the PR repo — onlyComfy-Org/github-workflows@${{ inputs.workflows_ref }}withpersist-credentials: false, behind the sameRequire a pinned workflows_refguard every other checkout site carries. It downloads the payload, mints the App token, renders the ledger note and runspost-review.pyexactly as before.permissions: contents: read+pull-requests: write,timeout-minutes: 10.post-review.pyanchors inline comments against is downloaded fromdiff-size's ownpr-diffartifact, not re-uploaded from the judge's job — so the copy the poster reads is the one the panel reviewed, not one the judge had shell access to.consolidate.Verified:
post-review.pyshells out toghand nothing else — nogit, no dependence on a PR checkout. Every value it needs arrives as a CLI arg or via the artifact.The invariant, stated once: no job in
cursor-review.ymlboth checks out PR code and holds a write-scoped credential..github/cursor-review/tests/test_workflow_job_isolation.py(new, stdlib-only, 9 tests) pins that structurally — it parses the workflow, classifies each job by whether anyactions/checkoutstep resolves to something other than the pinned assets repo, and asserts no such job carries a write-scopedpermissions:grant or referencesBOT_APP_PRIVATE_KEY. Every assertion was confirmed non-vacuous by injecting the matching regression and watching it fire.Carried along, because the split makes them wrong otherwise:
notify-complete's "One consolidated review is on the PR" Slack DM now keys on the post job's result rather thanconsolidate's, and distinguishes the two failures — a green consolidate no longer justifies claiming the review landed..github/workflow-pins/tests/moves 16 → 17 for the new job's assets checkout.check_workflow_pins.pyconfirms it carries its guard (0 exempt).SECURITY.md,CONTRIBUTING.mdandbuild-ledger.py's two docstrings each carried an explicit statement of the pre-split boundary ("It is not absolute for cursor-review's judge…", "a workflow whoseconsolidatejob holdspull-requests: write", "consolidate renders a banner"). All four now describe the split.README.mdcatalog row,.github/cursor-review/README.md,docs/callers/cursor-review.md(the "don't useConsolidate panelas a required check" gotcha now says the same ofPost review, whichneeds:it and skips in all the same cases plus the failure cases).Judgment calls
if: !cancelled() && needs.consolidate.result == 'success'. Both halves are load-bearing.!cancelled()rather thanalways()because the documented caller setscancel-in-progress: trueandalways()runs a job even when the run is cancelled — in the common supersede orderingconsolidatehas already finished and uploaded, so a needs-level!= 'cancelled'test passes and the poster would post against a superseded head SHA alongside the new run's review. Splitting the post into its own job widened that window, since cancellation used to kill the post step mid-job. Same reasoningover-cap-commentalready documents for its own!cancelled(). And== 'success'rather than "ran at all" because this job's entire input is a downloaded artifact: artifact names are run-global, a prompt-injected panel cell can claimcursor-review-consolidatedfirst, v4+ immutability then fails the real upload and redsconsolidate— so a gate that accepted a failed consolidate would post the spoofed payload under the bot's write token.consolidateskips this job rather than producing a second red one. Consequence of the above, and a reversal of this PR's first draft. The failure stays visible asconsolidate's own red status and innotify-complete's DM, which names which half broke; there was never anything worth posting on that path.Require the consolidated payloadis still there as the diagnostic for a transient download failure, and now also asserts each of the three expected files landed —if-no-files-found: errorfires only when ZERO paths match, so a partial bundle uploads clean. A judge that never submitted is not this case: its step iscontinue-on-error, soconsolidatestill succeeds and the degraded panel-union review still posts.ledger-note.txtout of the judge's job,post-reviewdownloads the pristinecursor-review-ledgerartifact itself. The banner's delivery check then covers both questions instead of one, and reports them separately, because they mean different things to the author: if the judge's download failed the panel really did review without prior-round context ("findings may repeat earlier rounds"); if only the poster's download failed — a case that exists only because this job fetches the note itself — the panel and judge had the context and only the header could not be rendered, so saying "findings may repeat" there would be false.triggered_byis validated on the way out of the meta file (^[A-Za-z0-9-]{1,39}(\[bot\])?$,[bot]becauseResolve triggererlegitimately emits it). It is the one review-header fieldpost-review.pyinterpolates raw rather than throughneutralize_mentions, which was safe while the value came fromgithub.actorin-job and stopped being safe once it started arriving out of the prompt-injectable job. A rejected value degrades to no attribution line, not to a wrong one.needs: [consolidate, ledger]rather than alsogate. Nothing in the job readsgate's outputs, and a skippedgatealready skipsconsolidate, which skips this.ledgeris needed forneeds.ledger.outputs.status.post-reviewinstead of a red one) fires only where the old code already produced no review.Residual
grep -nshowsecrets.BOT_APP_PRIVATE_KEYonly inpost-review. It also appears inover-cap-comment, a job that did not exist when the ticket was written. That job checks out nothing at all, so the boundary the criterion protects holds — but if you grep expecting one job, you will find two. The test asserts the accurate form (holders == {over-cap-comment, post-review}, neither with a PR checkout) rather than the literal one.notify-complete's success DM can still overstate what landed, and this is deferred to a follow-up rather than fixed here.post-review.pydeliberately exits 0 on a read-only-token 403 after writing the review only to$GITHUB_STEP_SUMMARY(and does the same on several otherPOST_FAILED_SUMMARY_NOTEpaths), so a greenpost-reviewjob does not strictly mean the review is on the PR. This PR did not introduce the gap — keying the DM onconsolidatebeforehand was wrong in exactly the same way — it only moved which job the DM reads. Closing it means changingpost-review.py's exit contract to emit a posted-versus-degraded value, surfacing it as a job output, and extendingtest_post_review.py; that is a separate concern from the isolation boundary.ci-cursor-review.ymldeliberately pins a merged-mainSHA rather than a local path (AGENTS.md: "do not 'simplify' them to a path"), so the cursor-review run on this PR executes the old, unsplit workflow; no consumer can reach the new shape until this lands and their SHA bump merges. What has been verified instead: the workflow parses,actionlintreports no new finding, everyrun:block passesbash -nandshellcheck, all three new/changed inline step bodies were executed locally against the real text extracted from the YAML (the payload-completeness guard, all 10LEDGER_NOTEbranches, and 10triggered_byinputs including markdown-link and team-mention attempts), and every isolation assertion was fired by injecting its regression. The first real consumer run after the bump is the acceptance test, and the thing to watch is thePost reviewjob: an artifact-plumbing mistake surfaces there as a red job with no review posted, not as a silently wrong review.@676fbb6L1503–1900,--approve-mcps, and aledger-note.txtupload). Two plan items turned out to be no-ops against the current tree and were not done:post-review.pyneeds no change (it never shells out togit), andtests/test_wire_bot_identity.pyasserts on the caller-wiring helper, not on job names or steps, so there was nothing in it to update. The new isolation suite is what covers the job structure instead..github/coderabbit-config's suite was not run: it needsjsonschemafrom its pinnedrequirements.txt, which is not installed in this environment. That directory is untouched by this diff, so this is a not-run, not a known failure.cursor-review.ymlalone.groom.ymlis documented as already credential-split andpr-size.ymlis the pattern this copies, but neither was re-verified here, andpr-area-label.yml,pr-derisk.ymlandlinear-ticket.ymlwere not looked at.Provenance
python3 -m unittest discover -s .github/cursor-review/tests: 314 passed, 0 failed (305 on base, +9 isolation tests);.github/workflow-pins/tests: 308 passed;.github/groom: 365;.github/public-repo-hygiene: 196;.github/refresh-reviewers: 48;.github/agents-md-integrity: 46 — all OK.python3 .github/workflow-pins/check_workflow_pins.py: OK, 11 workflows, 0 exempt;python3 .github/agents-md-integrity/check_agents_md.py --root .: passed (1 pre-existing CODEOWNERS warning);actionlint .github/workflows/cursor-review.yml: 1 finding, byte-identical to the one onorigin/main(job.workflow_sha, in the untouched ledger job);shellcheck+bash -nclean on everyrun:block in the file. Isolation suite proven non-vacuous against 7 injected regressions (permissions block deleted;pull-requests: 'write' # commentrestored onconsolidate;permissions: write-all; a workflow-level grant; the assets checkout switched to${{ github.repository }}; arun:-block forgery of therepository:exemption; the judge invocation removed).consolidatenow skips the poster rather than reddening it; that thenotify-completeDM's posted-versus-degraded gap is deferred to a follow-up; and that the end-to-end consumer-run criterion cannot be exercised before merge.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.