Skip to content

Commit 4f89e0d

Browse files
authored
ci(security-review): add safe-to-review label as alternate trigger (#1297)
* 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. * ci(security-review): use GitHub App token on every github-script step 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.
1 parent fcbdf59 commit 4f89e0d

2 files changed

Lines changed: 47 additions & 25 deletions

File tree

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

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Claude Security Review
22

33
on:
44
pull_request_target:
5-
types: [opened, reopened, synchronize]
5+
types: [opened, reopened, synchronize, labeled]
66
pull_request_review:
77
types: [submitted]
88
workflow_dispatch:
@@ -30,11 +30,13 @@ concurrency:
3030
jobs:
3131
authorize:
3232
runs-on: ubuntu-latest
33-
# On pull_request_review events, only proceed when the review state is 'approved'.
34-
# Comment/changes-requested reviews are ignored to avoid running the workflow on every review.
33+
# Filter event subtypes early:
34+
# - pull_request_review: only the 'approved' state authorizes (comment/changes-requested are ignored).
35+
# - pull_request_target labeled: only the 'safe-to-review' label triggers (other label changes are ignored).
3536
if: |
36-
github.event_name != 'pull_request_review' ||
37-
github.event.review.state == 'approved'
37+
(github.event_name != 'pull_request_review' || github.event.review.state == 'approved') &&
38+
(github.event_name != 'pull_request_target' || github.event.action != 'labeled' ||
39+
github.event.label.name == 'safe-to-review')
3840
outputs:
3941
authorized: ${{ steps.auth.outputs.authorized || steps.dispatch-auth.outputs.authorized }}
4042
steps:
@@ -44,14 +46,24 @@ jobs:
4446
uses: actions/github-script@v9
4547
with:
4648
script: |
47-
// pull_request_target: gate on the PR author (maintainer-authored PRs auto-run on open/reopen).
48-
// pull_request_review: gate on the reviewer who approved (so a maintainer approving a community PR
49-
// authorizes the security review).
49+
// pull_request_target opened/reopened/synchronize: gate on the PR author.
50+
// pull_request_target labeled (safe-to-review): gate on the labeler (sender).
51+
// pull_request_review (approved): gate on the reviewer who approved.
5052
const isApproval = context.eventName === 'pull_request_review';
51-
const user = isApproval
52-
? context.payload.review.user.login
53-
: context.payload.pull_request.user.login;
54-
const reason = isApproval ? `approver ${user}` : `PR author ${user}`;
53+
const isLabel =
54+
context.eventName === 'pull_request_target' && context.payload.action === 'labeled';
55+
let user;
56+
let reason;
57+
if (isApproval) {
58+
user = context.payload.review.user.login;
59+
reason = `approver ${user}`;
60+
} else if (isLabel) {
61+
user = context.payload.sender.login;
62+
reason = `labeler ${user}`;
63+
} else {
64+
user = context.payload.pull_request.user.login;
65+
reason = `PR author ${user}`;
66+
}
5567
try {
5668
await github.rest.teams.getMembershipForUserInOrg({
5769
org: context.repo.owner,
@@ -94,12 +106,23 @@ jobs:
94106
env:
95107
AWS_REGION: us-west-2
96108
steps:
109+
# Generate the GitHub App token first so every subsequent github-script step can
110+
# use it. The default GITHUB_TOKEN is read-only on pull_request_target /
111+
# pull_request_review events from forks, which makes label/comment writes 403.
112+
- name: Generate GitHub App token
113+
id: app-token
114+
uses: actions/create-github-app-token@v1
115+
with:
116+
app-id: ${{ vars.APP_ID }}
117+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
118+
97119
- name: Resolve PR number
98120
id: pr
99121
uses: actions/github-script@v9
100122
env:
101123
PR_NUMBER_INPUT: ${{ inputs.pr_number }}
102124
with:
125+
github-token: ${{ steps.app-token.outputs.token }}
103126
script: |
104127
const num =
105128
context.eventName === 'workflow_dispatch'
@@ -119,6 +142,7 @@ jobs:
119142
env:
120143
PR_NUMBER: ${{ steps.pr.outputs.number }}
121144
with:
145+
github-token: ${{ steps.app-token.outputs.token }}
122146
script: |
123147
const prNumber = parseInt(process.env.PR_NUMBER, 10);
124148
try {
@@ -181,13 +205,6 @@ jobs:
181205
echo "prompt_file=$OUT" >> "$GITHUB_OUTPUT"
182206
echo "Prompt size: $(wc -c < "$OUT") bytes"
183207
184-
- name: Generate GitHub App token
185-
id: app-token
186-
uses: actions/create-github-app-token@v1
187-
with:
188-
app-id: ${{ vars.APP_ID }}
189-
private-key: ${{ secrets.APP_PRIVATE_KEY }}
190-
191208
- name: Configure AWS credentials (OIDC)
192209
uses: aws-actions/configure-aws-credentials@v6
193210
with:
@@ -264,6 +281,7 @@ jobs:
264281
env:
265282
PR_NUMBER: ${{ steps.pr.outputs.number }}
266283
with:
284+
github-token: ${{ steps.app-token.outputs.token }}
267285
script: |
268286
const prNumber = parseInt(process.env.PR_NUMBER, 10);
269287
try {

CONTRIBUTING.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,17 @@ wanted' issues is a great place to start.
5151
## Maintainer notes: Claude security review on community PRs
5252

5353
The `Claude Security Review` workflow runs automatically on maintainer-authored PRs (opened/reopened) and on community
54-
PRs as soon as a maintainer submits an **approving review**. PRs from non-collaborators are otherwise skipped — the
55-
approval is the gate, so a maintainer must manually review the diff before the automated reviewer runs.
54+
PRs once a maintainer either submits an **approving review** _or_ applies the **`safe-to-review`** label. PRs from
55+
non-collaborators are otherwise skipped — that explicit signal is the gate, so a maintainer must manually review the
56+
diff before the automated reviewer runs.
5657

57-
To re-run the review on a later commit, submit another approving review (resolves to a fresh workflow run), or trigger
58-
the `Claude Security Review` workflow manually from the Actions tab with the PR number. Note that manual dispatch can
59-
verify the analysis and prompt plumbing but cannot post inline comments — the action's inline-comment MCP server only
60-
attaches on PR-context events (`pull_request_target`, `pull_request_review`).
58+
The label flow is convenient when you want to kick the security review without committing to an approval: drop the label
59+
on the PR and the workflow fires once. Removing and re-applying the label re-triggers the review.
60+
61+
To re-run the review on a later commit, submit another approving review, re-apply the label, or trigger the
62+
`Claude Security Review` workflow manually from the Actions tab with the PR number. Note that manual dispatch can verify
63+
the analysis and prompt plumbing but cannot post inline comments — the action's inline-comment MCP server only attaches
64+
on PR-context events (`pull_request_target`, `pull_request_review`).
6165

6266
## Code of Conduct
6367

0 commit comments

Comments
 (0)