Skip to content

Commit b722aa2

Browse files
authored
build: automatically triage issues (#79)
1 parent abece5c commit b722aa2

4 files changed

Lines changed: 185 additions & 0 deletions

File tree

.github/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

.github/workflows/publish.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
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+
115
name: Publish
216

317
on:

.github/workflows/release-please.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
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+
115
on:
216
push:
317
branches:

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()

0 commit comments

Comments
 (0)