-
Notifications
You must be signed in to change notification settings - Fork 66
220 lines (185 loc) · 8.21 KB
/
claude-code-pr.yml
File metadata and controls
220 lines (185 loc) · 8.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# GitHub Action: Claude Code PR Assistant
# ========================================
# PURPOSE: Custom workflow that responds to @.claude / @claude mentions in PR comments.
# Uses the Claude Code CLI directly (not the official Anthropic action).
#
# DISTINCTION from other PR workflows:
# - claude.yml: Uses official anthropics/claude-code-action for auto-review + @claude mentions
# - claude-code-review.yml: Uses official anthropics/claude-code-action for PR review only
# - claude-code-pr.yml (THIS): Custom CLI-based workflow with context extraction
#
# SECURITY: All user-controlled data is passed via env: blocks, never via ${{ }} in run: blocks.
# Ref: SECURITY-REMEDIATION-PLAN.md finding C-01
#
# Referência original: Boris Cherny GitHub Action for Claude Code
name: Claude Code PR Assistant
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
# Only run when comment contains @.claude or @claude
jobs:
claude-pr-assistant:
# Only run on PR comments containing @.claude or @claude
if: |
(github.event.issue.pull_request || github.event.pull_request) &&
(contains(github.event.comment.body, '@.claude') || contains(github.event.comment.body, '@claude'))
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
- name: Get PR details
id: pr-details
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const prNumber = context.issue?.number || context.payload.pull_request?.number;
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});
return {
title: pr.title,
body: pr.body || '',
branch: pr.head.ref,
base: pr.base.ref,
files: files.map(f => ({ filename: f.filename, status: f.status, additions: f.additions, deletions: f.deletions })),
comment: context.payload.comment.body,
comment_author: context.payload.comment.user.login
};
# SECURITY FIX (C-01): User-controlled comment content is passed via env:
# to prevent shell injection. Never use ${{ }} with user data in run: blocks.
- name: Parse Claude command
id: parse-command
env:
PR_COMMENT: ${{ fromJson(steps.pr-details.outputs.result).comment }}
run: |
# Extract command after @.claude or @claude
COMMAND=$(echo "$PR_COMMENT" | sed -n 's/.*@\.claude\s*\(.*\)/\1/p' | head -1)
if [ -z "$COMMAND" ]; then
COMMAND=$(echo "$PR_COMMENT" | sed -n 's/.*@claude\s*\(.*\)/\1/p' | head -1)
fi
# Default to "review" if no specific command
if [ -z "$COMMAND" ]; then
COMMAND="review this PR"
fi
echo "command=$COMMAND" >> $GITHUB_OUTPUT
- name: Install Claude Code CLI
run: |
npm install -g @anthropic-ai/claude-code@latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# SECURITY FIX (C-01): All PR metadata is passed via env: blocks.
- name: Run Claude analysis
id: claude-analysis
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PR_TITLE: ${{ fromJson(steps.pr-details.outputs.result).title }}
PR_BRANCH: ${{ fromJson(steps.pr-details.outputs.result).branch }}
PR_BASE: ${{ fromJson(steps.pr-details.outputs.result).base }}
PR_FILES: ${{ toJson(fromJson(steps.pr-details.outputs.result).files) }}
PR_BODY: ${{ fromJson(steps.pr-details.outputs.result).body }}
PR_COMMENT_AUTHOR: ${{ fromJson(steps.pr-details.outputs.result).comment_author }}
PR_COMMAND: ${{ steps.parse-command.outputs.command }}
run: |
# Create context file using env vars (safe from injection)
cat > /tmp/pr_context.md << CTXEOF
# PR Context
## PR: ${PR_TITLE}
**Branch:** ${PR_BRANCH} -> ${PR_BASE}
**Files Changed:**
${PR_FILES}
**PR Description:**
${PR_BODY}
## User Request
@${PR_COMMENT_AUTHOR} asked:
${PR_COMMAND}
CTXEOF
# Run Claude
RESPONSE=$(claude --print "$(cat /tmp/pr_context.md)" 2>&1 || echo "Error running Claude")
# Save response for next step
echo "$RESPONSE" > /tmp/claude_response.md
# Truncate if too long
if [ ${#RESPONSE} -gt 65000 ]; then
RESPONSE="${RESPONSE:0:65000}... (truncated)"
fi
# Set output
echo "response<<EOF" >> $GITHUB_OUTPUT
echo "$RESPONSE" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# SECURITY FIX (C-01): Response and author are read from env/outputs via JS,
# not via ${{ }} interpolation in template literals.
- name: Post response as comment
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
env:
CLAUDE_RESPONSE: ${{ steps.claude-analysis.outputs.response }}
COMMENT_AUTHOR: ${{ fromJson(steps.pr-details.outputs.result).comment_author }}
with:
script: |
const response = process.env.CLAUDE_RESPONSE || 'No response generated';
const author = process.env.COMMENT_AUTHOR || 'unknown';
const prNumber = context.issue?.number || context.payload.pull_request?.number;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: `## Claude Code Response\n\n${response}\n\n---\n*Triggered by @${author}'s comment*`
});
# SECURITY FIX (C-01): Command passed via env var
- name: Check if CLAUDE.md update requested
id: check-update
env:
PR_COMMAND: ${{ steps.parse-command.outputs.command }}
run: |
if echo "$PR_COMMAND" | grep -qi "update.*claude.md\|atualizar.*claude.md\|add.*rule\|adicionar.*regra"; then
echo "update_requested=true" >> $GITHUB_OUTPUT
else
echo "update_requested=false" >> $GITHUB_OUTPUT
fi
# SECURITY FIX (C-01): All user data via env vars in git operations
- name: Update CLAUDE.md if requested
if: steps.check-update.outputs.update_requested == 'true'
env:
PR_COMMAND: ${{ steps.parse-command.outputs.command }}
PR_COMMENT_AUTHOR: ${{ fromJson(steps.pr-details.outputs.result).comment_author }}
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
run: |
# Create a new branch for the update
git config user.name "Claude Code Bot"
git config user.email "claude-code-bot@users.noreply.github.com"
BRANCH="claude-update-$(date +%s)"
git checkout -b "$BRANCH"
# Append context to CLAUDE.md
echo "" >> CLAUDE.md
echo "## Auto-generated from PR #${PR_NUMBER}" >> CLAUDE.md
echo "" >> CLAUDE.md
echo "Request: ${PR_COMMAND}" >> CLAUDE.md
echo "" >> CLAUDE.md
git add CLAUDE.md
git commit -m "docs: Update CLAUDE.md from PR comment
Triggered by @${PR_COMMENT_AUTHOR}
PR: #${PR_NUMBER}"
git push origin "$BRANCH"
echo "Created update branch: $BRANCH"