Skip to content

Commit d96e9ab

Browse files
Add github-issue-autosolve reusable workflow
Turnkey reusable workflow for GitHub Issues integration that composes the autosolve/assess and autosolve/implement actions with: - Automatic issue comments for status updates (PR created, skipped, failed, already exists) - Label-based triggering with automatic label removal - Concurrency control per issue number - Step summaries for skipped/existing-PR cases - Token usage summary appended to step summary - Side-by-side checkout layout isolating credentials from Claude - Vertex AI and API key authentication modes Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
1 parent e3e7c9c commit d96e9ab

3 files changed

Lines changed: 389 additions & 0 deletions

File tree

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
name: GitHub Issue Autosolve
2+
on:
3+
workflow_call:
4+
inputs:
5+
issue_number:
6+
type: string
7+
required: true
8+
issue_title:
9+
type: string
10+
required: true
11+
issue_body:
12+
type: string
13+
required: true
14+
trigger_label:
15+
type: string
16+
required: false
17+
default: "autosolve"
18+
prompt:
19+
type: string
20+
required: false
21+
default: ""
22+
skill:
23+
type: string
24+
required: false
25+
default: ""
26+
additional_instructions:
27+
type: string
28+
required: false
29+
default: ""
30+
allowed_tools:
31+
type: string
32+
required: false
33+
claude_cli_version:
34+
type: string
35+
required: false
36+
default: "2.1.79"
37+
description: "Claude CLI version to install (e.g. '2.1.79' or 'latest')"
38+
model:
39+
type: string
40+
required: false
41+
default: "claude-opus-4-6"
42+
max_retries:
43+
type: string
44+
required: false
45+
default: "3"
46+
auth_mode:
47+
type: string
48+
required: false
49+
default: "vertex"
50+
vertex_project_id:
51+
type: string
52+
required: false
53+
default: ""
54+
vertex_region:
55+
type: string
56+
required: false
57+
default: "us-east5"
58+
vertex_workload_identity_provider:
59+
type: string
60+
required: false
61+
default: ""
62+
vertex_service_account:
63+
type: string
64+
required: false
65+
default: ""
66+
fork_owner:
67+
type: string
68+
required: true
69+
fork_repo:
70+
type: string
71+
required: true
72+
blocked_paths:
73+
type: string
74+
required: false
75+
default: ".github/workflows/"
76+
git_user_name:
77+
type: string
78+
required: false
79+
default: "autosolve[bot]"
80+
git_user_email:
81+
type: string
82+
required: false
83+
default: "autosolve[bot]@users.noreply.github.com"
84+
timeout_minutes:
85+
type: number
86+
required: false
87+
default: 20
88+
secrets:
89+
repo_token:
90+
required: true
91+
fork_push_token:
92+
required: true
93+
pr_create_token:
94+
required: true
95+
anthropic_api_key:
96+
required: false
97+
outputs:
98+
status:
99+
value: ${{ jobs.solve.outputs.status }}
100+
pr_url:
101+
value: ${{ jobs.solve.outputs.pr_url }}
102+
103+
concurrency:
104+
group: autosolve-issue-${{ inputs.issue_number }}
105+
cancel-in-progress: false
106+
107+
env:
108+
# Directory name for the target repo checkout (side-by-side layout).
109+
REPO_DIR: repo
110+
111+
jobs:
112+
solve:
113+
runs-on: ubuntu-latest
114+
timeout-minutes: ${{ inputs.timeout_minutes }}
115+
permissions:
116+
contents: read
117+
issues: write
118+
id-token: write
119+
defaults:
120+
run:
121+
working-directory: ${{ env.REPO_DIR }}
122+
outputs:
123+
status: ${{ steps.final_status.outputs.status }}
124+
pr_url: ${{ steps.implement.outputs.pr_url || steps.check.outputs.pr_url }}
125+
126+
steps:
127+
# Resolve the ref the caller used to invoke this reusable workflow,
128+
# then checkout the actions repo at that ref so ./autosolve/* actions
129+
# are available via relative uses paths.
130+
- name: Get workflow ref
131+
id: actions-ref
132+
uses: cockroachdb/actions/get-workflow-ref@pr/get-workflow-ref
133+
with:
134+
workflow_name: github-issue-autosolve.yml
135+
136+
- name: Checkout actions repo
137+
uses: actions/checkout@v5
138+
with:
139+
repository: cockroachdb/actions
140+
ref: ${{ steps.actions-ref.outputs.ref }}
141+
persist-credentials: false
142+
143+
# Target repo is checked out to repo/ (via defaults.run.working-directory)
144+
# so Claude cannot see or modify the actions checkout.
145+
- name: Checkout target repo
146+
uses: actions/checkout@v5
147+
with:
148+
fetch-depth: 0
149+
persist-credentials: false
150+
path: ${{ env.REPO_DIR }}
151+
152+
# --- Check for existing PR ---
153+
- name: Check for existing PR
154+
id: check
155+
shell: bash
156+
run: |
157+
pr_url="$(gh pr list --repo "$GITHUB_REPOSITORY" \
158+
--label "autosolve-issue-$ISSUE_NUMBER" \
159+
--json url --jq '.[0].url // empty')"
160+
exists=false
161+
if [ -n "$pr_url" ]; then
162+
exists=true
163+
fi
164+
echo "exists=$exists" >> "$GITHUB_OUTPUT"
165+
echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT"
166+
env:
167+
GH_TOKEN: ${{ secrets.pr_create_token }}
168+
ISSUE_NUMBER: ${{ inputs.issue_number }}
169+
170+
- name: Comment that PR already exists
171+
if: steps.check.outputs.exists == 'true'
172+
shell: bash
173+
run: |
174+
body="Auto-solver was triggered but a PR already exists for this issue: $PR_URL"
175+
body+=$'\n\nTo create a new attempt, close the existing PR and add the label again.'
176+
gh issue comment "$ISSUE_NUMBER" \
177+
--repo "$GITHUB_REPOSITORY" \
178+
--body "$body"
179+
env:
180+
GH_TOKEN: ${{ secrets.repo_token }}
181+
ISSUE_NUMBER: ${{ inputs.issue_number }}
182+
PR_URL: ${{ steps.check.outputs.pr_url }}
183+
184+
# --- Setup (skipped when PR already exists) ---
185+
- name: Validate Vertex auth inputs
186+
if: steps.check.outputs.exists != 'true' && inputs.auth_mode == 'vertex'
187+
shell: bash
188+
run: |
189+
missing=()
190+
[ -z "$VERTEX_PROJECT_ID" ] && missing+=(vertex_project_id)
191+
[ -z "$VERTEX_SA" ] && missing+=(vertex_service_account)
192+
[ -z "$VERTEX_WIP" ] && missing+=(vertex_workload_identity_provider)
193+
if [ ${#missing[@]} -gt 0 ]; then
194+
echo "::error::auth_mode is 'vertex' but required inputs are missing: ${missing[*]}"
195+
exit 1
196+
fi
197+
env:
198+
VERTEX_PROJECT_ID: ${{ inputs.vertex_project_id }}
199+
VERTEX_SA: ${{ inputs.vertex_service_account }}
200+
VERTEX_WIP: ${{ inputs.vertex_workload_identity_provider }}
201+
202+
- name: Authenticate to Google Cloud (Vertex)
203+
if: steps.check.outputs.exists != 'true' && inputs.auth_mode == 'vertex'
204+
uses: google-github-actions/auth@v3
205+
with:
206+
project_id: ${{ inputs.vertex_project_id }}
207+
service_account: ${{ inputs.vertex_service_account }}
208+
workload_identity_provider: ${{ inputs.vertex_workload_identity_provider }}
209+
210+
# --- Build prompt ---
211+
- name: Build prompt from issue
212+
if: steps.check.outputs.exists != 'true' && inputs.prompt == ''
213+
shell: bash
214+
run: |
215+
{
216+
echo "PROMPT<<END_OF_PROMPT"
217+
printf 'Fix GitHub issue #%s.\n' "$ISSUE_NUMBER"
218+
printf 'Title: %s\n' "$ISSUE_TITLE"
219+
printf 'Body: %s\n' "$ISSUE_BODY"
220+
echo "END_OF_PROMPT"
221+
} >> "$GITHUB_ENV"
222+
env:
223+
ISSUE_NUMBER: ${{ inputs.issue_number }}
224+
ISSUE_TITLE: ${{ inputs.issue_title }}
225+
ISSUE_BODY: ${{ inputs.issue_body }}
226+
227+
- name: Set explicit prompt
228+
if: steps.check.outputs.exists != 'true' && inputs.prompt != ''
229+
shell: bash
230+
run: |
231+
{
232+
echo "PROMPT<<END_OF_PROMPT"
233+
printf '%s\n' "$INPUT_PROMPT"
234+
echo "END_OF_PROMPT"
235+
} >> "$GITHUB_ENV"
236+
env:
237+
INPUT_PROMPT: ${{ inputs.prompt }}
238+
239+
# --- Assess ---
240+
- name: Assess
241+
id: assess
242+
if: steps.check.outputs.exists != 'true'
243+
uses: ./autosolve/assess
244+
env:
245+
ANTHROPIC_API_KEY: ${{ inputs.auth_mode == 'api_key' && secrets.anthropic_api_key || '' }}
246+
CLAUDE_CODE_USE_VERTEX: ${{ inputs.auth_mode == 'vertex' && '1' || '' }}
247+
ANTHROPIC_VERTEX_PROJECT_ID: ${{ inputs.auth_mode == 'vertex' && inputs.vertex_project_id || '' }}
248+
CLOUD_ML_REGION: ${{ inputs.auth_mode == 'vertex' && inputs.vertex_region || '' }}
249+
with:
250+
claude_cli_version: ${{ inputs.claude_cli_version }}
251+
prompt: ${{ env.PROMPT }}
252+
skill: ${{ inputs.skill }}
253+
additional_instructions: ${{ inputs.additional_instructions }}
254+
model: ${{ inputs.model }}
255+
blocked_paths: ${{ inputs.blocked_paths }}
256+
working_directory: ${{ env.REPO_DIR }}
257+
258+
- name: Comment that issue was skipped
259+
if: steps.check.outputs.exists != 'true' && steps.assess.outputs.assessment == 'SKIP'
260+
shell: bash
261+
run: |
262+
body="Auto-solver assessed this issue but determined it is not suitable for automated resolution."
263+
body+=$'\n\n```\n'"$SUMMARY"$'\n```'
264+
gh issue comment "$ISSUE_NUMBER" \
265+
--repo "$GITHUB_REPOSITORY" \
266+
--body "$body"
267+
env:
268+
GH_TOKEN: ${{ secrets.repo_token }}
269+
ISSUE_NUMBER: ${{ inputs.issue_number }}
270+
# Pass summary as an env var as a precautionary measure against
271+
# command injection. It came from the summary claude generated so we
272+
# expect it to be safe but let's be even safer.
273+
SUMMARY: ${{ steps.assess.outputs.summary }}
274+
275+
# --- Implement ---
276+
- name: Implement
277+
id: implement
278+
if: steps.check.outputs.exists != 'true' && steps.assess.outputs.assessment == 'PROCEED'
279+
uses: ./autosolve/implement
280+
env:
281+
ANTHROPIC_API_KEY: ${{ inputs.auth_mode == 'api_key' && secrets.anthropic_api_key || '' }}
282+
CLAUDE_CODE_USE_VERTEX: ${{ inputs.auth_mode == 'vertex' && '1' || '' }}
283+
ANTHROPIC_VERTEX_PROJECT_ID: ${{ inputs.auth_mode == 'vertex' && inputs.vertex_project_id || '' }}
284+
CLOUD_ML_REGION: ${{ inputs.auth_mode == 'vertex' && inputs.vertex_region || '' }}
285+
with:
286+
claude_cli_version: ${{ inputs.claude_cli_version }}
287+
prompt: ${{ env.PROMPT }}
288+
skill: ${{ inputs.skill }}
289+
additional_instructions: ${{ inputs.additional_instructions }}
290+
allowed_tools: ${{ inputs.allowed_tools }}
291+
model: ${{ inputs.model }}
292+
max_retries: ${{ inputs.max_retries }}
293+
fork_owner: ${{ inputs.fork_owner }}
294+
fork_repo: ${{ inputs.fork_repo }}
295+
fork_push_token: ${{ secrets.fork_push_token }}
296+
pr_create_token: ${{ secrets.pr_create_token }}
297+
pr_labels: "autosolve,autosolve-issue-${{ inputs.issue_number }}"
298+
pr_draft: "true"
299+
blocked_paths: ${{ inputs.blocked_paths }}
300+
git_user_name: ${{ inputs.git_user_name }}
301+
git_user_email: ${{ inputs.git_user_email }}
302+
branch_suffix: "issue-${{ inputs.issue_number }}"
303+
working_directory: ${{ env.REPO_DIR }}
304+
305+
- name: Comment that implementation succeeded
306+
if: steps.implement.outputs.status == 'SUCCESS'
307+
shell: bash
308+
run: |
309+
body="Auto-solver has created a draft PR: $PR_URL"
310+
body+=$'\n\nPlease review the changes carefully before approving.'
311+
gh issue comment "$ISSUE_NUMBER" \
312+
--repo "$GITHUB_REPOSITORY" \
313+
--body "$body"
314+
env:
315+
GH_TOKEN: ${{ secrets.repo_token }}
316+
ISSUE_NUMBER: ${{ inputs.issue_number }}
317+
PR_URL: ${{ steps.implement.outputs.pr_url }}
318+
319+
- name: Comment that implementation failed
320+
if: always() && steps.assess.outputs.assessment == 'PROCEED' && steps.implement.outputs.status != 'SUCCESS'
321+
shell: bash
322+
run: |
323+
body="Auto-solver attempted to fix this issue but was unable to complete the implementation."
324+
body+=$'\n\nThis issue may require human intervention.'
325+
gh issue comment "$ISSUE_NUMBER" \
326+
--repo "$GITHUB_REPOSITORY" \
327+
--body "$body"
328+
env:
329+
GH_TOKEN: ${{ secrets.repo_token }}
330+
ISSUE_NUMBER: ${{ inputs.issue_number }}
331+
332+
# --- Cleanup (always runs) ---
333+
- name: Remove label
334+
if: always()
335+
shell: bash
336+
# Best-effort: the label may already have been removed by another run.
337+
run: gh issue edit "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --remove-label "$TRIGGER_LABEL" || true
338+
env:
339+
GH_TOKEN: ${{ secrets.repo_token }}
340+
ISSUE_NUMBER: ${{ inputs.issue_number }}
341+
TRIGGER_LABEL: ${{ inputs.trigger_label }}
342+
343+
- name: Set final status
344+
id: final_status
345+
if: always()
346+
shell: bash
347+
run: |
348+
if [ "$PR_EXISTS" = "true" ]; then
349+
echo "status=EXISTING_PR" >> "$GITHUB_OUTPUT"
350+
{
351+
echo "## Autosolve"
352+
echo "**Status:** Skipped — a PR already exists for this issue: $EXISTING_PR_URL"
353+
echo ""
354+
echo "To create a new attempt, close the existing PR and add the label again."
355+
} >> "$GITHUB_STEP_SUMMARY"
356+
elif [ "$ASSESSMENT" = "SKIP" ]; then
357+
echo "status=SKIPPED" >> "$GITHUB_OUTPUT"
358+
{
359+
echo "## Autosolve"
360+
echo "**Status:** Skipped — assessment determined this issue is not suitable for automated resolution."
361+
} >> "$GITHUB_STEP_SUMMARY"
362+
elif [ "$IMPL_STATUS" = "SUCCESS" ]; then
363+
echo "status=SUCCESS" >> "$GITHUB_OUTPUT"
364+
else
365+
echo "status=FAILED" >> "$GITHUB_OUTPUT"
366+
fi
367+
env:
368+
PR_EXISTS: ${{ steps.check.outputs.exists }}
369+
EXISTING_PR_URL: ${{ steps.check.outputs.pr_url }}
370+
ASSESSMENT: ${{ steps.assess.outputs.assessment }}
371+
IMPL_STATUS: ${{ steps.implement.outputs.status }}
372+
373+
- name: Write token usage summary
374+
if: always() && steps.check.outputs.exists != 'true'
375+
shell: bash
376+
run: |
377+
f="$RUNNER_TEMP/autosolve-usage.md"
378+
if [ -f "$f" ]; then
379+
cat "$f" >> "$GITHUB_STEP_SUMMARY"
380+
fi

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Breaking changes are prefixed with "Breaking Change: ".
1010

1111
### Added
1212

13+
- `github-issue-autosolve` reusable workflow: turnkey GitHub Issues
14+
integration composing assess + implement with issue comments, label
15+
management, and concurrency control.
1316
- `autosolve/assess` action: evaluate tasks for automated resolution suitability
1417
using Claude in read-only mode.
1518
- `autosolve/implement` action: autonomously implement solutions, validate

0 commit comments

Comments
 (0)