|
1 | | -# Automatic Claude Code review of pull requests. |
| 1 | +# Automatic Claude Code review of pull requests -- thin caller of the central |
| 2 | +# Morrison-Lab/gha reusable review workflow. |
2 | 3 | # |
3 | | -# Runs the upstream `code-review@claude-code-plugins` plugin so we pick up |
4 | | -# any improvements to the canonical review skill, then layers an |
5 | | -# R-package-specific addendum on top. |
| 4 | +# Migrated from a bespoke anthropics/claude-code-action@v1 workflow (see #178) |
| 5 | +# so rpt inherits the upstream hardening automatically as @v2 slides: |
| 6 | +# stub-review retry, the is_error/verdict guard, prior-review-context dedup, |
| 7 | +# older-comment collapse, the cost comment, and gha#400's duplicate-review |
| 8 | +# fix. The reviewer stash/restore dance is handled inside the reusable |
| 9 | +# workflow too: all requested reviewers (human and AI, e.g. Copilot) are |
| 10 | +# cleared while Claude reviews and re-requested when it finishes, which |
| 11 | +# generalizes the d-morrison-only toggle this file used to carry. |
6 | 12 | # |
7 | | -# Each run posts a fresh review comment; prior reviews are left in place |
8 | | -# so the PR keeps a visible history rather than a rolling sticky. |
| 13 | +# The reusable workflow runs in AGENT MODE (track-progress: false), so reviews |
| 14 | +# are summary-only: the upstream inline-comment tool is not initialized in |
| 15 | +# agent mode (anthropics/claude-code-action#635), and tag mode is not safe |
| 16 | +# until anthropics/claude-code-action#1415 (a read_only input) ships. Set |
| 17 | +# track-progress: true here once one of those lands to restore inline comments. |
9 | 18 | # |
10 | | -# Skips drafts, Dependabot bumps, and fork PRs (fork PRs can't read repo |
11 | | -# secrets). Project guidance is in CLAUDE.md. Requires the |
12 | | -# CLAUDE_CODE_OAUTH_TOKEN repository secret. |
13 | | - |
| 19 | +# Keep this file named claude-code-review.yml and keep the workflow_dispatch |
| 20 | +# pr_number input: claude.yml re-dispatches a review with |
| 21 | +# `gh workflow run claude-code-review.yml -f pr_number=<n>` after an @claude |
| 22 | +# run pushes commits. Requires the CLAUDE_CODE_OAUTH_TOKEN repository secret. |
| 23 | +# See Morrison-Lab/gha examples/claude-code-review.yml for the upstream stub. |
14 | 24 | name: Claude Code Review |
15 | 25 |
|
16 | 26 | on: |
17 | 27 | pull_request: |
18 | 28 | types: [opened, synchronize, ready_for_review, reopened] |
19 | | - # Allow manual re-review from the Actions UI (e.g. after an @claude run |
20 | | - # pushes commits). GITHUB_TOKEN pushes don't fire `synchronize`, so a |
21 | | - # manual dispatch path is useful. |
| 29 | + # Lets claude.yml re-dispatch a fresh review after an @claude run pushes |
| 30 | + # commits (GITHUB_TOKEN pushes do not fire `synchronize`), and allows a |
| 31 | + # manual re-review from the Actions UI. |
22 | 32 | workflow_dispatch: |
23 | 33 | inputs: |
24 | 34 | pr_number: |
25 | 35 | description: 'Pull request number to review' |
26 | 36 | required: true |
27 | | - type: number |
28 | | - |
29 | | -# Serialize reviews per PR: a newer push cancels the in-progress review |
30 | | -# of the now-stale diff so only the freshest review runs (and posts a |
31 | | -# comment). Plain `cancel-in-progress: true` is safe here — this workflow |
32 | | -# is read-only (its allowedTools grant no git push / commit), so it never |
33 | | -# pushes a fix. |
34 | | -# `|| inputs.pr_number` keeps a workflow_dispatch review in the same |
35 | | -# group as the PR's pull_request reviews so they dedupe. |
36 | | -concurrency: |
37 | | - group: claude-review-${{ github.event.pull_request.number || inputs.pr_number }} |
38 | | - cancel-in-progress: true |
| 37 | + # String, not number: the reusable workflow's `pr-number` input is a |
| 38 | + # string and `gh workflow run -f` sends one. |
| 39 | + type: string |
39 | 40 |
|
40 | 41 | jobs: |
41 | | - claude-review: |
42 | | - # workflow_dispatch is a manual re-review from the Actions UI, so always |
43 | | - # run it — this intentionally bypasses the draft/Dependabot/fork guard |
44 | | - # below. A human with write access explicitly requesting a review can |
45 | | - # review any PR. For pull_request events, skip drafts, Dependabot bumps, |
46 | | - # and fork PRs (fork PRs can't read repo secrets via pull_request events, |
47 | | - # so CLAUDE_CODE_OAUTH_TOKEN would be empty and the run would fail with a |
48 | | - # noisy red check). |
49 | | - if: | |
50 | | - github.event_name == 'workflow_dispatch' || |
51 | | - (github.event.pull_request.draft == false && |
52 | | - github.event.pull_request.user.login != 'dependabot[bot]' && |
53 | | - github.event.pull_request.head.repo.full_name == github.repository) |
54 | | - runs-on: ubuntu-latest |
55 | | - timeout-minutes: 50 |
56 | | - env: |
57 | | - # Resolve the PR number once: from the pull_request event, or the |
58 | | - # workflow_dispatch input when a manual re-review is triggered. |
59 | | - PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} |
| 42 | + review: |
60 | 43 | permissions: |
61 | 44 | contents: read |
62 | 45 | pull-requests: write |
63 | | - issues: read |
| 46 | + issues: write |
64 | 47 | id-token: write |
65 | | - |
66 | | - steps: |
67 | | - # A PR that changes THIS workflow file can't be reviewed by the action: |
68 | | - # its App-token exchange requires the review workflow to match the |
69 | | - # default branch, so it 401s — the action itself says this is normal |
70 | | - # and to ignore it. Rather than post a failing check, detect that case |
71 | | - # and skip the review; it runs normally once merged. |
72 | | - - name: Skip self-review when the PR edits this workflow |
73 | | - id: selfmod |
74 | | - env: |
75 | | - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
76 | | - REPO: ${{ github.repository }} |
77 | | - run: | |
78 | | - files=$(gh api "repos/$REPO/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename' 2>/dev/null || true) |
79 | | - if printf '%s\n' "$files" | grep -qxF '.github/workflows/claude-code-review.yml'; then |
80 | | - echo "self_mod=true" >> "$GITHUB_OUTPUT" |
81 | | - echo "::notice::PR #$PR_NUMBER edits claude-code-review.yml — skipping self-review (runs after merge)." |
82 | | - else |
83 | | - echo "self_mod=false" >> "$GITHUB_OUTPUT" |
84 | | - fi |
85 | | -
|
86 | | - - name: Checkout repository |
87 | | - uses: actions/checkout@v7 |
88 | | - with: |
89 | | - fetch-depth: 1 |
90 | | - |
91 | | - # Skip reviewer toggling on fork PRs: GitHub downgrades GITHUB_TOKEN |
92 | | - # to read-only for fork events even when `pull-requests: write` is |
93 | | - # declared, so the DELETE / POST below would 403. The job-level |
94 | | - # reviewer (Run Claude Code Review) still runs because it uses its |
95 | | - # own OAuth token. |
96 | | - - name: Remove review request from d-morrison while Claude is reviewing |
97 | | - id: remove_reviewer |
98 | | - if: | |
99 | | - github.event.pull_request.number && |
100 | | - github.event.pull_request.head.repo.full_name == github.repository |
101 | | - env: |
102 | | - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
103 | | - HAD_REVIEWER: ${{ contains(github.event.pull_request.requested_reviewers.*.login, 'd-morrison') }} |
104 | | - run: | |
105 | | - echo "had_reviewer=$HAD_REVIEWER" >> "$GITHUB_OUTPUT" |
106 | | - if [ "$HAD_REVIEWER" = "true" ]; then |
107 | | - gh api -X DELETE \ |
108 | | - "repos/${{ github.repository }}/pulls/$PR_NUMBER/requested_reviewers" \ |
109 | | - -f "reviewers[]=d-morrison" \ |
110 | | - || echo "::warning::failed to remove d-morrison from reviewers on PR #$PR_NUMBER" |
111 | | - fi |
112 | | -
|
113 | | - - name: Run Claude Code Review |
114 | | - id: claude-review |
115 | | - if: steps.selfmod.outputs.self_mod != 'true' |
116 | | - uses: anthropics/claude-code-action@v1 |
117 | | - with: |
118 | | - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} |
119 | | - # A `claude` remote session pushing a commit to a PR fires |
120 | | - # `synchronize` with the `claude` bot as actor; another CI |
121 | | - # workflow pushing a commit fires it as `github-actions[bot]`. |
122 | | - # Without allowing these bots the action aborts with "Workflow |
123 | | - # initiated by non-human actor" and posts no review. The job |
124 | | - # `if:` already restricts runs to same-repo, non-Dependabot PRs, |
125 | | - # so accepting these bots here is safe. |
126 | | - allowed_bots: "github-actions[bot],claude" |
127 | | - plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' |
128 | | - plugins: 'code-review@claude-code-plugins' |
129 | | - track_progress: ${{ github.event_name == 'pull_request' && 'true' || 'false' }} |
130 | | - prompt: | |
131 | | - /code-review:code-review ${{ github.repository }}/pull/${{ env.PR_NUMBER }} |
132 | | -
|
133 | | - In addition to the standard checks above, this is an R package |
134 | | - template following UCD-SERG standards, so also prioritize: |
135 | | -
|
136 | | - 1. **R package correctness** |
137 | | - - Roxygen2 docs are in sync (`devtools::document()` is required |
138 | | - before committing; `R-check-docs.yml` enforces this). |
139 | | - - `NAMESPACE` and `man/` are not edited by hand. |
140 | | - - `README.md` is generated from `README.Rmd`; edits go there. |
141 | | -
|
142 | | - 2. **R style** (`.lintr.R` is authoritative) |
143 | | - - snake_case names, line length ≤ 80 chars, no `T`/`F` for |
144 | | - TRUE/FALSE, no `:::`-style internal calls. |
145 | | - - Tidyverse idioms and native `|>` pipe. |
146 | | -
|
147 | | - 3. **Testing** |
148 | | - - New/changed behaviour is covered by testthat. |
149 | | - - Random outputs use `set.seed()` so snapshots are deterministic. |
150 | | -
|
151 | | - 4. **Changelog** |
152 | | - - Every user-facing change has a `NEWS.md` bullet |
153 | | - (`news.yaml` enforces this; a missing entry is a CI failure). |
154 | | -
|
155 | | - 5. **CI hygiene** |
156 | | - - No new dependencies without a `DESCRIPTION` entry. |
157 | | - - Spell-check / lint failures are fixed at the source, |
158 | | - not suppressed. |
159 | | -
|
160 | | - **Post line-specific findings as inline review comments** anchored |
161 | | - to the relevant line(s). Reserve the top-level summary for a brief |
162 | | - overall verdict plus any finding not tied to a specific line; |
163 | | - don't restate each inline comment there. Skip generic praise. |
164 | | -
|
165 | | - - name: Re-request review from d-morrison when Claude finishes reviewing |
166 | | - if: | |
167 | | - always() && |
168 | | - github.event.pull_request.number && |
169 | | - github.event.pull_request.head.repo.full_name == github.repository && |
170 | | - steps.remove_reviewer.outputs.had_reviewer == 'true' |
171 | | - env: |
172 | | - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
173 | | - run: | |
174 | | - gh api -X POST \ |
175 | | - "repos/${{ github.repository }}/pulls/$PR_NUMBER/requested_reviewers" \ |
176 | | - -f "reviewers[]=d-morrison" \ |
177 | | - || echo "::warning::failed to re-request d-morrison as reviewer on PR #$PR_NUMBER" |
| 48 | + actions: read # lets the reviewer read CI status (github_ci MCP server) |
| 49 | + uses: Morrison-Lab/gha/.github/workflows/claude-code-review.yml@v2 |
| 50 | + secrets: |
| 51 | + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} |
| 52 | + with: |
| 53 | + # Wire the workflow_dispatch input through so claude.yml can re-dispatch |
| 54 | + # a review on Claude's commits; empty (and ignored) for pull_request runs. |
| 55 | + pr-number: ${{ inputs.pr_number }} |
| 56 | + # Accept the `claude` and github-actions[bot] actors so a review |
| 57 | + # dispatched after an @claude run (or another CI push) is not rejected as |
| 58 | + # "non-human". Matches the pre-migration workflow's allowed_bots. |
| 59 | + allowed-bots: "github-actions[bot],claude" |
| 60 | + prompt-addendum: | |
| 61 | + This is an R package template following UCD-SERG standards, so in |
| 62 | + addition to the standard checks also prioritize: |
| 63 | +
|
| 64 | + 1. **R package correctness** |
| 65 | + - Roxygen2 docs are in sync (`devtools::document()` is required |
| 66 | + before committing; `R-check-docs.yml` enforces this). |
| 67 | + - `NAMESPACE` and `man/` are not edited by hand. |
| 68 | + - `README.md` is generated from `README.Rmd`; edits go there. |
| 69 | +
|
| 70 | + 2. **R style** (`.lintr.R` is authoritative) |
| 71 | + - snake_case names, line length <= 80 chars, no `T`/`F` for |
| 72 | + TRUE/FALSE, no `:::`-style internal calls. |
| 73 | + - Tidyverse idioms and native `|>` pipe. |
| 74 | +
|
| 75 | + 3. **Testing** |
| 76 | + - New/changed behaviour is covered by testthat. |
| 77 | + - Random outputs use `set.seed()` so snapshots are deterministic. |
| 78 | +
|
| 79 | + 4. **Changelog** |
| 80 | + - Every user-facing change has a `NEWS.md` bullet |
| 81 | + (`news.yaml` enforces this; a missing entry is a CI failure). |
| 82 | +
|
| 83 | + 5. **CI hygiene** |
| 84 | + - No new dependencies without a `DESCRIPTION` entry. |
| 85 | + - Spell-check / lint failures are fixed at the source, |
| 86 | + not suppressed. |
0 commit comments