fix(ci): review the PR head, not the default branch - #121
Conversation
The on-demand Claude review checked out with no `ref:`. Neither `issue_comment` nor `pull_request_review_comment` is a PR event, so actions/checkout defaulted to the default branch and every review read `main` instead of the PR. Files a PR added or renamed were invisible, and the job still succeeded, so nothing reported red. Resolve the PR number before checkout (it already branches on `event_name`, since the number lives in `issue.number` for one event and `pull_request.number` for the other) and pin the checkout to `refs/pull/<n>/head`. `/head` over `/merge`: it is the tree the author pushed, it matches what `gh pr diff` and the inline-comment line anchors refer to, and it still exists when the PR has conflicts. Security properties are unchanged: no `contents: write`, same maintainer-only `author_association` gate, same `--allowedTools`, still `issue_comment`/`pull_request_review_comment` rather than `pull_request_target`. The header comment now records what the checkout is for, which is why this went unnoticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jwc9FFroR3mTZ4hLiSkkX3
There was a problem hiding this comment.
Pull request overview
Updates the on-demand Claude review workflow to ensure it checks out and reviews the pull request head ref (instead of unintentionally reviewing the default branch) when triggered by comment-based events.
Changes:
- Resolve the PR number in a prep step based on the triggering event payload.
- Pin
actions/checkouttorefs/pull/<pr>/headto ensure the reviewed working tree matches the PR being reviewed. - Expand the workflow header comment to document the checkout rationale and constraints.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@claude /review |
|
Claude finished @thecodedrift's task in 3m 54s —— View job I'll analyze this and get back to you. |
|
Claude finished @thecodedrift's task in 3m 15s —— View job Review: fix(ci): review the PR head, not the default branch
Overall assessmentThis is a correct, well-reasoned fix for the actual bug (checkout silently falling back to
One finding posted inline (line 80): checking out the PR head is now checking out potentially attacker-influenced content (any PR a maintainer comments Everything else — the header comment, the |
Three findings from Copilot on #121, all backed by conventions this repo already states elsewhere. `persist-credentials: false` on the checkout. `release.yml` and `vale-binaries.yml` both set it; this workflow did not, and it matters more here than in either of those: the tree being checked out is now contributor-authored PR content, so leaving the token in `.git/config` puts it one step away from anything that later runs in that tree. Pin `anthropics/claude-code-action` to a commit SHA. `release.yml`'s header states the convention — "Action refs are pinned to commit SHAs (supply-chain hardening); the trailing comment records the human-readable tag" — and this file was the exception, while holding a long-lived secret and `id-token: write`. `actions/checkout` is pinned to the same SHA the other workflows use. Correct the header's claim that `contents: read` is all this needs. The job also holds `pull-requests: write`, `issues: write`, and `id-token: write`; those exist for posting comments, not for the checkout. The point being made was that a checkout problem is not a permissions problem — now said that way, rather than in a form that reads as a description of the whole job.
What you saw
Nothing. Every on-demand
@claude /reviewfinished green and posted a plausible-looking review.What was actually happening
.github/workflows/claude-code-review-on-demand.ymlchecked out with noref::Neither
issue_commentnorpull_request_review_commentis a PR event, soactions/checkoutfalls back to the default branch. Confirmed in the log for job96140869912:git checkout --progress --force -B main refs/remotes/origin/main, withhead_branch: mainandhead_sha: 5596e390. The reviewer was readingmainwhile being told it was reviewing a PR.The
git hash-objectstack traces in #118 are cosmetic — upstream catches the failure, warns, setssha: "unknown", and the job concludes success. That is why nothing ever went red.The real damage is silent degradation. Across the four PRs reviewed at 16:18 on 2026-08-19 — every one checked out
main:.github/scripts/openspec-visibility.cjsand its test — the entire new scriptpackages/cli/test/ast-grep-vendor-contract.test.ts— the ~500-line contract that is the PR#115's posted review claimed it "spot-checked every topic name against real files in
packages/cli/src/agent/" — a directory that does not exist onmain. A review asserting verification it could not have performed is worse than no review.The fix
Resolve the PR number before checkout and pin the checkout to it:
The
Prepare review contextstep already branches ongithub.event_namebecause the PR number lives inissue.numberonissue_commentandpull_request.numberonpull_request_review_comment, each absent on the other event. Reusing it makes the reviewed tree and thePR NUMBER:in the prompt a single source of truth, rather than duplicating a||fallback over a null./head, not/merge./headis the tree the author pushed; it matches whatgh pr diffreturns and what the inline-comment line anchors are computed against, so a comment lands on the line the reviewer read./mergewould also fail to exist on a conflicted PR — and a conflicted PR is exactly when you still want a review.fetch-depth: 1is kept: the prompt forbids running the project's build/lint/test, and every allowed tool reads the diff throughgh(the API), not through local history.Security properties are unchanged — no
contents: write, same maintainer-onlyauthor_associationgate, same restricted--allowedTools, stillissue_comment/pull_request_review_commentand notpull_request_target, no permissions added.The header comment now records which tree gets reviewed and why, alongside the security model it already documented. This bug was invisible for as long as it was because nothing in the file said what the checkout was for.
Verification
Verified: the file parses as YAML and passes Prettier; the run log for the successful
issue_commentrun32275202719shows the rendered script asecho "pr=117"/echo "pr=", i.e.github.event.pull_request.numberis empty onissue_commentandissue.numberis populated;pull_request_review_commentruns carry the PR's ownhead_branch(e.g. run32291103997→openspec/nightly-1-workflow-split), and GitHub's webhook payload reference confirms that event carries a top-levelpull_requestand noissue.Reasoned, not executed: the workflow itself cannot be run from this branch, so the corrected checkout is confirmed by ref semantics and the payload evidence above rather than by a green run. The first real invocation after merge is the actual proof.
No changeset — this changes no published package. Labeled
skip-changeset.Fixes #118