Skip to content

Commit 2b6324d

Browse files
committed
chore(ci): review every org-authored PR automatically
Adds a pull_request trigger to the Claude review workflow so every PR opened by an org member is reviewed without anyone remembering to ask. This is the compensating change-management control for SOC 2 CC8.1: a sole maintainer cannot approve their own pull request, so an automatic second reader stands in for independent review. Also adds the author_association gate that this repo was missing entirely. The workflow is public and previously ran for any commenter. Outside contributors are now excluded on two independent checks - the head-repo test rejects fork PRs outright, and author_association rejects CONTRIBUTOR/NONE - so no one outside the org can invoke Claude or drain Claude usage. The review posts findings as a comment and does not approve. An unconditional bot approval on every PR would be a rubber stamp and worse audit evidence than the documented exception it would replace.
1 parent 2cf88a1 commit 2b6324d

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)