Skip to content

Commit 0c9c174

Browse files
authored
chore: dedupe Claude review comments and track latest verdict (#8804)
* chore: dedupe Claude review comments and track latest verdict Add concurrency cancel-in-progress, delete older claude[bot] summaries so only the latest is visible, skip duplicate auto-approvals, and dismiss approval + strip claude-approved/no QA needed when the latest Claude verdict is FAIL or PASS+COMPLEX. * chore: run Claude review on sensitive-path PRs to waive QA For PRs touching .github/prompts, .github/workflows, or CODEOWNERS, still run Claude review but never auto-APPROVE / add claude-approved. Human DEV review remains required. When Claude reports PASS+SIMPLE+ QA_REQUIRED:NO, apply the "no QA needed" label so enforce-approvals passes with just one DEV approval. Stale "no QA needed" and approvals are cleaned up on negative verdicts.
1 parent c7d396a commit 0c9c174

1 file changed

Lines changed: 95 additions & 39 deletions

File tree

.github/workflows/claude-pr-review.yml

Lines changed: 95 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ on:
88
pull_request:
99
types: [opened, synchronize, ready_for_review]
1010

11+
concurrency:
12+
group: claude-review-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
13+
cancel-in-progress: true
14+
1115
jobs:
1216
auto-review:
1317
if: |
@@ -44,11 +48,12 @@ jobs:
4448
f.filename === 'CODEOWNERS'
4549
);
4650
if (touchesSensitive) {
47-
core.notice('Auto-review disabled: PR modifies .github/prompts, .github/workflows, or CODEOWNERS. Human review required.');
51+
core.notice('Sensitive paths modified (.github/prompts, .github/workflows, or CODEOWNERS): Claude will review but not auto-approve. Human DEV review required; QA can still be waived if Claude reports QA_REQUIRED: NO.');
4852
}
4953
}
5054
51-
core.setOutput('is-auto-review', (isAutoReview && !touchesSensitive).toString());
55+
core.setOutput('is-auto-review', isAutoReview.toString());
56+
core.setOutput('touches-sensitive', touchesSensitive.toString());
5257
5358
- name: Set status to pending
5459
if: steps.check-type.outputs.is-auto-review == 'true'
@@ -121,17 +126,16 @@ jobs:
121126
with:
122127
script: |
123128
const prNumber = ${{ github.event.pull_request.number }};
129+
const owner = context.repo.owner;
130+
const repo = context.repo.repo;
131+
const touchesSensitive = '${{ steps.check-type.outputs.touches-sensitive }}' === 'true';
124132
125133
const comments = await github.rest.issues.listComments({
126-
owner: context.repo.owner,
127-
repo: context.repo.repo,
128-
issue_number: prNumber,
129-
per_page: 100
134+
owner, repo, issue_number: prNumber, per_page: 100
130135
});
131136
132-
const claudeComment = comments.data
133-
.reverse()
134-
.find(c => c.user?.login === 'claude[bot]');
137+
const claudeComments = comments.data.filter(c => c.user?.login === 'claude[bot]');
138+
const claudeComment = claudeComments[claudeComments.length - 1];
135139
136140
const output = claudeComment?.body ?? '';
137141
console.log('Claude comment preview:', output.slice(0, 300));
@@ -143,36 +147,77 @@ jobs:
143147
const hasReviewResult = output.includes('REVIEW_RESULT:');
144148
const passed = !failed && !errored;
145149
146-
if (passed && isSimple) {
147-
const approvalBody = qaRequired
148-
? 'Auto-approved by Claude — simple fix/chore with no blocking issues. QA approval is still required.'
149-
: 'Auto-approved by Claude — simple fix/chore with no blocking issues. No QA needed (non-runtime changes only).';
150-
151-
await github.rest.pulls.createReview({
152-
owner: context.repo.owner,
153-
repo: context.repo.repo,
154-
pull_number: prNumber,
155-
event: 'APPROVE',
156-
body: approvalBody
157-
});
158-
159-
const labels = ['claude-approved'];
160-
if (!qaRequired) {
161-
labels.push('no QA needed');
150+
const reviews = await github.rest.pulls.listReviews({
151+
owner, repo, pull_number: prNumber, per_page: 100
152+
});
153+
const ourApprovals = reviews.data.filter(r =>
154+
r.user?.login === 'github-actions[bot]' && r.state === 'APPROVED'
155+
);
156+
const prLabels = (context.payload.pull_request.labels || []).map(l => l.name);
157+
const COMPLEX_MARKER = '🔍 Claude reviewed this PR and found no blocking issues';
158+
const SENSITIVE_MARKER = '🔒 Claude reviewed this PR — sensitive paths modified';
159+
160+
const canAutoApprove = hasReviewResult && passed && isSimple && !touchesSensitive;
161+
const canWaiveQa = hasReviewResult && passed && isSimple && !qaRequired;
162+
163+
if (canAutoApprove) {
164+
if (ourApprovals.length === 0) {
165+
const approvalBody = qaRequired
166+
? 'Auto-approved by Claude — simple fix/chore with no blocking issues. QA approval is still required.'
167+
: 'Auto-approved by Claude — simple fix/chore with no blocking issues. No QA needed (non-runtime changes only).';
168+
await github.rest.pulls.createReview({
169+
owner, repo, pull_number: prNumber, event: 'APPROVE', body: approvalBody
170+
});
162171
}
163172
await github.rest.issues.addLabels({
164-
owner: context.repo.owner,
165-
repo: context.repo.repo,
166-
issue_number: prNumber,
167-
labels
168-
});
169-
} else if (passed && !isSimple) {
170-
await github.rest.issues.createComment({
171-
owner: context.repo.owner,
172-
repo: context.repo.repo,
173-
issue_number: prNumber,
174-
body: '🔍 Claude reviewed this PR and found no blocking issues, but assessed it as **complex** — human DEV review is still required before merging.'
173+
owner, repo, issue_number: prNumber, labels: ['claude-approved']
175174
});
175+
} else if (hasReviewResult) {
176+
for (const r of ourApprovals) {
177+
await github.rest.pulls.dismissReview({
178+
owner, repo, pull_number: prNumber, review_id: r.id,
179+
message: 'Dismissed: latest Claude review no longer auto-approves this PR.'
180+
}).catch(e => console.log(`dismissReview: ${e.message}`));
181+
}
182+
if (prLabels.includes('claude-approved')) {
183+
await github.rest.issues.removeLabel({
184+
owner, repo, issue_number: prNumber, name: 'claude-approved'
185+
}).catch(e => console.log(`removeLabel claude-approved: ${e.message}`));
186+
}
187+
if (touchesSensitive && passed && isSimple && !comments.data.some(c =>
188+
c.user?.login === 'github-actions[bot]' && c.body?.startsWith(SENSITIVE_MARKER))) {
189+
const qaNote = qaRequired
190+
? 'QA approval is still required.'
191+
: 'No QA needed (Claude reported `QA_REQUIRED: NO`).';
192+
await github.rest.issues.createComment({
193+
owner, repo, issue_number: prNumber,
194+
body: `${SENSITIVE_MARKER} (\`.github/prompts\`, \`.github/workflows\`, or \`CODEOWNERS\`). Claude will not auto-approve these PRs — human DEV review is required. ${qaNote}`
195+
});
196+
} else if (!touchesSensitive && passed && !isSimple && !comments.data.some(c =>
197+
c.user?.login === 'github-actions[bot]' && c.body?.startsWith(COMPLEX_MARKER))) {
198+
await github.rest.issues.createComment({
199+
owner, repo, issue_number: prNumber,
200+
body: COMPLEX_MARKER + ', but assessed it as **complex** — human DEV review is still required before merging.'
201+
});
202+
}
203+
}
204+
205+
if (canWaiveQa) {
206+
if (!prLabels.includes('no QA needed')) {
207+
await github.rest.issues.addLabels({
208+
owner, repo, issue_number: prNumber, labels: ['no QA needed']
209+
});
210+
}
211+
} else if (hasReviewResult && prLabels.includes('no QA needed')) {
212+
await github.rest.issues.removeLabel({
213+
owner, repo, issue_number: prNumber, name: 'no QA needed'
214+
}).catch(e => console.log(`removeLabel no QA needed: ${e.message}`));
215+
}
216+
217+
for (const c of claudeComments.slice(0, -1)) {
218+
await github.rest.issues.deleteComment({
219+
owner, repo, comment_id: c.id
220+
}).catch(e => console.log(`deleteComment ${c.id}: ${e.message}`));
176221
}
177222
178223
let description;
@@ -184,6 +229,10 @@ jobs:
184229
description = 'Claude review encountered an error';
185230
} else if (failed) {
186231
description = 'Claude found issues that must be resolved';
232+
} else if (touchesSensitive && isSimple) {
233+
description = canWaiveQa
234+
? 'No blocking issues — sensitive paths, DEV review required (no QA needed)'
235+
: 'No blocking issues — sensitive paths, DEV review required';
187236
} else if (isSimple) {
188237
description = 'No blocking issues found — PR auto-approved';
189238
} else {
@@ -373,9 +422,8 @@ jobs:
373422
per_page: 100
374423
});
375424
376-
const claudeComment = comments.data
377-
.reverse()
378-
.find(c => c.user?.login === 'claude[bot]');
425+
const claudeComments = comments.data.filter(c => c.user?.login === 'claude[bot]');
426+
const claudeComment = claudeComments[claudeComments.length - 1];
379427
380428
const output = claudeComment?.body ?? '';
381429
console.log('Claude comment preview:', output.slice(0, 200));
@@ -384,6 +432,14 @@ jobs:
384432
const errored = '${{ steps.claude.outcome }}' === 'failure';
385433
const hasReviewResult = output.includes('REVIEW_RESULT:');
386434
435+
for (const c of claudeComments.slice(0, -1)) {
436+
await github.rest.issues.deleteComment({
437+
owner: context.repo.owner,
438+
repo: context.repo.repo,
439+
comment_id: c.id
440+
}).catch(e => console.log(`deleteComment ${c.id}: ${e.message}`));
441+
}
442+
387443
let description;
388444
if (errored && !output) {
389445
description = 'Claude review failed — credit balance too low or API error';

0 commit comments

Comments
 (0)