Skip to content

Commit 731ed40

Browse files
kikosodkhawk
andauthored
build: added action to automatically triage issues (#838)
* build: automatically triage issues using Gemini API * build: new model * ci: robustly identify issue in triage-issue workflow * ci: permissions for testing * ci: priority labels * feat(triage): enhance automated issue triaging script and workflow * ci: priority labels * ci: restrict triage to issues only --------- Co-authored-by: dkhawk <107309+dkhawk@users.noreply.github.com>
1 parent c2f19e4 commit 731ed40

File tree

2 files changed

+143
-0
lines changed

2 files changed

+143
-0
lines changed

.github/scripts/triage_issue.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import os
2+
import json
3+
import urllib.request
4+
import sys
5+
6+
def get_gemini_response(api_key, prompt):
7+
# Using the stable Gemini 2.5 Flash
8+
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
9+
headers = {'Content-Type': 'application/json'}
10+
data = {
11+
"contents": [{
12+
"parts": [{"text": prompt}]
13+
}],
14+
"generationConfig": {
15+
"response_mime_type": "application/json"
16+
}
17+
}
18+
19+
req = urllib.request.Request(url, data=json.dumps(data).encode('utf-8'), headers=headers)
20+
try:
21+
with urllib.request.urlopen(req) as response:
22+
res_data = json.loads(response.read().decode('utf-8'))
23+
return res_data['candidates'][0]['content']['parts'][0]['text']
24+
except urllib.error.HTTPError as e:
25+
print(f"Gemini API Error ({e.code}): {e.reason}", file=sys.stderr)
26+
try:
27+
error_body = e.read().decode('utf-8')
28+
print(f"Error details: {error_body}", file=sys.stderr)
29+
except:
30+
pass
31+
return None
32+
except Exception as e:
33+
print(f"Error calling Gemini API: {e}", file=sys.stderr)
34+
return None
35+
36+
def main():
37+
api_key = os.getenv("GEMINI_API_KEY")
38+
issue_title = os.getenv("ISSUE_TITLE")
39+
issue_body = os.getenv("ISSUE_BODY")
40+
41+
if not api_key:
42+
print("GEMINI_API_KEY not found", file=sys.stderr)
43+
sys.exit(1)
44+
45+
if not issue_title and not issue_body:
46+
print("Error: ISSUE_TITLE and ISSUE_BODY are both empty. Triage skipped.", file=sys.stderr)
47+
sys.exit(0) # Exit gracefully so the workflow doesn't just fail without a reason
48+
49+
prompt = f"""
50+
You are an expert software engineer and triage assistant.
51+
Analyze the following GitHub Issue details and suggest appropriate labels.
52+
53+
Issue Title: {issue_title}
54+
Issue Description: {issue_body}
55+
56+
Triage Criteria:
57+
- Severity:
58+
- priority: p0: Critical issues, crashes, security vulnerabilities (specifically if it mentions "crash" or "exception").
59+
- priority: p1: Important issues that block release.
60+
- priority: p2: Normal priority bugs or improvements.
61+
- priority: p3: Minor enhancements or non-critical fixes.
62+
- priority: p4: Low priority, nice-to-have eventually.
63+
64+
Return a JSON object with a 'labels' key containing an array of suggested label names.
65+
The response MUST be valid JSON.
66+
Example: {{"labels": ["priority: p2", "type: bug"]}}
67+
"""
68+
69+
response_text = get_gemini_response(api_key, prompt)
70+
if response_text:
71+
try:
72+
# Clean up response text in case it has markdown wrapping
73+
if response_text.startswith("```json"):
74+
response_text = response_text.replace("```json", "", 1).replace("```", "", 1).strip()
75+
76+
result = json.loads(response_text)
77+
labels = result.get("labels", [])
78+
# Print labels as a comma-separated string for GitHub Actions
79+
print(",".join(labels))
80+
except Exception as e:
81+
print(f"Error parsing Gemini response: {e}", file=sys.stderr)
82+
print(f"Raw response: {response_text}", file=sys.stderr)
83+
sys.exit(1)
84+
else:
85+
sys.exit(1)
86+
87+
if __name__ == "__main__":
88+
main()

.github/workflows/triage-issue.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Issue Triage with Gemini
2+
3+
on:
4+
issues:
5+
types: [opened, edited]
6+
workflow_dispatch:
7+
inputs:
8+
title:
9+
description: 'Mock Issue Title'
10+
default: 'Test Issue'
11+
body:
12+
description: 'Mock Issue Body'
13+
default: 'This is a test issue description.'
14+
15+
jobs:
16+
triage:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
issues: write
20+
contents: read
21+
steps:
22+
- name: Checkout code
23+
uses: actions/checkout@v4
24+
25+
- name: Set up Python
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: '3.x'
29+
30+
- name: Run Triage Script
31+
id: run_script
32+
env:
33+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
34+
ISSUE_TITLE: ${{ github.event.issue.title || github.event.inputs.title }}
35+
ISSUE_BODY: ${{ github.event.issue.body || github.event.inputs.body }}
36+
run: |
37+
labels=$(python .github/scripts/triage_issue.py)
38+
echo "labels=$labels" >> $GITHUB_OUTPUT
39+
40+
- name: Apply Labels
41+
if: steps.run_script.outputs.labels != '' && (github.event.issue.number)
42+
env:
43+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44+
ISSUE_NUMBER: ${{ github.event.issue.number }}
45+
run: |
46+
# Convert comma-separated labels to gh command arguments
47+
IFS=',' read -ra ADDR <<< "${{ steps.run_script.outputs.labels }}"
48+
for i in "${ADDR[@]}"; do
49+
# Trim whitespace
50+
label=$(echo "$i" | xargs)
51+
# Only add priority labels as requested
52+
if [[ "$label" == priority:* ]]; then
53+
gh issue edit "$ISSUE_NUMBER" --add-label "$label"
54+
fi
55+
done

0 commit comments

Comments
 (0)