|
1 | 1 | name: Claude Code Review |
2 | 2 |
|
| 3 | +# Dispatches PR review requests to the Henry review agent (nsheaps/.ai-agent-henry). |
| 4 | +# Henry is the source of truth for review execution, prompts, and check status reporting. |
| 5 | +# This workflow handles trigger detection, label management, and dispatch only. |
| 6 | + |
3 | 7 | on: |
4 | 8 | pull_request: |
5 | 9 | types: [opened, synchronize, ready_for_review, labeled] |
6 | | - # Optional: Only run on specific file changes |
7 | | - # paths: |
8 | | - # - "src/**/*.ts" |
9 | | - # - "src/**/*.tsx" |
10 | | - # - "src/**/*.js" |
11 | | - # - "src/**/*.jsx" |
12 | | - # Don't run this when this file is modified in the PR since it will fail (for security reasons) |
13 | | - # Sometimes claude will reject to review the PR if it sees this file changed (programatically) but it doesn't |
14 | | - # seem consistent |
15 | | - # paths-ignore: |
16 | | - # - ".github/workflows/claude-code-review.yaml" |
17 | 10 |
|
18 | 11 | concurrency: |
19 | | - group: ${{ github.event_name == 'pull_request' && format('claude-review-{0}', github.event.pull_request.number) || format('claude-review-{0}-{1}', github.ref, github.sha) }} |
20 | | - # always let a previous review finish (so that time isn't wasted), but queued builds will still |
21 | | - # be cancelled if a newer build is pushed |
22 | | - # NOTE THIS IS NOT GUARANTEED AT THE JOB LEVEL |
| 12 | + group: claude-review-${{ github.event.pull_request.number }} |
| 13 | + # Let a previous dispatch finish before starting a new one. |
| 14 | + # Queued dispatches will still be cancelled if a newer push arrives. |
23 | 15 | cancel-in-progress: false |
24 | 16 |
|
25 | 17 | jobs: |
26 | | - claude-review: |
| 18 | + dispatch-review: |
27 | 19 | # Triggers: |
28 | 20 | # - Automatically on all open (non-draft) PRs |
29 | | - # - On draft PRs only when 'request-review' label is added (label is removed after review starts) |
30 | | - # |
31 | | - # Logic: (not a draft) OR (labeled event with request-review) |
| 21 | + # - On draft PRs only when 'request-review' label is added (label is removed after dispatch) |
32 | 22 | if: | |
33 | 23 | github.event.pull_request.draft != true || |
34 | | - ( github.event.action == 'labeled' && github.event.label.name == 'request-review') |
| 24 | + (github.event.action == 'labeled' && github.event.label.name == 'request-review') |
35 | 25 |
|
36 | 26 | runs-on: ubuntu-latest |
37 | 27 | permissions: |
38 | | - contents: write |
| 28 | + contents: read |
39 | 29 | pull-requests: write |
40 | | - issues: write |
41 | | - id-token: write |
42 | | - actions: read |
43 | 30 |
|
44 | 31 | steps: |
45 | | - - name: Checkout and authenticate as GitHub App |
| 32 | + - name: Generate review bot token |
46 | 33 | id: auth |
47 | | - uses: nsheaps/github-actions/.github/actions/checkout-as-app@603052841cd49b9fdc99bc32979ff9c45c9b669e # checkout-as-app@main |
| 34 | + uses: actions/create-github-app-token@v2 |
48 | 35 | with: |
49 | 36 | app-id: ${{ secrets.REVIEW_GITHUB_APP_ID }} |
50 | 37 | private-key: ${{ secrets.REVIEW_GITHUB_APP_PRIVATE_KEY }} |
51 | | - fetch-depth: '50' |
| 38 | + owner: nsheaps |
52 | 39 |
|
53 | 40 | - name: Remove request-review label |
54 | 41 | if: github.event.action == 'labeled' && github.event.label.name == 'request-review' |
55 | 42 | env: |
56 | 43 | GH_TOKEN: ${{ steps.auth.outputs.token }} |
57 | 44 | run: | |
58 | | - gh pr edit ${{ github.event.pull_request.number }} --remove-label "request-review" |
59 | | -
|
60 | | - - name: Get additional job context |
61 | | - uses: qoomon/actions--context@v5 |
62 | | - if: ${{ !cancelled() }} |
63 | | - id: additional-context |
64 | | - continue-on-error: true |
| 45 | + gh pr edit ${{ github.event.pull_request.number }} \ |
| 46 | + --remove-label "request-review" \ |
| 47 | + --repo "${{ github.repository }}" |
65 | 48 |
|
66 | | - - name: Ensure gh has the review extension to aid in getting review comments (not PR comments) |
67 | | - # Used by claude to fetch PR review comments rather than direct API queries. |
68 | | - # See https://github.com/cli/cli/discussions/3993 |
69 | | - # See https://github.com/cli/cli/issues/8409 |
70 | | - shell: bash |
71 | | - run: | |
72 | | - gh extension install agynio/gh-pr-review || echo "gh-pr-review extension already installed" |
73 | | -
|
74 | | - - name: Interpolate review prompt |
75 | | - id: prompt |
76 | | - uses: ./.github/actions/interpolate-prompt |
77 | | - env: |
78 | | - REPO: ${{ github.repository }} |
79 | | - PR_NUMBER: ${{ github.event.pull_request.number }} |
80 | | - JOB_CONTEXT: ${{ toJson(steps.additional-context.outputs) }} |
| 49 | + - name: Dispatch review to Henry |
| 50 | + uses: peter-evans/repository-dispatch@v4 |
81 | 51 | with: |
82 | | - template-file: .github/prompts/claude-code-review.md |
83 | | - |
84 | | - - name: Run Claude Code Review |
85 | | - id: claude-review |
86 | | - uses: anthropics/claude-code-action@v1 |
87 | | - env: |
88 | | - GH_TOKEN: '${{ steps.auth.outputs.token }}' |
89 | | - GITHUB_TOKEN: '${{ steps.auth.outputs.token }}' |
90 | | - # These are in addition to any parsed settings.json files |
91 | | - # Prefer this over claude_args, almost everything can be specified via settings json or env vars |
92 | | - # While Bash is allowed, the list here is passed directly to the agent, so explicitly defining them |
93 | | - # causes the agent to see those Bash commands as more relevant and will use them more often. |
94 | | - # Ref: https://code.claude.com/docs/en/settings |
95 | | - INPUT_SETTINGS_JSON: | |
| 52 | + token: ${{ steps.auth.outputs.token }} |
| 53 | + repository: nsheaps/.ai-agent-henry |
| 54 | + event-type: pull_request/review |
| 55 | + client-payload: | |
96 | 56 | { |
97 | | - "permissions": { |
98 | | - "additionalDirectories": ["/tmp/"], |
99 | | - "allow":[ |
100 | | - "Task", |
101 | | - "Glob", |
102 | | - "Grep", |
103 | | - "LS", |
104 | | - "ExitPlanMode", |
105 | | - "Read", |
106 | | - "Edit", |
107 | | - "Write", |
108 | | - "Replace", |
109 | | - "MultiEdit", |
110 | | - "NotebookRead", |
111 | | - "NotebookEdit", |
112 | | - "TodoWrite", |
113 | | - "WebSearch", |
114 | | - "WebFetch", |
115 | | - "Bash", |
116 | | - "BashOutput", |
117 | | - "KillBash", |
118 | | - "ListMcpResourcesTool", |
119 | | - "ReadMcpResourceTool", |
120 | | - "SlashCommand", |
121 | | - "Skill", |
122 | | -
|
123 | | - "mcp__github__github_support_docs_search", |
124 | | - "mcp__github__get_pull_request", |
125 | | - "mcp__github__get_pull_request_diff", |
126 | | - "mcp__github__get_pull_request_files", |
127 | | - "mcp__github__get_pull_request_review_comments", |
128 | | - "mcp__github__get_pull_request_reviews", |
129 | | - "mcp__github__get_pull_request_status", |
130 | | - "mcp__github__create_pending_pull_request_review", |
131 | | - "mcp__github__add_comment_to_pending_review", |
132 | | - "mcp__github__submit_pending_pull_request_review", |
133 | | -
|
134 | | - "Bash(git log:*)", |
135 | | - "Bash(git diff:*)", |
136 | | - "Bash(git status:*)", |
137 | | - "Bash(git rev-parse:*)", |
138 | | - "Bash(git fetch:*)", |
139 | | - "Bash(git show:*)", |
140 | | - "Bash(git merge:*)", |
141 | | - "Bash(git remote get-url:*)", |
142 | | - "Bash(git rm:*)", |
143 | | - "Bash(gh issue view:*)", |
144 | | - "Bash(gh search:*)", |
145 | | - "Bash(gh issue list:*)", |
146 | | - "Bash(gh pr comment:*)", |
147 | | - "Bash(gh pr diff:*)", |
148 | | - "Bash(gh pr view:*)", |
149 | | - "Bash(gh pr-review:*)", |
150 | | - "Bash(gh pr-review review view:*)", |
151 | | - "Bash(gh pr list:*)", |
152 | | - "Bash(gh run list:*)", |
153 | | - "Bash(gh run view:*)", |
154 | | - "Bash(gh extension list:*)" |
155 | | - ], |
156 | | - "_denyComment": "It will wait for it's own check run to complete, not to mention it should review the code.", |
157 | | - "deny": [ |
158 | | - "mcp__github_ci__get_ci_status", |
159 | | - "mcp__github_ci__get_workflow_run_details", |
160 | | - "mcp__github_ci__download_job_log", |
161 | | - "Bash(gh pr checks:*)", |
162 | | - "Bash(git push:*)" |
163 | | - ] |
| 57 | + "referer": "${{ github.repository }}/.github/workflows/claude-code-review.yaml", |
| 58 | + "source": { |
| 59 | + "repo": "${{ github.repository }}", |
| 60 | + "pr_number": ${{ github.event.pull_request.number }}, |
| 61 | + "head_sha": "${{ github.event.pull_request.head.sha }}", |
| 62 | + "head_ref": "${{ github.event.pull_request.head.ref }}", |
| 63 | + "base_ref": "${{ github.event.pull_request.base.ref }}" |
164 | 64 | }, |
165 | | - "model": "opus", |
166 | | - "env": { |
167 | | - "GH_TOKEN": "${{ steps.auth.outputs.token }}", |
168 | | - "GITHUB_TOKEN": "${{ steps.auth.outputs.token }}", |
169 | | - "_GH_PAGER_COMMENT": "disable gh from being able to treat the terminal as interactive", |
170 | | - "GH_PAGER": "cat", |
171 | | - "GH_PROMPT_DISABLED": "true", |
172 | | - "_GH_BROWSER_COMMENT": "what app to open URLs with, since claude is not interactive, we use curl to pipe it to stdout", |
173 | | - "GH_BROWSER": "curl", |
174 | | - "CLAUDE_CODE_REMOTE": "true" |
| 65 | + "trigger": { |
| 66 | + "event": "pull_request", |
| 67 | + "action": "${{ github.event.action }}", |
| 68 | + "label": ${{ toJson(github.event.label.name || '') }} |
175 | 69 | } |
176 | 70 | } |
177 | | - with: |
178 | | - anthropic_api_key: ${{ secrets.REVIEW_ANTHROPIC_API_KEY || secrets.ANTHROPIC_API_KEY || '' }} |
179 | | - claude_code_oauth_token: ${{ (secrets.REVIEW_ANTHROPIC_API_KEY || secrets.ANTHROPIC_API_KEY) && '' || secrets.CLAUDE_CODE_OAUTH_TOKEN }} |
180 | | - # WARNING: PRINTS FULL JSON OUTPUT WHICH MAY INCLUDE SECRETS. |
181 | | - # ONLY USE IN PRIVATE ENVIRONMENTS. |
182 | | - show_full_output: true |
183 | | - # bug: https://github.com/anthropics/claude-code-action/issues/759 |
184 | | - # won't find the past comments since it's looking for github-actions[bot], not the auth it uses to post the comment. |
185 | | - bot_id: ${{ steps.auth.outputs.user-id }} |
186 | | - bot_name: ${{ steps.auth.outputs.user-name }} |
187 | | - allowed_bots: 'github-actions[bot],renovate[bot],claude[bot],automation-nsheaps[bot]' |
188 | | - github_token: ${{ steps.auth.outputs.token }} |
189 | | - settings: ${{ env.INPUT_SETTINGS_JSON }} |
190 | | - additional_permissions: | |
191 | | - actions: read |
192 | | - # Prompt is loaded from external template file via interpolate-prompt action |
193 | | - prompt: ${{ steps.prompt.outputs.prompt }} |
194 | | - # Prefer using settings.json, but some are needed to be set here for the action |
195 | | - # to automatically enable the MCP servers. Placing anything with `mcp__github__` prefix will enable the github mcp server. |
196 | | - # See https://github.com/anthropics/claude-code-action/issues/723#issuecomment-3716307305 |
197 | | - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md |
198 | | - # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options |
199 | | - # Note: We intentionally do NOT include 'github_comment' (used for Claude's persistent "working on it" comment) |
200 | | - # or 'github_inline_comment' (used for inline comments outside of reviews) in the allowed tools / claude_args, |
201 | | - # to prevent Claude from posting persistent status comments or inline comments outside of proper reviews. |
202 | | - claude_args: >- |
203 | | - --allowedTools |
204 | | - mcp__github__github_support_docs_search, |
205 | | - mcp__github__get_pull_request, |
206 | | - mcp__github__get_pull_request_diff, |
207 | | - mcp__github__get_pull_request_files, |
208 | | - mcp__github__get_pull_request_review_comments, |
209 | | - mcp__github__get_pull_request_reviews, |
210 | | - mcp__github__get_pull_request_status, |
211 | | - mcp__github__create_pending_pull_request_review, |
212 | | - mcp__github__add_comment_to_pending_review, |
213 | | - mcp__github__submit_pending_pull_request_review |
0 commit comments