Skip to content

Commit 860c3ef

Browse files
authored
build: automatically triage issues (#1652)
* build: automatically triage issues * build: headers
1 parent 9edfdbe commit 860c3ef

File tree

2 files changed

+171
-0
lines changed

2 files changed

+171
-0
lines changed

.github/scripts/triage_issue.py

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

.github/workflows/triage-issue.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: Issue Triage with Gemini
16+
17+
on:
18+
issues:
19+
types: [opened, edited]
20+
workflow_dispatch:
21+
inputs:
22+
title:
23+
description: 'Mock Issue Title'
24+
default: 'Test Issue'
25+
body:
26+
description: 'Mock Issue Body'
27+
default: 'This is a test issue description.'
28+
29+
jobs:
30+
triage:
31+
runs-on: ubuntu-latest
32+
permissions:
33+
issues: write
34+
contents: read
35+
steps:
36+
- name: Checkout code
37+
uses: actions/checkout@v4
38+
39+
- name: Set up Python
40+
uses: actions/setup-python@v5
41+
with:
42+
python-version: '3.x'
43+
44+
- name: Run Triage Script
45+
id: run_script
46+
env:
47+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
48+
ISSUE_TITLE: ${{ github.event.issue.title || github.event.inputs.title }}
49+
ISSUE_BODY: ${{ github.event.issue.body || github.event.inputs.body }}
50+
run: |
51+
labels=$(python .github/scripts/triage_issue.py)
52+
echo "labels=$labels" >> $GITHUB_OUTPUT
53+
54+
- name: Apply Labels
55+
if: steps.run_script.outputs.labels != '' && (github.event.issue.number)
56+
env:
57+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58+
ISSUE_NUMBER: ${{ github.event.issue.number }}
59+
run: |
60+
# Convert comma-separated labels to gh command arguments
61+
IFS=',' read -ra ADDR <<< "${{ steps.run_script.outputs.labels }}"
62+
for i in "${ADDR[@]}"; do
63+
# Trim whitespace
64+
label=$(echo "$i" | xargs)
65+
# Only add priority labels as requested
66+
if [[ "$label" == priority:* ]]; then
67+
gh issue edit "$ISSUE_NUMBER" --add-label "$label"
68+
fi
69+
done

0 commit comments

Comments
 (0)