Skip to content

Commit 4aa1852

Browse files
committed
build: automatically triage issues using Gemini API
1 parent 5fc395c commit 4aa1852

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

.github/scripts/triage_issue.py

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

.github/workflows/triage-issue.yml

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

0 commit comments

Comments
 (0)