Skip to content

Commit 0af45b2

Browse files
authored
chore(ci): review every org-authored PR automatically (#190)
## Summary Every pull request opened by an org member now gets an automatic Claude review, and this repository's workflow gains the author gate it was missing entirely. The review is the compensating change-management control for SOC 2 CC8.1: a sole maintainer cannot approve their own pull request — GitHub forbids self-approval — so an automatic second reader stands in for independent review. This repo is one of three that previously had a pull-request requirement with *nothing* verifying the change before merge: no required status checks, and a `copilot_code_review` ruleset rule that is configured but produces no reviews (verified: zero reviews on the last three merged PRs here). ## Changes **`.github/workflows/claude.yml`** - Added a `pull_request` trigger on `[opened, ready_for_review]`. Not `synchronize` — every push to an open PR would re-run the review. - **Added the `author_association` gate, which this workflow did not have.** It previously ran for any commenter mentioning `@claude`, on a public repository. All four comment-driven arms are now restricted to `OWNER`/`MEMBER`/`COLLABORATOR`. - The new `pull_request` arm is guarded twice: `head.repo.full_name == github.repository` rejects fork PRs outright, and `author_association` rejects `CONTRIBUTOR`/`NONE`. Stays on `pull_request` rather than `pull_request_target`, so a fork PR carries no secrets regardless. - Added a `concurrency` group keyed on the PR number with `cancel-in-progress`. - Added a conditional `prompt` with a review brief tuned to this repo — it calls out the post-1.0 two-tier SDK contract, so the reviewer flags anything touching the stable tier that would require a coordinated client major. Left empty for comment-driven events. The brief instructs it to post findings as a comment and **not** to approve. ## Breaking Changes None. CI configuration only — no change to any published symbol, and neither tier of the SDK contract is touched. ## Testing Workflow YAML validated by parsing with `yaml.safe_load` and asserting the trigger set, gate count, fork check, and concurrency block. Repo pre-commit hooks passed (lint, format, typecheck, full unit suite). This PR is itself the first live exercise of the new trigger.
2 parents 2cf88a1 + 2b6324d commit 0af45b2

1 file changed

Lines changed: 34 additions & 6 deletions

File tree

‎.github/workflows/claude.yml‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
name: Claude Code Review
22

33
on:
4+
pull_request:
5+
types: [opened, ready_for_review]
46
issue_comment:
57
types: [created]
68
pull_request_review_comment:
@@ -10,13 +12,33 @@ on:
1012
pull_request_review:
1113
types: [submitted]
1214

15+
# One review per PR at a time. A rapid draft->ready->draft toggle would otherwise
16+
# stack runs and burn Claude usage on states nobody is waiting for.
17+
concurrency:
18+
group: claude-review-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
19+
cancel-in-progress: true
20+
1321
jobs:
1422
claude:
23+
# Defense-in-depth author gate: only run when the triggering actor is a repo
24+
# OWNER/MEMBER/COLLABORATOR, so a drive-by comment or a fork PR from an
25+
# outside account cannot invoke Claude or drain Claude usage. This repo is
26+
# public, so the gate is load-bearing, not decorative.
27+
#
28+
# The pull_request arm reviews every PR opened by an org member — the
29+
# compensating control for CC8.1, since a sole maintainer cannot approve
30+
# their own PR. It is `pull_request`, never `pull_request_target`, so a fork
31+
# PR carries no secrets even in the case where it somehow ran.
1532
if: |
16-
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17-
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18-
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19-
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
33+
(
34+
github.event_name == 'pull_request' &&
35+
github.event.pull_request.head.repo.full_name == github.repository &&
36+
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association)
37+
) ||
38+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
39+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
40+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)) ||
41+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association))
2042
runs-on: ubuntu-latest
2143
timeout-minutes: 15
2244
permissions:
@@ -41,8 +63,14 @@ jobs:
4163
additional_permissions: |
4264
actions: read
4365
44-
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
45-
# prompt: 'Update the pull request description to include a summary of changes.'
66+
# On the automatic pull_request trigger there is no comment to act on, so
67+
# supply the review brief. Left empty for comment-driven events so Claude
68+
# follows the instructions in the comment that tagged it.
69+
#
70+
# Deliberately does NOT approve: org policy sets
71+
# can_approve_pull_request_reviews=false, and an unconditional bot approval
72+
# on every PR is a rubber stamp - worse audit evidence than none.
73+
prompt: ${{ github.event_name == 'pull_request' && 'Review this pull request. This repository is a published SDK on a post-1.0 semver contract with external integrators, maintained by a single engineer who cannot approve their own PRs, so this review is the compensating change-management control - be substantive rather than cursory. Focus on correctness bugs, whether anything touches the stable tier (SDK facades, root exports, error classes, auth config, and the symbols the integration template imports) which would require a coordinated client major, and whether the PR description matches the diff. Post your findings as a comment. Do not approve the pull request.' || '' }}
4674

4775
# Optional: Add claude_args to customize behavior and configuration
4876
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md

0 commit comments

Comments
 (0)