Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions .github/workflows/claude-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,59 @@ on:
default: sonnet
secrets:
CLAUDE_CODE_OAUTH_TOKEN:
description: 'Subscription token from `claude setup-token`. Usage draws on the allowance of whoever generated it.'
required: true
description: 'Subscription token from `claude setup-token`. Usage draws on the allowance of whoever generated it. Not required, so that a run without it - a Dependabot or fork pull request, which GitHub gives no secrets - is skipped with a notice rather than failing.'
required: false

jobs:
gate:
# Decides whether this run can be reviewed at all, before a runner is spent on the review.
# GitHub gives no secrets to a run that Dependabot or a fork pull request started, and the
# review action rejects a bot actor, so either would otherwise end as a red job on every
# such pull request. Those are skipped by design: bot pull requests are reviewed by hand.
# The skip is reported as a notice rather than passed over in silence, so a missing token
# in a repository that should have one is visible.
#
# A fork pull request is refused even when a maintainer asks by comment, by decision: that
# run carries the token while the reviewed text is written by someone with no access, who
# could steer the verdict. A refused request gets a reply so it does not look ignored.
runs-on: [ubuntu-latest]
name: Can this be reviewed
timeout-minutes: 2
outputs:
run: ${{ steps.check.outputs.run }}
steps:
- name: Check for a token, a human actor and a same-repository branch
id: check
env:
HAS_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN != '' }}
ACTOR: ${{ github.actor }}
EVENT: ${{ github.event_name }}
GH_TOKEN: ${{ github.token }}
PR: ${{ inputs.pr-number }}
run: |
# Fetched first and on its own, so a failed lookup stops the job rather than reading
# as "not a fork".
cross=$(gh pr view "$PR" --repo "$GITHUB_REPOSITORY" --json isCrossRepository --jq '.isCrossRepository')
if [ "$cross" = true ]; then
echo "::notice::Review refused: pull request #$PR comes from a fork, and fork pull requests are not reviewed."
if [ "$EVENT" = issue_comment ]; then
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" \
--body 'Claude review: refused. This pull request comes from a fork, and fork pull requests are not reviewed automatically, because the reviewed text is written by someone with no access while the run carries the token.'
fi
echo "run=false" >> "$GITHUB_OUTPUT"
elif [ "$HAS_TOKEN" != true ]; then
echo "::notice::Review skipped: no CLAUDE_CODE_OAUTH_TOKEN reached this run. GitHub withholds secrets from Dependabot and fork pull requests; elsewhere, check the secret is set and passed by the caller."
echo "run=false" >> "$GITHUB_OUTPUT"
elif [[ "$ACTOR" == *'[bot]' ]]; then
echo "::notice::Review skipped: $ACTOR is a bot, and bot pull requests are reviewed by hand."
echo "run=false" >> "$GITHUB_OUTPUT"
else
echo "run=true" >> "$GITHUB_OUTPUT"
fi

review:
needs: gate
if: needs.gate.outputs.run == 'true'
runs-on: [ubuntu-latest]
name: Review
# A backstop against a runaway session only. Coverage is deliberately not capped with
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ inputs are at the top of `.github/workflows/claude-review.yaml`.
and the range it covers starts at the last commit a posted review recorded, so commits the
older one would have reviewed are not skipped. A burst of pushes during a review produces one
delta review of all of them.
- **Bot and fork pull requests are skipped.** GitHub gives no secrets to a run started by
Dependabot or by a fork pull request, and bot pull requests are reviewed by hand, so a gate job
skips those runs with a notice instead of failing. A fork pull request is refused even when a
maintainer asks by comment, because that run carries the token over text written by someone
with no access; the request gets a reply saying so. A notice saying no token arrived, on a
pull request that is neither, means the secret is missing or not passed.
- **The caller needs** `contents: read` and `pull-requests: write`, and must pass the secret
explicitly.

Expand Down
Loading