Skip to content
Merged
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
53 changes: 45 additions & 8 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,18 @@ jobs:
id-token: write
actions: read
steps:
# Actions are pinned to full commit SHAs rather than mutable major tags:
# this job holds ANTHROPIC_API_KEY and grants the agent Bash, so a
# force-moved tag would be an unreviewed code change inside a
# secret-holding job. The trailing `# vX.Y.Z` comment is the form
# Dependabot reads, so pinning costs us no upgrade automation. (#1882)
- name: Get PR details
if: |
(github.event_name == 'issue_comment' && github.event.issue.pull_request) ||
github.event_name == 'pull_request_review_comment' ||
github.event_name == 'pull_request_review'
id: pr
uses: actions/github-script@v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
let prNumber;
Expand All @@ -57,26 +62,58 @@ jobs:
pull_number: prNumber
});

// A fork PR's head lives in a different repository — or in none at
// all, if the fork was deleted after the PR was opened (`head.repo`
// is then null). Neither is ours to check out, so both count as a
// fork here and the steps below decline rather than guess.
const headRepo = pr.data.head.repo?.full_name ?? null;
const isFork = headRepo !== `${context.repo.owner}/${context.repo.repo}`;

core.setOutput('sha', pr.data.head.sha);
core.setOutput('repo', pr.data.head.repo.full_name);
core.setOutput('is_fork', String(isFork));

# A fork PR's head is untrusted code, and checking it out would put it in
# reach of a tool-enabled agent run holding ANTHROPIC_API_KEY. Reviewing
# the base tree instead would only trade that for a confident review of
# the wrong tree, so decline visibly and leave the reason in the run.
- name: Decline fork PR
if: steps.pr.outcome == 'success' && steps.pr.outputs.is_fork == 'true'
run: |
{
echo "### Claude Code declined this pull request"
echo
echo "The head branch lives in a fork, so its code is not checked out and Claude is not run."
echo "Push the branch to this repository and re-trigger if a review is needed."
} >> "$GITHUB_STEP_SUMMARY"

# No `repository:` — a same-repo head is all that reaches this step, and
# checkout defaults to `github.repository`, which no PR can influence.
- name: Checkout PR branch
if: steps.pr.outcome == 'success'
uses: actions/checkout@v6
if: steps.pr.outcome == 'success' && steps.pr.outputs.is_fork == 'false'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.pr.outputs.sha }}
repository: ${{ steps.pr.outputs.repo }}
fetch-depth: 0

# `skipped` means the trigger was an issue or a non-PR comment, so there
# is no head to check out and the base tree is the right one. The lookup
# having *failed* is deliberately not included: this condition carries no
# status-check function, so GitHub applies an implicit `success()` and
# skips the step after a failed prior step anyway. Spelling out `skipped`
# keeps that from having to be re-derived by the next reader.
- name: Checkout repository
if: steps.pr.outcome != 'success'
uses: actions/checkout@v6
if: steps.pr.outcome == 'skipped'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Run Claude Code
# Runs only against a tree this workflow actually checked out: the base
# repo (non-PR trigger) or a same-repo PR head. A fork PR reaches here
# with `is_fork == 'true'` and both disjuncts false, so it is skipped.
if: steps.pr.outcome == 'skipped' || steps.pr.outputs.is_fork == 'false'
id: claude
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@5ef2e550a465a721f4f45e4a7d3c340c873e1dcc # v1.0.190
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

Expand Down
Loading