diff --git a/.github/workflows/claude-code-review-on-demand.yml b/.github/workflows/claude-code-review-on-demand.yml index a088fa36..74fa95e8 100644 --- a/.github/workflows/claude-code-review-on-demand.yml +++ b/.github/workflows/claude-code-review-on-demand.yml @@ -13,6 +13,21 @@ name: Claude Code Review (on demand) # - no `contents: write`, so it cannot push commits; # - fires ONLY on a maintainer's comment (author_association gate), so an # outside contributor on a fork can never trigger it. +# +# The checkout MUST be the PR's own ref, never the default one. Neither +# `issue_comment` nor `pull_request_review_comment` is a PR event, so +# actions/checkout with no `ref:` lands on the DEFAULT BRANCH — the review then +# reads `main` while claiming to review the PR. Files a PR adds or renames are +# simply absent, and nothing reports red (the job still succeeds). So the PR +# number is resolved FIRST and the checkout is pinned to it. `refs/pull/N/head` +# (not `/merge`): it is the tree the author actually pushed, it matches what +# `gh pr diff` and the inline-comment line anchors refer to, and unlike `/merge` +# it still exists when the PR has conflicts — a review is exactly what you want +# on a conflicted PR. Reading a PR ref needs no more than the `contents: read` +# this job already has — the write scopes below exist for posting comments, not +# for the checkout. So do NOT "fix" a checkout problem by reaching for +# `pull_request_target` or by widening permissions: neither was ever the +# blocker, and both trade a read problem for a write capability. on: issue_comment: types: [created] @@ -38,13 +53,11 @@ jobs: id-token: write steps: - # Note: claude-code-action adds its own 👀 reaction to the triggering - # comment, so there's no explicit reaction step here. - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - + # Resolved BEFORE checkout: the checkout ref depends on it. The PR number + # lives in a different payload field per event — `issue.number` on + # issue_comment, `pull_request.number` on pull_request_review_comment — + # and each is absent on the other event, so branch on `event_name` rather + # than relying on a `||` fallback over a null. - name: Prepare review context id: prep run: | @@ -60,8 +73,23 @@ jobs: } >> "$GITHUB_OUTPUT" fi + # `fetch-depth: 1` is enough: the prompt forbids running the project's + # build/lint/test, and every allowed tool reads the diff through `gh` + # (the API), not through local history. + - name: Checkout PR head + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: refs/pull/${{ steps.prep.outputs.pr }}/head + fetch-depth: 1 + # Nothing here writes to git, and this checkout is contributor-authored + # PR content — leaving the token in `.git/config` would put it a step + # away from anything that later runs in this tree. + persist-credentials: false + + # Note: claude-code-action adds its own 👀 reaction to the triggering + # comment, so there's no explicit reaction step here. - name: Run Claude Code Review - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@d40ddef4c030e508327d6e35a9c45f3368482c50 # v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # Single tracking comment (in-progress → results), updated in place.