-
Notifications
You must be signed in to change notification settings - Fork 0
164 lines (153 loc) · 7.79 KB
/
Copy pathclaude-code-review.yml
File metadata and controls
164 lines (153 loc) · 7.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
# Serialize runs per PR. The stash/restore reviewer dance below assumes
# only one run is mutating the PR's reviewer list at a time; without
# this, an overlapping second run reads an already-cleared list, stashes
# [], and on restore wipes the original reviewers permanently. Cancel
# the in-flight run on a new push so the freshest diff wins.
concurrency:
group: claude-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
# The reviewer stash/restore is a serialization signal: we clear the
# human reviewers before Claude runs so they aren't notified mid-run,
# then re-add them when Claude finishes so the re-add fires a fresh
# GitHub notification — letting the human see Claude's review before
# they start their own. We also snapshot the PR head SHA so the
# post-Claude step can detect whether Claude pushed any commits and
# route the re-request accordingly (human assignees vs. original
# reviewer set). The restore is load-bearing for the notification
# signal, so it must NOT silently swallow errors (see restore step
# below).
- name: Stash and clear reviewers; record starting head SHA
id: stash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
HEAD_SHA=$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq '.head.sha')
echo "head_before=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "Starting PR head: $HEAD_SHA"
REVIEWERS_JSON=$(gh api "repos/$REPO/pulls/$PR_NUMBER/requested_reviewers")
USERS=$(echo "$REVIEWERS_JSON" | jq -c '[.users[].login]')
TEAMS=$(echo "$REVIEWERS_JSON" | jq -c '[.teams[].slug]')
echo "users=$USERS" >> "$GITHUB_OUTPUT"
echo "teams=$TEAMS" >> "$GITHUB_OUTPUT"
echo "Stashed users: $USERS"
echo "Stashed teams: $TEAMS"
if [ "$USERS" != "[]" ] || [ "$TEAMS" != "[]" ]; then
# `|| true` is intentional and asymmetric with the restore step:
# a failed stash leaves reviewers in place, which only causes a
# mid-run notification (recoverable). A failed restore would
# silently drop them entirely (not recoverable) — see that step.
jq -n --argjson users "$USERS" --argjson teams "$TEAMS" \
'{reviewers: $users, team_reviewers: $teams}' \
| gh api -X DELETE \
"repos/$REPO/pulls/$PR_NUMBER/requested_reviewers" \
--input - || true
fi
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# Force tag mode (anthropics/claude-code-action src/modes/detector.ts).
# In the default agent mode for pull_request events, the action never
# posts any non-inline PR comment by itself (src/modes/agent/index.ts:
# "No tracking comment in agent mode", commentId: undefined). The
# code-review plugin will only post a top-level comment if it finds
# issues scoring >= 80, so silent runs are common on small/mechanical
# PRs. track_progress forces tag mode, which creates a tracking
# comment up front via createInitialComment() and updates it at the
# end — guaranteeing a PR comment regardless of plugin output.
track_progress: 'true'
# Bundle the tracking comment into a single sticky per PR rather than
# a new comment each run. (Only consulted in tag mode.) Each new run
# updates the same comment in place — the latest review is always
# visible, and prior reviews from `@claude review` invocations (which
# post as separate non-sticky comments) are left untouched.
use_sticky_comment: 'true'
# Also archive the formatted report to the Actions step summary page
# (does not affect PR comments).
display_report: 'true'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
- name: Re-assign reviewers after Claude finishes
if: always() && steps.stash.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_BEFORE: ${{ steps.stash.outputs.head_before }}
USERS_BEFORE: ${{ steps.stash.outputs.users }}
TEAMS_BEFORE: ${{ steps.stash.outputs.teams }}
run: |
PR_JSON=$(gh api "repos/$REPO/pulls/$PR_NUMBER")
HEAD_AFTER=$(echo "$PR_JSON" | jq -r '.head.sha')
echo "Head before Claude: $HEAD_BEFORE"
echo "Head after Claude: $HEAD_AFTER"
if [ "$HEAD_AFTER" = "$HEAD_BEFORE" ]; then
# No new commits — restore the original reviewer set.
USERS=${USERS_BEFORE:-[]}
TEAMS=${TEAMS_BEFORE:-[]}
if [ "$USERS" = "[]" ] && [ "$TEAMS" = "[]" ]; then
echo "Claude made no commits and no reviewers were originally requested."
exit 0
fi
echo "Claude made no commits; restoring original reviewers."
# Do NOT add `|| true` here. A failed restore means the human
# reviewer was silently dropped from the PR and never gets the
# post-Claude notification — fail loudly so it can be re-added
# manually instead of vanishing.
jq -n --argjson users "$USERS" --argjson teams "$TEAMS" \
'{reviewers: $users, team_reviewers: $teams}' \
| gh api -X POST \
"repos/$REPO/pulls/$PR_NUMBER/requested_reviewers" \
--input -
exit 0
fi
# Claude pushed commits: route the re-request to the PR's human
# assignees so they can review Claude's changes and delegate
# further reviews. Bot assignees (e.g. copilot-swe-agent) are
# filtered out.
ASSIGNEES=$(echo "$PR_JSON" | jq -c '[.assignees[] | select(.type == "User") | .login]')
echo "Human assignees: $ASSIGNEES"
if [ "$ASSIGNEES" = "[]" ]; then
echo "Claude added commits but PR has no human assignees; leaving reviewers cleared."
exit 0
fi
# Same no-`|| true` rule as above — failure here drops the human
# assignee silently, so let it surface.
jq -n --argjson users "$ASSIGNEES" '{reviewers: $users}' \
| gh api -X POST \
"repos/$REPO/pulls/$PR_NUMBER/requested_reviewers" \
--input -