Skip to content

Commit 77547d1

Browse files
ajstormclaude
andcommitted
dev-inf: add GitHub Actions workflows for issue auto-solving
Add two new GitHub Actions workflows to enable automated issue resolution using Claude Code: 1. issue-autosolve.yml - Triggered when an issue is labeled c-autosolve: - Assesses issue feasibility using Claude Opus - Implements fix with tests if suitable - Creates a cross-repo draft PR with o-autosolver label - Comments on the issue with results 2. pr-autosolve-comments.yml - Triggered when comments are left on PRs with the o-autosolver label: - Fetches all review comments - Uses Claude to address feedback - Amends commit and force-pushes - Posts summary with loop-prevention marker Both workflows use Vertex AI authentication and the Claude Opus model. Release note: None Epic: None Generated with Claude Code Co-Authored-By: Claude <[email protected]>
1 parent b479177 commit 77547d1

File tree

2 files changed

+540
-0
lines changed

2 files changed

+540
-0
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
name: Issue Auto-Solver
2+
3+
on:
4+
issues:
5+
types: [labeled]
6+
7+
concurrency:
8+
group: autosolve-issue-${{ github.event.issue.number }}
9+
cancel-in-progress: true
10+
11+
jobs:
12+
auto-solve-issue:
13+
runs-on: ubuntu-latest
14+
timeout-minutes: 60
15+
if: github.event.label.name == 'c-autosolve'
16+
permissions:
17+
contents: write
18+
pull-requests: write
19+
issues: write
20+
id-token: write
21+
22+
steps:
23+
- name: Validate required secrets
24+
run: |
25+
if [ -z "${{ secrets.AUTOSOLVER_PAT }}" ]; then
26+
echo "::error::AUTOSOLVER_PAT secret is not configured"
27+
exit 1
28+
fi
29+
30+
- name: Checkout repository
31+
uses: actions/checkout@v5
32+
with:
33+
fetch-depth: 0
34+
35+
- name: Authenticate to Google Cloud
36+
uses: 'google-github-actions/auth@v3'
37+
with:
38+
project_id: 'vertex-model-runners'
39+
service_account: '[email protected]'
40+
workload_identity_provider: 'projects/72497726731/locations/global/workloadIdentityPools/ai-review/providers/ai-review'
41+
42+
- name: Stage 1 - Assess Issue Feasibility
43+
id: assess
44+
uses: cockroachdb/claude-code-action@v1
45+
env:
46+
ANTHROPIC_VERTEX_PROJECT_ID: vertex-model-runners
47+
CLOUD_ML_REGION: us-east5
48+
# Pass user-controlled content via env vars to prevent prompt injection
49+
ISSUE_TITLE: ${{ github.event.issue.title }}
50+
ISSUE_BODY: ${{ github.event.issue.body }}
51+
with:
52+
github_token: ${{ secrets.GITHUB_TOKEN }}
53+
use_vertex: "true"
54+
claude_args: |
55+
--model claude-opus-4-5@20251101
56+
--allowedTools "Read,Grep,Glob,Bash(gh issue view:*)"
57+
prompt: |
58+
Assess GitHub issue #${{ github.event.issue.number }}
59+
60+
The issue title and body are provided in the ISSUE_TITLE and ISSUE_BODY environment variables.
61+
Use `gh issue view ${{ github.event.issue.number }}` to read the full issue details.
62+
63+
Determine if this issue is suitable for automated one-shot resolution.
64+
65+
Criteria for PROCEED:
66+
- Clear bug description (reproduction steps or description of how to reproduce)
67+
- Single component affected
68+
- No architectural changes required
69+
70+
Criteria for SKIP:
71+
- Requires design decisions or RFC
72+
- Affects multiple major components
73+
- Requires human judgment on product direction
74+
75+
**OUTPUT REQUIREMENT**: End your response with a single line containing only:
76+
- `ASSESSMENT_RESULT - PROCEED` or
77+
- `ASSESSMENT_RESULT - SKIP`
78+
79+
- name: Extract Assessment Result
80+
id: assess_result
81+
if: steps.assess.conclusion == 'success'
82+
run: |
83+
if [ ! -f "${{ steps.assess.outputs.execution_file }}" ]; then
84+
echo "::error::Execution file not found: ${{ steps.assess.outputs.execution_file }}"
85+
exit 1
86+
fi
87+
88+
RESULT=$(jq -r '.[] | select(.type == "result") | .result' "${{ steps.assess.outputs.execution_file }}") || {
89+
echo "::error::Failed to parse execution file with jq"
90+
exit 1
91+
}
92+
93+
if [ -z "$RESULT" ]; then
94+
echo "::error::No result found in execution file"
95+
exit 1
96+
fi
97+
98+
{
99+
echo 'result<<EOF'
100+
echo "$RESULT"
101+
echo 'EOF'
102+
} >> "$GITHUB_OUTPUT"
103+
echo "Assessment result extracted (${#RESULT} characters)"
104+
105+
# Validate that the result contains a valid assessment marker
106+
if ! echo "$RESULT" | grep -qE 'ASSESSMENT_RESULT - (PROCEED|SKIP)'; then
107+
echo "::error::Assessment result does not contain valid ASSESSMENT_RESULT marker"
108+
echo "Expected 'ASSESSMENT_RESULT - PROCEED' or 'ASSESSMENT_RESULT - SKIP'"
109+
exit 1
110+
fi
111+
112+
- name: Stage 2 - Implement Fix
113+
id: implement
114+
if: contains(steps.assess_result.outputs.result, 'ASSESSMENT_RESULT - PROCEED')
115+
uses: cockroachdb/claude-code-action@v1
116+
env:
117+
ANTHROPIC_VERTEX_PROJECT_ID: vertex-model-runners
118+
CLOUD_ML_REGION: us-east5
119+
# Pass user-controlled content via env vars to prevent prompt injection
120+
ISSUE_TITLE: ${{ github.event.issue.title }}
121+
ISSUE_BODY: ${{ github.event.issue.body }}
122+
with:
123+
github_token: ${{ secrets.GITHUB_TOKEN }}
124+
use_vertex: "true"
125+
claude_args: |
126+
--model claude-opus-4-5@20251101
127+
--allowedTools "Read,Write,Edit,Grep,Glob,Bash(gh issue view:*),Bash(./dev test:*),Bash(./dev testlogic:*),Bash(./dev build:*),Bash(./dev generate:*),Bash(git:*)"
128+
prompt: |
129+
Fix GitHub issue #${{ github.event.issue.number }}
130+
131+
The issue title and body are provided in the ISSUE_TITLE and ISSUE_BODY environment variables.
132+
Use `gh issue view ${{ github.event.issue.number }}` or read the env vars to understand the issue.
133+
134+
Instructions:
135+
1. Read CLAUDE.md for project conventions and commit message format
136+
2. Read and understand the issue
137+
3. Implement the minimal fix required
138+
4. Add or update tests to verify the fix
139+
5. Run tests with ./dev test or ./dev testlogic
140+
6. Stage all changes with git add
141+
142+
When formatting commits and PRs, follow the guidelines in CLAUDE.md.
143+
144+
**OUTPUT REQUIREMENT**: End your response with a single line containing only:
145+
- `IMPLEMENTATION_RESULT - SUCCESS` or
146+
- `IMPLEMENTATION_RESULT - FAILED`
147+
148+
- name: Extract Implementation Result
149+
id: implement_result
150+
if: steps.implement.conclusion == 'success'
151+
run: |
152+
if [ ! -f "${{ steps.implement.outputs.execution_file }}" ]; then
153+
echo "::error::Execution file not found: ${{ steps.implement.outputs.execution_file }}"
154+
exit 1
155+
fi
156+
157+
RESULT=$(jq -r '.[] | select(.type == "result") | .result' "${{ steps.implement.outputs.execution_file }}") || {
158+
echo "::error::Failed to parse execution file with jq"
159+
exit 1
160+
}
161+
162+
if [ -z "$RESULT" ]; then
163+
echo "::error::No result found in execution file"
164+
exit 1
165+
fi
166+
167+
{
168+
echo 'result<<EOF'
169+
echo "$RESULT"
170+
echo 'EOF'
171+
} >> "$GITHUB_OUTPUT"
172+
echo "Implementation result extracted (${#RESULT} characters)"
173+
174+
- name: Create branch and push to fork
175+
id: push
176+
if: contains(steps.implement_result.outputs.result, 'IMPLEMENTATION_RESULT - SUCCESS')
177+
env:
178+
AUTOSOLVER_PAT: ${{ secrets.AUTOSOLVER_PAT }}
179+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
180+
run: |
181+
git config user.name "crdb-autosolver"
182+
git config user.email "[email protected]"
183+
184+
# Configure git credential helper to use PAT for the fork (avoids exposing token in URLs)
185+
git config --global credential.helper store
186+
echo "https://crdb-autosolver:${AUTOSOLVER_PAT}@github.com" > ~/.git-credentials
187+
chmod 600 ~/.git-credentials
188+
189+
# Add the fork as a remote (handle case where it already exists)
190+
git remote add fork https://github.com/crdb-autosolver/cockroach.git 2>/dev/null || \
191+
git remote set-url fork https://github.com/crdb-autosolver/cockroach.git
192+
193+
# Create branch first, then add files
194+
BRANCH_NAME="fix/issue-${{ github.event.issue.number }}"
195+
git checkout -b "$BRANCH_NAME"
196+
git add -A
197+
198+
# Check if there are any staged changes to commit
199+
if git diff --quiet --cached; then
200+
echo "::error::No changes were staged by the implementation step"
201+
exit 1
202+
fi
203+
204+
# Get issue title for commit message and sanitize it
205+
# Remove newlines and limit length to prevent commit message issues
206+
ISSUE_TITLE=$(gh issue view ${{ github.event.issue.number }} --json title -q '.title' 2>/dev/null || echo "fix issue #${{ github.event.issue.number }}")
207+
# Sanitize: remove newlines, backticks, and limit to 100 chars for commit title
208+
ISSUE_TITLE=$(echo "$ISSUE_TITLE" | tr '\n\r' ' ' | tr '`' "'" | cut -c1-100)
209+
210+
# Get the first package modified for commit prefix (use --cached since files are now staged)
211+
PREFIX=$(git diff --name-only --cached 2>/dev/null | grep '\.go$' | head -1 | sed 's|pkg/||' | cut -d'/' -f1)
212+
if [ -z "$PREFIX" ]; then
213+
PREFIX="*"
214+
fi
215+
216+
# Create commit with proper formatting per CLAUDE.md
217+
# Using quoted heredoc ('EOF') to prevent variable expansion issues
218+
git commit -F - <<EOF
219+
${PREFIX}: ${ISSUE_TITLE}
220+
221+
Fixes #${{ github.event.issue.number }}
222+
223+
Release note: None
224+
225+
Epic: None
226+
227+
Generated by Claude Code Auto-Solver
228+
Co-Authored-By: Claude <[email protected]>
229+
EOF
230+
231+
# Push to the fork
232+
# NOTE: Force push is safe here because we're pushing to a new branch on the bot's fork,
233+
# not to a shared branch. This ensures a clean branch state for each issue attempt.
234+
git push -u fork "$BRANCH_NAME" --force
235+
236+
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
237+
238+
- name: Create PR
239+
id: create_pr
240+
if: steps.push.conclusion == 'success'
241+
env:
242+
GH_TOKEN: ${{ secrets.AUTOSOLVER_PAT }}
243+
run: |
244+
# Get issue title and sanitize for safe use in PR body
245+
ISSUE_TITLE=$(gh issue view ${{ github.event.issue.number }} --repo ${{ github.repository }} --json title -q '.title' 2>/dev/null || echo "Issue #${{ github.event.issue.number }}")
246+
# Sanitize: remove newlines and backticks
247+
ISSUE_TITLE=$(echo "$ISSUE_TITLE" | tr '\n\r' ' ' | tr '`' "'")
248+
249+
# Get commit stats (already safe as git output)
250+
STATS=$(git diff --stat HEAD~1..HEAD 2>/dev/null || echo "No stats available")
251+
252+
# Get commit title
253+
COMMIT_TITLE=$(git log -1 --pretty=%s)
254+
255+
# Create PR body using heredoc for safe variable interpolation
256+
PR_BODY=$(cat <<'PREOF'
257+
Fixes #${{ github.event.issue.number }}
258+
259+
## Summary
260+
261+
This PR fixes **ISSUE_TITLE_PLACEHOLDER**.
262+
263+
### Changes Made
264+
265+
```
266+
STATS_PLACEHOLDER
267+
```
268+
269+
## Test Plan
270+
271+
- [x] Tests pass locally
272+
273+
---
274+
275+
*This PR was auto-generated by [crdb-issue-autosolver](https://github.com/ajstorm/crdb-issue-autosolver) using Claude Code.*
276+
*Please review carefully before approving.*
277+
PREOF
278+
)
279+
280+
# Substitute placeholders with actual values (using | as delimiter to avoid issues with /)
281+
PR_BODY=$(echo "$PR_BODY" | sed "s|ISSUE_TITLE_PLACEHOLDER|${ISSUE_TITLE}|g")
282+
PR_BODY=$(echo "$PR_BODY" | sed "s|STATS_PLACEHOLDER|${STATS}|g")
283+
284+
# Create the PR from fork to upstream
285+
PR_URL=$(gh pr create \
286+
--repo ${{ github.repository }} \
287+
--head crdb-autosolver:${{ steps.push.outputs.branch_name }} \
288+
--base master \
289+
--draft \
290+
--title "$COMMIT_TITLE" \
291+
--body "$PR_BODY" \
292+
--label "o-autosolver")
293+
294+
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
295+
echo "Created PR: $PR_URL"
296+
297+
- name: Comment on issue - Success
298+
if: steps.create_pr.conclusion == 'success'
299+
env:
300+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
301+
run: |
302+
gh issue comment ${{ github.event.issue.number }} --body \
303+
"Auto-solver has created a draft PR to address this issue: ${{ steps.create_pr.outputs.pr_url }}
304+
305+
Please review the changes carefully before approving."
306+
307+
- name: Comment on issue - Skipped
308+
if: contains(steps.assess_result.outputs.result, 'ASSESSMENT_RESULT - SKIP')
309+
env:
310+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
311+
run: |
312+
gh issue comment ${{ github.event.issue.number }} --body \
313+
"Auto-solver assessed this issue but determined it is not suitable for automated resolution.
314+
315+
**Assessment:**
316+
${{ steps.assess_result.outputs.result }}
317+
318+
This issue may require human intervention due to complexity, architectural considerations, or ambiguity."
319+
320+
- name: Comment on issue - Failed
321+
if: |
322+
(steps.implement.conclusion == 'failure' ||
323+
contains(steps.implement_result.outputs.result, 'IMPLEMENTATION_RESULT - FAILED')) &&
324+
!contains(steps.assess_result.outputs.result, 'ASSESSMENT_RESULT - SKIP')
325+
env:
326+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
327+
run: |
328+
gh issue comment ${{ github.event.issue.number }} --body \
329+
"Auto-solver attempted to fix this issue but was unable to complete the implementation.
330+
331+
This issue may require human intervention."
332+
333+
- name: Remove c-autosolve label
334+
if: always()
335+
env:
336+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
337+
run: |
338+
gh issue edit ${{ github.event.issue.number }} --remove-label "c-autosolve" || true
339+
340+
- name: Cleanup credentials
341+
if: always()
342+
run: |
343+
# Remove credentials file to prevent potential exposure in artifacts/logs
344+
rm -f ~/.git-credentials
345+
git config --global --unset credential.helper || true

0 commit comments

Comments
 (0)