Skip to content

Commit 7baf239

Browse files
committed
feat: add auto issue triage workflow
Add a GitHub Actions workflow and cagent agent config that automatically triages bug reports when labeled `kind/bug`. The agent evaluates if the report has enough info, asks for details if not, or implements a fix and opens a draft PR.
1 parent 934a5f6 commit 7baf239

2 files changed

Lines changed: 364 additions & 0 deletions

File tree

‎.github/agents/issue-triager.yaml‎

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
models:
2+
# Sonnet is capable enough for both triage and code fixes
3+
claude-sonnet:
4+
provider: anthropic
5+
model: claude-sonnet-4-5
6+
max_tokens: 8192
7+
temperature: 0.1
8+
9+
agents:
10+
root:
11+
model: claude-sonnet
12+
description: Triages bug reports and delegates fixes to the fixer sub-agent
13+
sub_agents:
14+
- fixer
15+
instruction: |
16+
You are an issue triage agent. You evaluate GitHub bug reports to determine if they
17+
contain enough information to act on, and if so, delegate to the fixer sub-agent to
18+
implement a fix.
19+
20+
## Input
21+
22+
You receive a prompt containing:
23+
- Issue number, title, body, and labels
24+
- The bug report template for reference
25+
26+
## Your workflow
27+
28+
### Step 1: Evaluate the issue
29+
30+
Determine if the issue is actionable by checking:
31+
32+
1. **Is it actually a bug?** (not a feature request, question, or support issue)
33+
2. **Clear description?** Does it explain what's wrong?
34+
3. **Reproduction steps?** Can we understand how to trigger the bug?
35+
4. **Expected vs actual behavior?** Do we know what should happen?
36+
37+
An issue does NOT need to fill in every template field to be actionable. Use judgment:
38+
- A well-written description with clear context can compensate for missing fields
39+
- A stack trace or error message often provides enough to investigate
40+
- Version info is helpful but not always strictly required
41+
42+
### Step 2a: If NOT enough info
43+
44+
If the issue is missing critical information needed to understand or reproduce the bug:
45+
46+
1. Use `gh issue comment` to post a polite comment explaining what's missing and
47+
asking for specific details. Be helpful, not bureaucratic. Example:
48+
49+
```bash
50+
gh issue comment ISSUE_NUMBER --body "$(cat <<'EOF'
51+
Thanks for reporting this! To help us investigate, could you provide:
52+
53+
- Steps to reproduce the issue
54+
- The version you're using (`docker agent version`)
55+
- The full error message or stack trace
56+
57+
This will help us track down the root cause faster.
58+
EOF
59+
)"
60+
```
61+
62+
2. Add the `status/needs-info` label:
63+
```bash
64+
gh issue edit ISSUE_NUMBER --add-label "status/needs-info"
65+
```
66+
67+
3. Output exactly: `RESULT:NEEDS_INFO`
68+
69+
### Step 2b: If enough info
70+
71+
If the issue has enough information to investigate:
72+
73+
1. First, explore the codebase to understand the project structure and locate
74+
relevant code related to the bug report
75+
2. Delegate to the `fixer` sub-agent with a clear description of the bug and
76+
pointers to relevant files/code
77+
3. When the fixer returns, verify that files were actually modified by listing
78+
changed files. Do NOT rely solely on the fixer's self-reported success:
79+
- If files were actually changed on disk → output exactly: `RESULT:FIXED`
80+
- If no files were changed → output exactly: `RESULT:NO_CHANGES`
81+
82+
## Important rules
83+
84+
- ALWAYS output exactly one of: `RESULT:NEEDS_INFO`, `RESULT:FIXED`, `RESULT:NO_CHANGES`
85+
- The result marker MUST be the LAST line of your output
86+
- Be empathetic in issue comments — these are real users reporting real problems
87+
- Do NOT commit or push any code changes — the workflow handles that
88+
- Do NOT close or reassign issues
89+
90+
toolsets:
91+
- type: shell
92+
- type: filesystem
93+
- type: think
94+
95+
fixer:
96+
model: claude-sonnet
97+
description: Investigates bugs and implements fixes in the codebase
98+
instruction: |
99+
You are a bug fixer agent. You receive a bug description from the triager and your
100+
job is to investigate the root cause and implement a fix.
101+
102+
## Your workflow
103+
104+
1. **Understand the bug**: Read the bug description carefully. Identify what's
105+
going wrong and where in the codebase it might originate.
106+
107+
2. **Investigate**: Use the filesystem tools to explore the codebase:
108+
- Read relevant source files
109+
- Trace the code path that triggers the bug
110+
- Look at related tests for context
111+
- Check recent changes to affected files
112+
113+
3. **Plan the fix**: Before writing any code, think through:
114+
- What's the root cause?
115+
- What's the minimal change that fixes it?
116+
- Could this fix break anything else?
117+
- Are there existing tests that need updating?
118+
119+
4. **Implement**: Make the necessary code changes:
120+
- Keep changes minimal and focused
121+
- Follow existing code style and conventions
122+
- Update or add tests if appropriate
123+
124+
5. **Verify**: Run tests and linting to make sure the fix is correct:
125+
```bash
126+
task test
127+
task lint
128+
```
129+
If tests fail, investigate and fix. Do not leave broken tests.
130+
131+
## Important rules
132+
133+
- Do NOT commit or push changes — the workflow handles git operations
134+
- Do NOT modify CI/CD configs, workflows, or unrelated files
135+
- Keep changes minimal — fix the bug, nothing more
136+
- If you cannot determine a fix with confidence, make no changes and explain why
137+
- Always run `task test` and `task lint` before finishing
138+
139+
toolsets:
140+
- type: filesystem
141+
- type: shell
142+
- type: think
143+
144+
permissions:
145+
allow:
146+
- shell:cmd=gh issue comment *
147+
- shell:cmd=gh issue edit *
148+
- shell:cmd=gh issue view *
149+
- shell:cmd=gh api *
150+
- shell:cmd=task test*
151+
- shell:cmd=task lint*
152+
- shell:cmd=task build*
153+
- shell:cmd=go *
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
name: Auto Issue Triage
2+
3+
on:
4+
issues:
5+
types: [labeled]
6+
7+
# Elevated permissions needed for the fix+PR path (commit, push, create PR).
8+
# The needs-info path only uses issues: write, but splitting into separate
9+
# jobs would add complexity without meaningful security benefit since this
10+
# only triggers on maintainer-applied labels.
11+
permissions:
12+
contents: write
13+
issues: write
14+
pull-requests: write
15+
16+
concurrency:
17+
group: issue-triage-${{ github.event.issue.number }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
triage:
22+
if: github.event.label.name == 'kind/bug'
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 15
25+
env:
26+
HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }}
27+
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
31+
with:
32+
fetch-depth: 1
33+
34+
- name: Generate GitHub App token
35+
if: env.HAS_APP_SECRETS == 'true'
36+
id: app-token
37+
continue-on-error: true
38+
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2
39+
with:
40+
app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }}
41+
private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }}
42+
43+
- name: Construct prompt
44+
id: prompt
45+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
46+
with:
47+
script: |
48+
const issue = context.payload.issue;
49+
const labels = issue.labels.map(l => l.name).join(', ');
50+
51+
const bugTemplate = [
52+
'## Bug Report Template (for reference)',
53+
'- Describe the bug',
54+
'- Version affected',
55+
'- How To Reproduce (steps)',
56+
'- Expected behavior',
57+
'- Screenshots (optional)',
58+
'- OS and Terminal type',
59+
'- Additional context',
60+
].join('\n');
61+
62+
const prompt = [
63+
`## Issue #${issue.number}: ${issue.title}`,
64+
'',
65+
`**Labels:** ${labels}`,
66+
`**Author:** ${issue.user.login}`,
67+
`**Created:** ${issue.created_at}`,
68+
'',
69+
'### Issue Body',
70+
'',
71+
issue.body || '(empty)',
72+
'',
73+
'---',
74+
'',
75+
bugTemplate,
76+
'',
77+
'---',
78+
'',
79+
`Triage this bug report. The issue number for gh CLI commands is ${issue.number}.`,
80+
].join('\n');
81+
82+
core.setOutput('text', prompt);
83+
84+
- name: Run triage agent
85+
id: agent
86+
uses: docker/cagent-action@latest
87+
env:
88+
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
89+
with:
90+
agent: ${{ github.workspace }}/.github/agents/issue-triager.yaml
91+
prompt: ${{ steps.prompt.outputs.text }}
92+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
93+
github-token: ${{ steps.app-token.outputs.token || github.token }}
94+
timeout: 600
95+
96+
- name: Parse agent result
97+
id: result
98+
shell: bash
99+
run: |
100+
OUTPUT_FILE="${{ steps.agent.outputs.output-file }}"
101+
if [ ! -f "$OUTPUT_FILE" ]; then
102+
echo "No output file found"
103+
echo "action=none" >> "$GITHUB_OUTPUT"
104+
exit 0
105+
fi
106+
107+
CONTENT=$(cat "$OUTPUT_FILE")
108+
echo "--- Agent output ---"
109+
echo "$CONTENT"
110+
echo "--------------------"
111+
112+
# Check for result markers (search from the end of output)
113+
if echo "$CONTENT" | grep -q "RESULT:NEEDS_INFO"; then
114+
echo "action=needs_info" >> "$GITHUB_OUTPUT"
115+
elif echo "$CONTENT" | grep -q "RESULT:FIXED"; then
116+
echo "action=fixed" >> "$GITHUB_OUTPUT"
117+
elif echo "$CONTENT" | grep -q "RESULT:NO_CHANGES"; then
118+
echo "action=none" >> "$GITHUB_OUTPUT"
119+
else
120+
echo "No recognized result marker found"
121+
echo "action=none" >> "$GITHUB_OUTPUT"
122+
fi
123+
124+
- name: Check for changes
125+
if: steps.result.outputs.action == 'fixed'
126+
id: changes
127+
shell: bash
128+
run: |
129+
if [ -n "$(git status --porcelain)" ]; then
130+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
131+
else
132+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
133+
fi
134+
135+
- name: Commit and push fix
136+
if: steps.result.outputs.action == 'fixed' && steps.changes.outputs.has_changes == 'true'
137+
id: push
138+
shell: bash
139+
env:
140+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
141+
run: |
142+
ISSUE_NUMBER=${{ github.event.issue.number }}
143+
BRANCH_NAME="fix/issue-${ISSUE_NUMBER}"
144+
145+
git config user.name "github-actions[bot]"
146+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
147+
148+
git checkout -b "$BRANCH_NAME"
149+
git add -A
150+
git commit -m "fix: auto-triage fix for #${ISSUE_NUMBER}
151+
152+
Automated fix generated by issue triage agent.
153+
Resolves #${ISSUE_NUMBER}"
154+
155+
git push origin "$BRANCH_NAME" || {
156+
echo "::error::Failed to push branch $BRANCH_NAME"
157+
exit 1
158+
}
159+
echo "branch=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
160+
161+
- name: Create draft PR and comment on issue
162+
if: steps.push.outputs.branch != ''
163+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
164+
env:
165+
BRANCH_NAME: ${{ steps.push.outputs.branch }}
166+
with:
167+
github-token: ${{ steps.app-token.outputs.token || github.token }}
168+
script: |
169+
const issue = context.payload.issue;
170+
const branch = process.env.BRANCH_NAME;
171+
172+
// Create draft PR
173+
const pr = await github.rest.pulls.create({
174+
owner: context.repo.owner,
175+
repo: context.repo.repo,
176+
title: `fix: auto-triage fix for #${issue.number}`,
177+
body: [
178+
`## Summary`,
179+
``,
180+
`Automated fix for #${issue.number}.`,
181+
``,
182+
`> **${issue.title}**`,
183+
``,
184+
`This PR was generated by the issue triage agent. Please review carefully before merging.`,
185+
``,
186+
`## Test plan`,
187+
``,
188+
`- [ ] Review the changes for correctness`,
189+
`- [ ] Verify tests pass in CI`,
190+
`- [ ] Manual testing if applicable`,
191+
].join('\n'),
192+
head: branch,
193+
base: 'main',
194+
draft: true,
195+
});
196+
197+
// Comment on the issue with the PR link
198+
await github.rest.issues.createComment({
199+
owner: context.repo.owner,
200+
repo: context.repo.repo,
201+
issue_number: issue.number,
202+
body: [
203+
`I've analyzed this bug report and created an automated fix:`,
204+
``,
205+
`**Draft PR:** ${pr.data.html_url}`,
206+
``,
207+
`Please review the changes — this was generated automatically and may need adjustments.`,
208+
].join('\n'),
209+
});
210+
211+
core.info(`Created draft PR #${pr.data.number}: ${pr.data.html_url}`);

0 commit comments

Comments
 (0)