From 2b6324dade6788f19058a526fae0a541d1210eab Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Sat, 15 Aug 2026 18:13:15 -0500 Subject: [PATCH] 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. --- .github/workflows/claude.yml | 40 ++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 4ffd9f1..dc40ddb 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -1,6 +1,8 @@ name: Claude Code Review on: + pull_request: + types: [opened, ready_for_review] issue_comment: types: [created] pull_request_review_comment: @@ -10,13 +12,33 @@ on: pull_request_review: types: [submitted] +# One review per PR at a time. A rapid draft->ready->draft toggle would otherwise +# stack runs and burn Claude usage on states nobody is waiting for. +concurrency: + group: claude-review-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }} + cancel-in-progress: true + jobs: claude: + # Defense-in-depth author gate: only run when the triggering actor is a repo + # OWNER/MEMBER/COLLABORATOR, so a drive-by comment or a fork PR from an + # outside account cannot invoke Claude or drain Claude usage. This repo is + # public, so the gate is load-bearing, not decorative. + # + # The pull_request arm reviews every PR opened by an org member — the + # compensating control for CC8.1, since a sole maintainer cannot approve + # their own PR. It is `pull_request`, never `pull_request_target`, so a fork + # PR carries no secrets even in the case where it somehow ran. if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + ( + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) + ) || + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)) || + (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)) runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -41,8 +63,14 @@ jobs: additional_permissions: | actions: read - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' + # On the automatic pull_request trigger there is no comment to act on, so + # supply the review brief. Left empty for comment-driven events so Claude + # follows the instructions in the comment that tagged it. + # + # Deliberately does NOT approve: org policy sets + # can_approve_pull_request_reviews=false, and an unconditional bot approval + # on every PR is a rubber stamp - worse audit evidence than none. + 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.' || '' }} # Optional: Add claude_args to customize behavior and configuration # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md