Skip to content

Commit 077b1a1

Browse files
committed
Initial Commit
Signed-off-by: Nicholas Gates <nick@nickgates.com>
1 parent 86a3754 commit 077b1a1

2 files changed

Lines changed: 56 additions & 14 deletions

File tree

.github/workflows/claude-review.yml

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ name: Claude Code (Review)
2020
# claude-write.yml so maintainers can ask Claude to make follow-up changes there.
2121

2222
concurrency:
23+
# `issue_comment` events on PRs expose the PR number via `github.event.issue.number`,
24+
# so this falls back there when `github.event.pull_request.number` is unset.
2325
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }}
2426
cancel-in-progress: true
2527

@@ -88,6 +90,10 @@ jobs:
8890
pullNumber = context.payload.pull_request?.number ?? null;
8991
}
9092
93+
if (pullNumber) {
94+
core.setOutput('pull_number', String(pullNumber));
95+
}
96+
9197
let reason = '';
9298
9399
if (!mentioned) {
@@ -103,11 +109,15 @@ jobs:
103109
const permission = await getPermission(sender);
104110
core.setOutput('actor_permission', permission);
105111
actorHasWrite = ['admin', 'maintain', 'write'].includes(permission) ? 'true' : 'false';
106-
if (actorHasWrite != 'true') {
112+
if (actorHasWrite !== 'true') {
107113
reason = 'actor_lacks_write';
108114
}
109115
}
110116
117+
if (!reason && context.eventName === 'issue_comment' && !trustedClaudeLogin) {
118+
reason = 'missing_claude_app_login';
119+
}
120+
111121
if (!reason) {
112122
let pr = null;
113123
const response = await github.rest.pulls.get({
@@ -118,8 +128,7 @@ jobs:
118128
pr = response.data;
119129
120130
const headRepo = pr.head.repo;
121-
const isFork = !headRepo || Boolean(headRepo.fork) || headRepo.full_name !== `${context.repo.owner}/${context.repo.repo}`;
122-
core.setOutput('pull_number', String(pullNumber));
131+
const isFork = !headRepo || headRepo.fork || headRepo.full_name !== `${context.repo.owner}/${context.repo.repo}`;
123132
core.setOutput('checkout_ref', pr.head.sha);
124133
125134
if (isFork) {
@@ -139,6 +148,8 @@ jobs:
139148
pull_number: pullNumber,
140149
per_page: 100,
141150
});
151+
// Refuse PRs that modify `.github/` because workflow files define the
152+
// automation policy this review job is enforcing.
142153
if (files.some(f => f.filename.startsWith('.github/'))) {
143154
reason = 'modifies_github_dir';
144155
}
@@ -151,24 +162,28 @@ jobs:
151162
env:
152163
CLAUDE_APP_LOGIN: ${{ vars.CLAUDE_APP_LOGIN }}
153164

154-
refuse-fork:
155-
name: Explain Fork Refusal
165+
explain-refusal:
166+
name: Explain Refusal
156167
needs: gate
157-
if: needs.gate.outputs.reason == 'fork_pr_refused' && needs.gate.outputs.actor_has_write == 'true'
168+
if: |
169+
needs.gate.outputs.actor_has_write == 'true' &&
170+
(
171+
needs.gate.outputs.reason == 'fork_pr_refused' ||
172+
needs.gate.outputs.reason == 'modifies_github_dir' ||
173+
needs.gate.outputs.reason == 'missing_claude_app_login'
174+
)
158175
runs-on: ubuntu-latest
159176
permissions:
160177
issues: write
161178
pull-requests: write
162179
steps:
163-
- name: Comment with the fork policy
180+
- name: Comment with the refusal reason
164181
uses: actions/github-script@v7
165182
with:
166183
script: |
167-
await github.rest.issues.createComment({
168-
owner: context.repo.owner,
169-
repo: context.repo.repo,
170-
issue_number: Number(${{ needs.gate.outputs.pull_number }}),
171-
body: [
184+
const reason = ${{ toJSON(needs.gate.outputs.reason) }};
185+
const messages = {
186+
fork_pr_refused: [
172187
"Claude review automation is disabled for fork pull requests.",
173188
"",
174189
"Why:",
@@ -177,7 +192,32 @@ jobs:
177192
"- there is no promotion path for forks",
178193
"",
179194
"If maintainers want Claude to implement a change, restate the task on an issue and use the issue-driven Claude workflow instead."
180-
].join("\\n"),
195+
].join("\n"),
196+
modifies_github_dir: [
197+
"Claude review automation is disabled for pull requests that modify `.github/` files.",
198+
"",
199+
"Why:",
200+
"- workflow and action files are part of the automation policy",
201+
"- this review workflow refuses to evaluate automation changes from the same PR",
202+
"",
203+
"Ask a human reviewer to inspect workflow changes directly."
204+
].join("\n"),
205+
missing_claude_app_login: [
206+
"Claude review automation is misconfigured for issue-comment triggers.",
207+
"",
208+
"Why:",
209+
"- `CLAUDE_APP_LOGIN` is not set",
210+
"- without that bot login, the review workflow cannot safely route comments on Claude-owned PRs to the write workflow",
211+
"",
212+
"Set the `CLAUDE_APP_LOGIN` Actions variable, then retry the command."
213+
].join("\n"),
214+
};
215+
216+
await github.rest.issues.createComment({
217+
owner: context.repo.owner,
218+
repo: context.repo.repo,
219+
issue_number: Number(${{ needs.gate.outputs.pull_number }}),
220+
body: messages[reason],
181221
});
182222
183223
claude:

.github/workflows/claude-write.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ jobs:
9999
checkoutRef = pr.head.sha;
100100
101101
const headRepo = pr.head.repo;
102-
const isFork = !headRepo || Boolean(headRepo.fork) || headRepo.full_name !== `${context.repo.owner}/${context.repo.repo}`;
102+
const isFork = !headRepo || headRepo.fork || headRepo.full_name !== `${context.repo.owner}/${context.repo.repo}`;
103103
if (isFork) {
104104
reason = 'fork_pr_refused';
105105
} else if (!trustedClaudeLogin) {
@@ -115,6 +115,8 @@ jobs:
115115
pull_number: context.payload.issue.number,
116116
per_page: 100,
117117
});
118+
// Refuse workflow changes so write-capable automation never acts on
119+
// `.github/` modifications from the same pull request.
118120
if (files.some(f => f.filename.startsWith('.github/'))) {
119121
reason = 'modifies_github_dir';
120122
}

0 commit comments

Comments
 (0)