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 ()
0 commit comments