-
Notifications
You must be signed in to change notification settings - Fork 0
387 lines (329 loc) · 15.3 KB
/
create-pr.yml
File metadata and controls
387 lines (329 loc) · 15.3 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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
name: Create Claude PR
on:
workflow_dispatch:
inputs:
source_branch:
description: "Source branch to create PR from (defaults to current branch)"
required: false
type: string
target_branch:
description: "Target branch to merge into"
required: true
type: choice
options:
- main
default: main
pr_type:
description: "Type of PR"
required: true
type: choice
options:
- release
- feature
- bugfix
- hotfix
default: release
claude_review:
description: "Request Claude review automatically"
required: false
type: boolean
default: true
jobs:
create-pr:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
# ACTIONS_TOKEN (PAT) preferred - PRs created with github.token won't trigger CI workflows
GH_TOKEN: ${{ secrets.ACTIONS_TOKEN || github.token }}
steps:
- name: Check token configuration
run: |
if [ -z "${{ secrets.ACTIONS_TOKEN }}" ]; then
echo "::warning::ACTIONS_TOKEN not set - using github.token as fallback"
echo ""
echo "⚠️ PRs created with github.token have limitations:"
echo " - Won't trigger on:pull_request workflows (CI won't run automatically)"
echo " - May be blocked by branch protection rules"
echo ""
echo "To enable full functionality, set ACTIONS_TOKEN secret:"
echo " gh secret set ACTIONS_TOKEN"
else
echo "✅ Using ACTIONS_TOKEN for full PR functionality"
fi
- name: Checkout
uses: actions/checkout@v4
with:
# ACTIONS_TOKEN preferred for pushing to protected branches
token: ${{ secrets.ACTIONS_TOKEN || github.token }}
fetch-depth: 0
- name: Determine source branch
id: source-branch
run: |
if [ -n "${{ inputs.source_branch }}" ]; then
SOURCE_BRANCH="${{ inputs.source_branch }}"
echo "Using specified source branch: $SOURCE_BRANCH"
git checkout "$SOURCE_BRANCH"
else
SOURCE_BRANCH=$(git branch --show-current)
echo "Using current branch: $SOURCE_BRANCH"
fi
echo "source_branch=$SOURCE_BRANCH" >> $GITHUB_OUTPUT
- name: Validate branches
id: validate
run: |
SOURCE_BRANCH="${{ steps.source-branch.outputs.source_branch }}"
TARGET_BRANCH="${{ inputs.target_branch }}"
echo "Source branch: $SOURCE_BRANCH"
echo "Target branch: $TARGET_BRANCH"
# Check if source branch exists
if ! git show-ref --verify --quiet refs/heads/$SOURCE_BRANCH && ! git show-ref --verify --quiet refs/remotes/origin/$SOURCE_BRANCH; then
echo "❌ Source branch $SOURCE_BRANCH does not exist"
exit 1
fi
# Check if target branch exists
if ! git show-ref --verify --quiet refs/heads/$TARGET_BRANCH && ! git show-ref --verify --quiet refs/remotes/origin/$TARGET_BRANCH; then
echo "❌ Target branch $TARGET_BRANCH does not exist"
exit 1
fi
# Check if branches are different
if [ "$SOURCE_BRANCH" = "$TARGET_BRANCH" ]; then
echo "❌ Source and target branches cannot be the same"
exit 1
fi
echo "✅ Branch validation passed"
- name: Analyze changes with Claude
id: analyze
run: |
SOURCE_BRANCH="${{ steps.source-branch.outputs.source_branch }}"
TARGET_BRANCH="${{ inputs.target_branch }}"
PR_TYPE="${{ inputs.pr_type }}"
# Get commit range for analysis
git fetch origin $TARGET_BRANCH
COMMIT_RANGE="origin/$TARGET_BRANCH...$SOURCE_BRANCH"
# Get diff stats (limit output to prevent memory issues)
DIFF_STATS=$(git diff --stat --no-color $COMMIT_RANGE | head -50 | sed 's/`//g') # Limit to 50 lines
FILES_CHANGED=$(git diff --name-only $COMMIT_RANGE | wc -l)
COMMITS_COUNT=$(git rev-list --count $COMMIT_RANGE)
# Get commit messages (limit to recent commits)
COMMIT_MESSAGES=$(git log --oneline --no-color $COMMIT_RANGE | head -20 | sed 's/`//g') # Limit to 20 commits
# Get detailed changes (limit to prevent memory issues)
DETAILED_CHANGES=$(git diff $COMMIT_RANGE --name-status --no-color | head -100 | sed 's/`//g') # Limit to 100 files
# Create analysis prompt for Claude
cat << 'EOF' > /tmp/pr_analysis_prompt.txt
I need you to analyze the following git changes and create a comprehensive PR title and description.
**Context:**
- Source Branch: ${SOURCE_BRANCH}
- Target Branch: ${TARGET_BRANCH}
- PR Type: ${PR_TYPE}
- Files Changed: ${FILES_CHANGED}
- Commits: ${COMMITS_COUNT}
**Commit Messages:**
${COMMIT_MESSAGES}
**Diff Statistics:**
${DIFF_STATS}
**Detailed Changes:**
${DETAILED_CHANGES}
Please provide:
1. A concise, descriptive PR title (50-72 characters)
2. A comprehensive PR description with:
- Summary of changes
- Key accomplishments
- Breaking changes (if any)
- Testing notes
- Infrastructure considerations (avoid mentioning specific script paths or commands)
Format the response as:
TITLE: [your title here]
DESCRIPTION:
[your description here]
EOF
# Create the final prompt by writing directly to a file (avoiding envsubst issues)
cat > /tmp/pr_analysis_prompt_final.txt << PROMPT_EOF
I need you to analyze the following git changes and create a comprehensive PR title and description.
**Context:**
- Source Branch: $SOURCE_BRANCH
- Target Branch: $TARGET_BRANCH
- PR Type: $PR_TYPE
- Files Changed: $FILES_CHANGED
- Commits: $COMMITS_COUNT
**Commit Messages:**
$COMMIT_MESSAGES
**Diff Statistics:**
$DIFF_STATS
**Detailed Changes:**
$DETAILED_CHANGES
Please provide:
1. A concise, descriptive PR title (50-72 characters)
2. A comprehensive PR description with:
- Summary of changes
- Key accomplishments
- Breaking changes (if any)
- Testing notes
- Infrastructure considerations (avoid mentioning specific script paths or commands)
Format the response as:
TITLE: [your title here]
DESCRIPTION:
[your description here]
PROMPT_EOF
mv /tmp/pr_analysis_prompt_final.txt /tmp/pr_analysis_prompt.txt
echo "analysis_prompt_file=/tmp/pr_analysis_prompt.txt" >> $GITHUB_OUTPUT
echo "commit_range=$COMMIT_RANGE" >> $GITHUB_OUTPUT
echo "files_changed=$FILES_CHANGED" >> $GITHUB_OUTPUT
echo "commits_count=$COMMITS_COUNT" >> $GITHUB_OUTPUT
- name: Call Claude API
id: claude
run: |
# Call Claude API with the analysis prompt
PROMPT=$(cat /tmp/pr_analysis_prompt.txt)
# Create a proper JSON payload using jq to handle escaping
PAYLOAD=$(jq -n \
--arg model "${{ vars.CLAUDE_MODEL || 'claude-sonnet-4-6' }}" \
--arg content "$PROMPT" \
'{
"model": $model,
"max_tokens": 4000,
"messages": [
{
"role": "user",
"content": $content
}
]
}')
# Retry logic with exponential backoff
MAX_RETRIES=3
RETRY_COUNT=0
RETRY_DELAY=5
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
RESPONSE=$(curl -s -X POST "https://api.anthropic.com/v1/messages" \
-H "Content-Type: application/json" \
-H "x-api-key: ${{ secrets.ANTHROPIC_API_KEY }}" \
-H "anthropic-version: 2023-06-01" \
-d "$PAYLOAD")
# Check for API errors
if echo "$RESPONSE" | jq -e '.error' > /dev/null; then
ERROR_TYPE=$(echo "$RESPONSE" | jq -r '.error.type // "unknown"')
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // "Unknown error"')
# Check if it's a rate limit or overload error
if [[ "$ERROR_TYPE" == "overloaded_error" ]] || [[ "$ERROR_TYPE" == "rate_limit_error" ]] || [[ "$ERROR_MSG" == *"Overloaded"* ]]; then
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "⚠️ Claude API overloaded (attempt $RETRY_COUNT/$MAX_RETRIES). Retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
RETRY_DELAY=$((RETRY_DELAY * 2)) # Exponential backoff
continue
else
echo "❌ Claude API error after $MAX_RETRIES attempts: $ERROR_MSG"
echo "Using fallback PR description..."
# Set a fallback description
PR_TITLE="PR: ${{ steps.source-branch.outputs.source_branch }} → ${{ inputs.target_branch }}"
PR_DESCRIPTION="Automated PR from ${{ steps.source-branch.outputs.source_branch }} to ${{ inputs.target_branch }}"$'\n\n'"**Type:** ${{ inputs.pr_type }}"$'\n'"**Files Changed:** ${{ steps.analyze.outputs.files_changed }}"$'\n'"**Commits:** ${{ steps.analyze.outputs.commits_count }}"$'\n\n'"Please review the changes in the Files tab."
break
fi
else
echo "❌ Claude API error: $ERROR_MSG"
exit 1
fi
else
# Success - break out of retry loop
break
fi
done
# Only process Claude response if we didn't use fallback
if [ -z "$PR_TITLE" ]; then
# Check for successful response
if echo "$RESPONSE" | jq -e '.content[0].text' > /dev/null; then
# Extract title and description from Claude's response
CLAUDE_OUTPUT=$(echo "$RESPONSE" | jq -r '.content[0].text')
# Parse the title and description
PR_TITLE=$(echo "$CLAUDE_OUTPUT" | grep "^TITLE:" | sed 's/^TITLE: //')
PR_DESCRIPTION=$(echo "$CLAUDE_OUTPUT" | sed -n '/^DESCRIPTION:/,$ p' | sed '1d')
# Validate that Claude returned meaningful content
if [ -z "$PR_TITLE" ] || [ -z "$PR_DESCRIPTION" ]; then
echo "⚠️ Claude returned empty title or description, using fallback"
PR_TITLE="PR: ${{ steps.source-branch.outputs.source_branch }} → ${{ inputs.target_branch }}"
PR_DESCRIPTION="Automated PR from ${{ steps.source-branch.outputs.source_branch }} to ${{ inputs.target_branch }}"
PR_DESCRIPTION="${PR_DESCRIPTION}"$'\n\n'"**Type:** ${{ inputs.pr_type }}"
PR_DESCRIPTION="${PR_DESCRIPTION}"$'\n'"**Files Changed:** ${{ steps.analyze.outputs.files_changed }}"
PR_DESCRIPTION="${PR_DESCRIPTION}"$'\n'"**Commits:** ${{ steps.analyze.outputs.commits_count }}"
PR_DESCRIPTION="${PR_DESCRIPTION}"$'\n\n'"Please review the changes in the Files tab."
fi
else
echo "⚠️ Invalid Claude response format, using fallback"
PR_TITLE="PR: ${{ steps.source-branch.outputs.source_branch }} → ${{ inputs.target_branch }}"
PR_DESCRIPTION="Automated PR from ${{ steps.source-branch.outputs.source_branch }} to ${{ inputs.target_branch }}"
PR_DESCRIPTION="${PR_DESCRIPTION}"$'\n\n'"**Type:** ${{ inputs.pr_type }}"
PR_DESCRIPTION="${PR_DESCRIPTION}"$'\n\n'"Please review the changes in the Files tab."
fi
fi
# Save to outputs (escape for JSON)
{
echo "pr_title<<EOF"
echo "$PR_TITLE"
echo "EOF"
} >> $GITHUB_OUTPUT
{
echo "pr_description<<EOF"
echo "$PR_DESCRIPTION"
echo "EOF"
} >> $GITHUB_OUTPUT
echo "✅ Claude analysis completed"
- name: Create Pull Request
id: create-pr
env:
CLAUDE_PR_TITLE: ${{ steps.claude.outputs.pr_title }}
CLAUDE_PR_DESCRIPTION: ${{ steps.claude.outputs.pr_description }}
run: |
SOURCE_BRANCH="${{ steps.source-branch.outputs.source_branch }}"
TARGET_BRANCH="${{ inputs.target_branch }}"
# Use environment variables to safely handle Claude's output
PR_TITLE="$CLAUDE_PR_TITLE"
PR_DESCRIPTION="$CLAUDE_PR_DESCRIPTION"
# Check if PR already exists
EXISTING_PR=$(gh pr list --head "$SOURCE_BRANCH" --base "$TARGET_BRANCH" --json url --jq '.[0].url' 2>/dev/null || echo "")
if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then
echo "✅ PR already exists: $EXISTING_PR"
echo "pr_url=$EXISTING_PR" >> $GITHUB_OUTPUT
PR_URL="$EXISTING_PR"
else
# Create PR description with Claude analysis + footer
cat > /tmp/pr_description.txt << EOF
$PR_DESCRIPTION
---
🤖 Generated with [Claude Code](https://claude.ai/code)
**Branch Info:**
- Source: \`$SOURCE_BRANCH\`
- Target: \`$TARGET_BRANCH\`
- Type: ${{ inputs.pr_type }}
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
FINAL_PR_DESCRIPTION=$(cat /tmp/pr_description.txt)
# Create the PR
PR_URL=$(gh pr create \
--title "$PR_TITLE" \
--body "$FINAL_PR_DESCRIPTION" \
--base "$TARGET_BRANCH" \
--head "$SOURCE_BRANCH")
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
echo "✅ Pull Request created: $PR_URL"
fi
- name: Request Claude review
if: ${{ inputs.claude_review }}
run: |
PR_NUMBER=$(echo "${{ steps.create-pr.outputs.pr_url }}" | sed 's/.*pull\///')
gh pr comment "$PR_NUMBER" --body "@claude please review this PR"
echo "✅ Claude review requested"
- name: Create summary
run: |
SOURCE_BRANCH="${{ steps.source-branch.outputs.source_branch }}"
TARGET_BRANCH="${{ inputs.target_branch }}"
PR_URL="${{ steps.create-pr.outputs.pr_url }}"
echo "## 🚀 Claude-Powered PR Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**PR URL:** [$PR_URL]($PR_URL)" >> $GITHUB_STEP_SUMMARY
echo "**Source:** \`$SOURCE_BRANCH\`" >> $GITHUB_STEP_SUMMARY
echo "**Target:** \`$TARGET_BRANCH\`" >> $GITHUB_STEP_SUMMARY
echo "**Type:** ${{ inputs.pr_type }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### PR Details" >> $GITHUB_STEP_SUMMARY
echo "**Title:** ${{ steps.claude.outputs.pr_title }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commits:** ${{ steps.analyze.outputs.commit_range }}" >> $GITHUB_STEP_SUMMARY