From c4560c791fe179b8a0d68a48a922a24d32ce6064 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Mon, 18 May 2026 17:29:28 -0400 Subject: [PATCH 1/2] ci(security-review): add safe-to-review label as alternate trigger for community PRs Re-add the labeled event to pull_request_target so maintainers can kick the security review on a community PR by applying 'safe-to-review' without committing to an approval. The auth gate keys on the labeler (sender) for that path, mirroring the approver path. Other label changes are filtered out at the job level so unrelated label churn doesn't spawn API calls. CONTRIBUTING.md updated to describe the dual-path. --- .github/workflows/pr-security-review.yml | 36 ++++++++++++++++-------- CONTRIBUTING.md | 16 +++++++---- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/.github/workflows/pr-security-review.yml b/.github/workflows/pr-security-review.yml index 6d2184f84..d91d7cb69 100644 --- a/.github/workflows/pr-security-review.yml +++ b/.github/workflows/pr-security-review.yml @@ -2,7 +2,7 @@ name: Claude Security Review on: pull_request_target: - types: [opened, reopened, synchronize] + types: [opened, reopened, synchronize, labeled] pull_request_review: types: [submitted] workflow_dispatch: @@ -30,11 +30,13 @@ concurrency: jobs: authorize: runs-on: ubuntu-latest - # On pull_request_review events, only proceed when the review state is 'approved'. - # Comment/changes-requested reviews are ignored to avoid running the workflow on every review. + # Filter event subtypes early: + # - pull_request_review: only the 'approved' state authorizes (comment/changes-requested are ignored). + # - pull_request_target labeled: only the 'safe-to-review' label triggers (other label changes are ignored). if: | - github.event_name != 'pull_request_review' || - github.event.review.state == 'approved' + (github.event_name != 'pull_request_review' || github.event.review.state == 'approved') && + (github.event_name != 'pull_request_target' || github.event.action != 'labeled' || + github.event.label.name == 'safe-to-review') outputs: authorized: ${{ steps.auth.outputs.authorized || steps.dispatch-auth.outputs.authorized }} steps: @@ -44,14 +46,24 @@ jobs: uses: actions/github-script@v9 with: script: | - // pull_request_target: gate on the PR author (maintainer-authored PRs auto-run on open/reopen). - // pull_request_review: gate on the reviewer who approved (so a maintainer approving a community PR - // authorizes the security review). + // pull_request_target opened/reopened/synchronize: gate on the PR author. + // pull_request_target labeled (safe-to-review): gate on the labeler (sender). + // pull_request_review (approved): gate on the reviewer who approved. const isApproval = context.eventName === 'pull_request_review'; - const user = isApproval - ? context.payload.review.user.login - : context.payload.pull_request.user.login; - const reason = isApproval ? `approver ${user}` : `PR author ${user}`; + const isLabel = + context.eventName === 'pull_request_target' && context.payload.action === 'labeled'; + let user; + let reason; + if (isApproval) { + user = context.payload.review.user.login; + reason = `approver ${user}`; + } else if (isLabel) { + user = context.payload.sender.login; + reason = `labeler ${user}`; + } else { + user = context.payload.pull_request.user.login; + reason = `PR author ${user}`; + } try { await github.rest.teams.getMembershipForUserInOrg({ org: context.repo.owner, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c58358e4..296863c11 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,13 +51,17 @@ wanted' issues is a great place to start. ## Maintainer notes: Claude security review on community PRs The `Claude Security Review` workflow runs automatically on maintainer-authored PRs (opened/reopened) and on community -PRs as soon as a maintainer submits an **approving review**. PRs from non-collaborators are otherwise skipped — the -approval is the gate, so a maintainer must manually review the diff before the automated reviewer runs. +PRs once a maintainer either submits an **approving review** _or_ applies the **`safe-to-review`** label. PRs from +non-collaborators are otherwise skipped — that explicit signal is the gate, so a maintainer must manually review the +diff before the automated reviewer runs. -To re-run the review on a later commit, submit another approving review (resolves to a fresh workflow run), or trigger -the `Claude Security Review` workflow manually from the Actions tab with the PR number. Note that manual dispatch can -verify the analysis and prompt plumbing but cannot post inline comments — the action's inline-comment MCP server only -attaches on PR-context events (`pull_request_target`, `pull_request_review`). +The label flow is convenient when you want to kick the security review without committing to an approval: drop the label +on the PR and the workflow fires once. Removing and re-applying the label re-triggers the review. + +To re-run the review on a later commit, submit another approving review, re-apply the label, or trigger the +`Claude Security Review` workflow manually from the Actions tab with the PR number. Note that manual dispatch can verify +the analysis and prompt plumbing but cannot post inline comments — the action's inline-comment MCP server only attaches +on PR-context events (`pull_request_target`, `pull_request_review`). ## Code of Conduct From c36cf12bd779a7f1ea111729178883c677fefb15 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Mon, 18 May 2026 18:28:14 -0400 Subject: [PATCH 2/2] ci(security-review): use GitHub App token on every github-script step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default GITHUB_TOKEN is read-only on pull_request_target / pull_request_review events from forks (GitHub policy, ignores the workflow's permissions: block). That made the label-add step fail with 403 'Resource not accessible by integration' on community PRs, which then short-circuited the rest of the job (including the Generate GitHub App token step), causing the summary step to fail with 'Input required and not supplied: github-token'. Hoist the App token step to the top of the security-review job and pass ${{ steps.app-token.outputs.token }} on every github-script step that mutates state (label add, label remove, summary comment). The Resolve PR number step also gets it for consistency, even though it only reads. Verified the cancelled state on PR #1297's first run was unrelated: it was concurrency: cancel-in-progress canceling the pull_request_target run when the pull_request_review run queued — expected behavior, not a bug. --- .github/workflows/pr-security-review.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-security-review.yml b/.github/workflows/pr-security-review.yml index d91d7cb69..daf9fc176 100644 --- a/.github/workflows/pr-security-review.yml +++ b/.github/workflows/pr-security-review.yml @@ -106,12 +106,23 @@ jobs: env: AWS_REGION: us-west-2 steps: + # Generate the GitHub App token first so every subsequent github-script step can + # use it. The default GITHUB_TOKEN is read-only on pull_request_target / + # pull_request_review events from forks, which makes label/comment writes 403. + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + - name: Resolve PR number id: pr uses: actions/github-script@v9 env: PR_NUMBER_INPUT: ${{ inputs.pr_number }} with: + github-token: ${{ steps.app-token.outputs.token }} script: | const num = context.eventName === 'workflow_dispatch' @@ -131,6 +142,7 @@ jobs: env: PR_NUMBER: ${{ steps.pr.outputs.number }} with: + github-token: ${{ steps.app-token.outputs.token }} script: | const prNumber = parseInt(process.env.PR_NUMBER, 10); try { @@ -193,13 +205,6 @@ jobs: echo "prompt_file=$OUT" >> "$GITHUB_OUTPUT" echo "Prompt size: $(wc -c < "$OUT") bytes" - - name: Generate GitHub App token - id: app-token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - name: Configure AWS credentials (OIDC) uses: aws-actions/configure-aws-credentials@v6 with: @@ -276,6 +281,7 @@ jobs: env: PR_NUMBER: ${{ steps.pr.outputs.number }} with: + github-token: ${{ steps.app-token.outputs.token }} script: | const prNumber = parseInt(process.env.PR_NUMBER, 10); try {