-
-
Notifications
You must be signed in to change notification settings - Fork 440
196 lines (163 loc) · 8.87 KB
/
claude_issue_fixer.yaml
File metadata and controls
196 lines (163 loc) · 8.87 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
name: Auto Fix Issue
on:
schedule:
- cron: '0 */12 * * *' # every 12 hours
workflow_dispatch: # allow manual trigger
permissions:
contents: write
pull-requests: write
issues: read
jobs:
pick-and-fix:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
# 1. Checkout
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# 2. Install Claude Code
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
# 3. Install PHP & Composer dependencies
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
coverage: none
- name: Install Composer dependencies
uses: ramsey/composer-install@v3
# 4. Pick a random open issue from rectorphp/rector (skip any already covered by a PR)
- name: Pick a fixable issue
id: pick
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
ISSUES_REPO="rectorphp/rector"
# Fetch open issues (exclude PRs)
issues=$(gh api "repos/$ISSUES_REPO/issues?state=open&per_page=100" \
--jq '[.[] | select(.pull_request == null) | {number, title, body}]')
# Collect issue numbers already covered by an open PR in this repo
covered=$(gh api "repos/${{ github.repository }}/pulls?state=open&per_page=100" \
--jq '[.[].head.ref]' | grep -oP '(?<=issue-)\d+' | tr '\n' ' ' || true)
echo "Issues covered by open PRs: ${covered:-none}"
# Exclude covered issues and pick one at random
candidates=$(echo "$issues" | jq -c \
--argjson covered "$(echo "$covered" | jq -Rsc 'split(" ") | map(select(. != ""))')" \
'[.[] | select((.number | tostring) as $n | $covered | index($n) == null)]')
count=$(echo "$candidates" | jq 'length')
echo "Candidates: $count"
if [ "$count" -eq 0 ]; then
echo "no_issue=true" >> "$GITHUB_OUTPUT"
exit 0
fi
issue=$(echo "$candidates" | jq -c ".[$(( RANDOM % count ))]")
issue_number=$(echo "$issue" | jq -r '.number')
issue_title=$(echo "$issue" | jq -r '.title')
issue_body=$(echo "$issue" | jq -r '.body // ""')
echo "Picked issue #$issue_number — $issue_title"
branch="fix/issue-${issue_number}-$(echo "$issue_title" | \
tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | cut -c1-50 | sed 's/-$//')"
echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT"
echo "issue_title=$issue_title" >> "$GITHUB_OUTPUT"
echo "issue_body<<EOF" >> "$GITHUB_OUTPUT"
echo "$issue_body" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "branch=$branch" >> "$GITHUB_OUTPUT"
echo "no_issue=false" >> "$GITHUB_OUTPUT"
# 5. Early exit if no suitable issue was found
- name: No suitable issue found
if: steps.pick.outputs.no_issue == 'true'
run: echo "::notice::No fixable open issues found — skipping this run."
# 6. Configure git
- name: Configure git identity
if: steps.pick.outputs.no_issue == 'false'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# 7. Create fix branch
- name: Create fix branch
if: steps.pick.outputs.no_issue == 'false'
run: git checkout -b "${{ steps.pick.outputs.branch }}"
# 8. Ask Claude Code to fix the issue
- name: Run Claude Code to fix the issue
if: steps.pick.outputs.no_issue == 'false'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CLAUDE_ISSUE_NUMBER: ${{ steps.pick.outputs.issue_number }}
CLAUDE_ISSUE_TITLE: ${{ steps.pick.outputs.issue_title }}
CLAUDE_ISSUE_BODY: ${{ steps.pick.outputs.issue_body }}
run: |
claude --print --dangerously-skip-permissions \
"You are an expert software engineer. Fix the following GitHub issue in this repository.
Issue #${CLAUDE_ISSUE_NUMBER}: ${CLAUDE_ISSUE_TITLE}
Issue description:
${CLAUDE_ISSUE_BODY}
Instructions:
1. Understand the root cause of the issue deeply before making any changes.
2. Make the minimal change that fixes the issue — no unrelated refactoring.
3. Follow the existing code style, naming conventions, and formatting.
4. If the project has a test suite, add or update a test that covers this fix.
5. Run the test suite and make sure all tests pass.
6. Stage all changed files with 'git add'.
7. Commit with the message: fix: <short description> (fixes #${CLAUDE_ISSUE_NUMBER})
8. Do NOT push — only commit locally.
9. If you cannot confidently fix this issue without breaking anything, output exactly:
CANNOT_FIX: <reason>
and make no changes."
# 9. Verify something was committed
- name: Check for new commits
if: steps.pick.outputs.no_issue == 'false'
id: check_commit
run: |
ahead=$(git rev-list --count origin/HEAD..HEAD 2>/dev/null || echo 0)
echo "commits_ahead=$ahead" >> "$GITHUB_OUTPUT"
if [ "$ahead" -eq 0 ]; then
echo "::warning::Claude Code made no commits — issue may not be fixable automatically."
fi
# 10. Push branch and open PR
- name: Push branch and open PR
if: >
steps.pick.outputs.no_issue == 'false' &&
steps.check_commit.outputs.commits_ahead != '0'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: ${{ steps.pick.outputs.branch }}
ISSUE_NUMBER: ${{ steps.pick.outputs.issue_number }}
ISSUE_TITLE: ${{ steps.pick.outputs.issue_title }}
run: |
git push origin "$BRANCH"
gh pr create \
--title "fix: $ISSUE_TITLE (fixes rectorphp/rector#$ISSUE_NUMBER)" \
--body "## Summary
Fixes rectorphp/rector#${ISSUE_NUMBER} — ${ISSUE_TITLE}
## Root cause
See the commit message and diff for details of what was changed and why.
## Testing
- Test suite run inside the workflow — see CI logs for details.
> Opened automatically by the [Auto Fix Issue](.github/workflows/auto-fix-issue.yml) workflow. Please review carefully before merging." \
--head "$BRANCH" \
--label "auto-fix"
# 11. Summary
- name: Write job summary
if: always()
env:
NO_ISSUE: ${{ steps.pick.outputs.no_issue }}
ISSUE_NUMBER: ${{ steps.pick.outputs.issue_number }}
ISSUE_TITLE: ${{ steps.pick.outputs.issue_title }}
BRANCH: ${{ steps.pick.outputs.branch }}
COMMITS_AHEAD: ${{ steps.check_commit.outputs.commits_ahead }}
run: |
if [ "$NO_ISSUE" = "true" ]; then
echo "## ✅ No candidates found" >> "$GITHUB_STEP_SUMMARY"
echo "No open issues without an existing PR were found." >> "$GITHUB_STEP_SUMMARY"
elif [ "${COMMITS_AHEAD:-0}" = "0" ]; then
echo "## ⚠️ Could not fix issue #${ISSUE_NUMBER}" >> "$GITHUB_STEP_SUMMARY"
echo "**${ISSUE_TITLE}** — Claude Code was unable to produce a commit." >> "$GITHUB_STEP_SUMMARY"
else
echo "## 🚀 PR opened for issue #${ISSUE_NUMBER}" >> "$GITHUB_STEP_SUMMARY"
echo "**${ISSUE_TITLE}** — branch: \`${BRANCH}\`" >> "$GITHUB_STEP_SUMMARY"
fi