fix: make interrupted and failed cycles recoverable #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Claude PR Review | |
| # Triggers: | |
| # - pull_request opened/reopened/ready_for_review: fires once when a PR is | |
| # created (or unmarked draft). Intentionally does NOT include synchronize, | |
| # so subsequent commits do not re-trigger and burn Max quota. | |
| # - issue_comment created: lets a trusted commenter (OWNER, MEMBER, or | |
| # COLLABORATOR) say `@claude review` in any PR comment to request a fresh | |
| # pass on demand. The author_association gate prevents drive-by triggers. | |
| on: | |
| pull_request: | |
| types: [opened, reopened, ready_for_review] | |
| issue_comment: | |
| types: [created] | |
| jobs: | |
| claude-review: | |
| if: | | |
| (github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) || | |
| (github.event_name == 'issue_comment' && github.event.issue.pull_request != null && contains(github.event.comment.body, '@claude review') && (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # contents: read means the action cannot push commits or modify files even | |
| # if it tried. Comment writes are explicit. id-token: write is required | |
| # so the action can fetch an OIDC token and exchange it for the GitHub | |
| # App installation token used to post review comments. | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| # If you push twice in quick succession (or comment twice), cancel the | |
| # in-progress run so only the latest review lands. | |
| concurrency: | |
| group: claude-review-${{ github.event.pull_request.number || github.event.issue.number }} | |
| cancel-in-progress: true | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| # For comment-triggered reviews: | |
| # 1. Refuse cross-repo (fork) PRs. issue_comment runs in the base-repo | |
| # context with full secrets; feeding an attacker-controlled diff | |
| # into the agent (which has `gh pr comment` allowed) is a known | |
| # prompt-injection -> env-exfiltration path. Refusing here blocks | |
| # it cleanly, regardless of how the agent consumes the diff. | |
| # 2. For same-repo PRs, switch the working tree to the PR's actual | |
| # base branch (not the repo default). PRs targeting release or | |
| # backport branches would otherwise have Claude inspecting | |
| # unrelated default-branch code. | |
| - name: Prepare working tree for comment-triggered reviews | |
| if: github.event_name == 'issue_comment' | |
| id: prep | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| INFO=$(gh pr view ${{ github.event.issue.number }} --json isCrossRepository,baseRefName) | |
| IS_CROSS=$(echo "$INFO" | jq -r '.isCrossRepository') | |
| BASE=$(echo "$INFO" | jq -r '.baseRefName') | |
| if [ "$IS_CROSS" = "true" ]; then | |
| # Set the skip flag first so the decision is recorded even if the | |
| # explanatory comment fails to post; treat the comment as best-effort. | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| gh pr comment ${{ github.event.issue.number }} -b "Cannot run \`@claude review\` on fork PRs: doing so would expose this workflow's secrets to attacker-controlled diff content. Open a branch in the base repo or have a maintainer cherry-pick the changes." || true | |
| exit 0 | |
| fi | |
| git fetch origin "$BASE" | |
| git checkout "$BASE" | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| - name: Claude Review | |
| if: success() && steps.prep.outputs.skip != 'true' | |
| uses: anthropics/claude-code-action@c3d45e8e941e1b2ad7b278c57482d9c5bf1f35b3 # v1.0.99 | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| trigger_phrase: "@claude review" | |
| prompt: | | |
| REPO: ${{ github.repository }} | |
| PR NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} | |
| Review this pull request. Focus on: | |
| - Correctness, edge cases, regressions | |
| - Test coverage gaps | |
| - Security or data-integrity risks | |
| - Style and naming consistent with the surrounding code | |
| Inspect the PR's changes using `gh pr diff ${{ github.event.pull_request.number || github.event.issue.number }}`. | |
| Do not assume the working tree reflects the PR. For pull_request | |
| events the working tree is the merge ref; for issue_comment events | |
| it is the PR's base branch. Either way, the diff command is | |
| authoritative. | |
| Use `gh pr comment` for the top-level summary. | |
| Use `mcp__github_inline_comment__create_inline_comment` (with `confirmed: true`) | |
| for specific code-line issues. Only post GitHub comments; do not | |
| submit review text as chat messages. | |
| Be concise. If nothing material is found, say so in one line. | |
| claude_args: | | |
| --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" | |
| # Quota cap or transient failure should not turn the PR check red. | |
| continue-on-error: true |