mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-15 14:23:02 -07:00
142 lines
5.5 KiB
Python
142 lines
5.5 KiB
Python
"""
|
|
Purpose: Automatically categorizes GitHub issues as 'bug' or 'feature' and applies the corresponding label on GitHub.
|
|
It fetches issues matching a search URL, uses the Gemini API to classify them, and runs 'gh issue edit' to update GitHub.
|
|
"""
|
|
import argparse
|
|
import urllib.parse
|
|
import urllib.request
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import concurrent.futures
|
|
|
|
MODEL = "gemini-3-flash-preview"
|
|
|
|
ISSUE_TYPES = {
|
|
"bug": "IT_kwDOCaSVvs4BR7vP",
|
|
"feature": "IT_kwDOCaSVvs4BR7vQ"
|
|
}
|
|
|
|
def fetch_issues(search_query, limit):
|
|
cmd = ["gh", "issue", "list", "--search", search_query, "--limit", str(limit), "--json", "id,number,title,body,url"]
|
|
try:
|
|
res = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
return json.loads(res.stdout)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error fetching issues: {e.stderr}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
def categorize_issue(issue, api_key):
|
|
url = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent?key={api_key}"
|
|
prompt = f"""
|
|
Analyze the following GitHub issue and determine if it is a bug or a feature request.
|
|
Reply ONLY with a valid JSON object matching exactly this schema, without Markdown formatting:
|
|
{{"type": "bug" | "feature", "reasoning": "brief justification"}}
|
|
|
|
Issue Title: {issue.get('title')}
|
|
Issue Body: {issue.get('body', '')[:1500]}
|
|
"""
|
|
data = {
|
|
"contents": [{"parts": [{"text": prompt}]}],
|
|
"generationConfig": {"temperature": 0.1}
|
|
}
|
|
req = urllib.request.Request(url, data=json.dumps(data).encode('utf-8'), headers={'Content-Type': 'application/json'})
|
|
try:
|
|
with urllib.request.urlopen(req) as response:
|
|
result = json.loads(response.read().decode('utf-8'))
|
|
text = result['candidates'][0]['content']['parts'][0]['text']
|
|
|
|
# Clean markdown block if present
|
|
if text.startswith('```json'):
|
|
text = text[7:]
|
|
if text.startswith('```'):
|
|
text = text[3:]
|
|
if text.endswith('```'):
|
|
text = text[:-3]
|
|
|
|
parsed = json.loads(text.strip())
|
|
return parsed
|
|
except Exception as e:
|
|
print(f"Error processing issue {issue['number']}: {e}")
|
|
return None
|
|
|
|
def process_issue(issue, api_key):
|
|
print(f"Categorizing Issue #{issue['number']}...")
|
|
result = categorize_issue(issue, api_key)
|
|
if not result or 'type' not in result:
|
|
print(f"Failed to categorize #{issue['number']}.")
|
|
return
|
|
|
|
issue_type = result['type']
|
|
label = f"type/{issue_type}"
|
|
print(f"Issue #{issue['number']} is a {issue_type}. Applying label '{label}' and setting Issue Type on GitHub...")
|
|
|
|
# 1. Add label via gh issue edit
|
|
cmd_label = ["gh", "issue", "edit", str(issue['number']), "--add-label", label]
|
|
try:
|
|
subprocess.run(cmd_label, capture_output=True, text=True, check=True)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error labeling #{issue['number']}: {e.stderr}", file=sys.stderr)
|
|
|
|
# 2. Set the native GitHub Issue Type using GraphQL
|
|
type_node_id = ISSUE_TYPES.get(issue_type)
|
|
issue_node_id = issue.get('id')
|
|
|
|
if type_node_id and issue_node_id:
|
|
mutation = f"""
|
|
mutation {{
|
|
updateIssue(input: {{id: "{issue_node_id}", issueTypeId: "{type_node_id}"}}) {{
|
|
issue {{
|
|
id
|
|
}}
|
|
}}
|
|
}}
|
|
"""
|
|
cmd_type = ["gh", "api", "graphql", "-f", f"query={mutation}"]
|
|
try:
|
|
subprocess.run(cmd_type, capture_output=True, text=True, check=True)
|
|
print(f"Successfully labeled and set native Issue Type for #{issue['number']}.")
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error setting Issue Type for #{issue['number']}: {e.stderr}", file=sys.stderr)
|
|
else:
|
|
print(f"Could not resolve node IDs to set native Issue Type for #{issue['number']}.")
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Auto-categorize GitHub issues (bug vs feature) from a GitHub URL and apply labels on GitHub.")
|
|
parser.add_argument("url", help="The full GitHub Issues search URL (e.g., https://github.com/.../issues/?q=...)")
|
|
parser.add_argument("--api-key", required=True, help="Gemini API Key")
|
|
parser.add_argument("--limit", type=int, default=50, help="Maximum number of issues to process")
|
|
args = parser.parse_args()
|
|
|
|
parsed_url = urllib.parse.urlparse(args.url)
|
|
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
|
|
if 'q' in query_params:
|
|
search_query = query_params['q'][0]
|
|
else:
|
|
print("Warning: No 'q=' search parameter found in URL. Fetching default open issues.")
|
|
search_query = "is:issue is:open"
|
|
|
|
if 'repo:' not in search_query:
|
|
path_parts = [p for p in parsed_url.path.split('/') if p]
|
|
if len(path_parts) >= 2:
|
|
repo = f"{path_parts[0]}/{path_parts[1]}"
|
|
search_query = f"repo:{repo} {search_query}"
|
|
|
|
print(f"Fetching issues matching: '{search_query}'")
|
|
issues = fetch_issues(search_query, args.limit)
|
|
if not issues:
|
|
print("No issues found matching the query.")
|
|
return
|
|
|
|
print(f"Found {len(issues)} issues to categorize.")
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
|
|
futures = [executor.submit(process_issue, issue, args.api_key) for issue in issues]
|
|
concurrent.futures.wait(futures)
|
|
|
|
print("Done categorizing issues.")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|