From 4aa18528545ce2bed2153756b7184fad2a79c1b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 4 Feb 2026 15:12:57 +0100 Subject: [PATCH 1/8] build: automatically triage issues using Gemini API --- .github/scripts/triage_issue.py | 95 ++++++++++++++++++++++++++++++ .github/workflows/triage-issue.yml | 54 +++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 .github/scripts/triage_issue.py create mode 100644 .github/workflows/triage-issue.yml diff --git a/.github/scripts/triage_issue.py b/.github/scripts/triage_issue.py new file mode 100644 index 00000000..41b137fe --- /dev/null +++ b/.github/scripts/triage_issue.py @@ -0,0 +1,95 @@ +import os +import json +import urllib.request +import sys + +def get_gemini_response(api_key, prompt): + url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={api_key}" + headers = {'Content-Type': 'application/json'} + data = { + "contents": [{ + "parts": [{"text": prompt}] + }], + "generationConfig": { + "response_mime_type": "application/json" + } + } + + req = urllib.request.Request(url, data=json.dumps(data).encode('utf-8'), headers=headers) + try: + with urllib.request.urlopen(req) as response: + res_data = json.loads(response.read().decode('utf-8')) + return res_data['candidates'][0]['content']['parts'][0]['text'] + except urllib.error.HTTPError as e: + print(f"Gemini API Error ({e.code}): {e.reason}", file=sys.stderr) + try: + error_body = e.read().decode('utf-8') + print(f"Error details: {error_body}", file=sys.stderr) + except: + pass + return None + except Exception as e: + print(f"Error calling Gemini API: {e}", file=sys.stderr) + return None + +def main(): + api_key = os.getenv("GEMINI_API_KEY") + issue_title = os.getenv("ISSUE_TITLE") + issue_body = os.getenv("ISSUE_BODY") + + if not api_key: + print("GEMINI_API_KEY not found", file=sys.stderr) + sys.exit(1) + + if not issue_title and not issue_body: + print("Error: ISSUE_TITLE and ISSUE_BODY are both empty. Triage skipped.", file=sys.stderr) + sys.exit(0) # Exit gracefully so the workflow doesn't just fail without a reason + + prompt = f""" + You are an expert software engineer and triage assistant. + Analyze the following GitHub Issue details and suggest appropriate labels. + + Issue Title: {issue_title} + Issue Description: {issue_body} + + Triage Criteria: + - Severity: + - priority: p0: Critical issues, crashes, security vulnerabilities (specifically if it mentions "crash" or "exception"). + - priority: p1: Important issues that block release. + - priority: p2: Normal priority bugs or improvements. + - priority: p3: Minor enhancements or non-critical fixes. + - priority: p4: Low priority, nice-to-have eventually. + - Type: + - type: docs: Documentation issues or requests. + - type: typo: Mentioning typos in the codebase or UI. + - type: test: Issues related to testing. + - type: feature: Feature requests. + - type: bug: Bug reports. + - Environment: + - environment: no-google-play: If the issue specifically mentions devices without Google Play services, Huawei devices, or microG. + + Return a JSON object with a 'labels' key containing an array of suggested label names. + The response MUST be valid JSON. + Example: {{"labels": ["priority: p2", "type: bug"]}} + """ + + response_text = get_gemini_response(api_key, prompt) + if response_text: + try: + # Clean up response text in case it has markdown wrapping + if response_text.startswith("```json"): + response_text = response_text.replace("```json", "", 1).replace("```", "", 1).strip() + + result = json.loads(response_text) + labels = result.get("labels", []) + # Print labels as a comma-separated string for GitHub Actions + print(",".join(labels)) + except Exception as e: + print(f"Error parsing Gemini response: {e}", file=sys.stderr) + print(f"Raw response: {response_text}", file=sys.stderr) + sys.exit(1) + else: + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml new file mode 100644 index 00000000..455ba29f --- /dev/null +++ b/.github/workflows/triage-issue.yml @@ -0,0 +1,54 @@ +name: Issue Triage with Gemini + +on: + issues: + types: [opened, edited] + pull_request: + workflow_dispatch: + inputs: + title: + description: 'Mock Issue Title' + default: 'Test Issue' + body: + description: 'Mock Issue Body' + default: 'This is a test issue description.' + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + issues: write + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Run Triage Script + id: run_script + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + ISSUE_TITLE: ${{ github.event.issue.title || github.event.pull_request.title || github.event.inputs.title }} + ISSUE_BODY: ${{ github.event.issue.body || github.event.pull_request.body || github.event.inputs.body }} + run: | + labels=$(python .github/scripts/triage_issue.py) + echo "labels=$labels" >> $GITHUB_OUTPUT + + - name: Apply Labels + if: steps.run_script.outputs.labels != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Convert comma-separated labels to gh command arguments + IFS=',' read -ra ADDR <<< "${{ steps.run_script.outputs.labels }}" + for i in "${ADDR[@]}"; do + # Trim whitespace and only add if not empty + label=$(echo "$i" | xargs) + if [ -n "$label" ]; then + gh issue edit ${{ github.event.issue.number }} --add-label "$label" + fi + done From 26d3c6d802f44d555c5523729a4701e9207dfaf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Thu, 5 Feb 2026 19:13:11 +0100 Subject: [PATCH 2/8] build: new model --- .github/scripts/triage_issue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/triage_issue.py b/.github/scripts/triage_issue.py index 41b137fe..ba916304 100644 --- a/.github/scripts/triage_issue.py +++ b/.github/scripts/triage_issue.py @@ -4,7 +4,8 @@ import sys def get_gemini_response(api_key, prompt): - url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={api_key}" +# Using the stable Gemini 2.5 Flash + url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}" headers = {'Content-Type': 'application/json'} data = { "contents": [{ From d3cc14d486a4074f90051e11644e3ec4878976f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Thu, 5 Feb 2026 19:15:57 +0100 Subject: [PATCH 3/8] ci: robustly identify issue in triage-issue workflow --- .github/workflows/triage-issue.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml index 455ba29f..8cd2c809 100644 --- a/.github/workflows/triage-issue.yml +++ b/.github/workflows/triage-issue.yml @@ -39,9 +39,10 @@ jobs: echo "labels=$labels" >> $GITHUB_OUTPUT - name: Apply Labels - if: steps.run_script.outputs.labels != '' + if: steps.run_script.outputs.labels != '' && (github.event.issue.number || github.event.pull_request.number) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} run: | # Convert comma-separated labels to gh command arguments IFS=',' read -ra ADDR <<< "${{ steps.run_script.outputs.labels }}" @@ -49,6 +50,6 @@ jobs: # Trim whitespace and only add if not empty label=$(echo "$i" | xargs) if [ -n "$label" ]; then - gh issue edit ${{ github.event.issue.number }} --add-label "$label" + gh issue edit "$ISSUE_NUMBER" --add-label "$label" fi done From c3a3c2995769f681d58ec0ea100ecb7fc5c3cde4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Thu, 5 Feb 2026 19:17:30 +0100 Subject: [PATCH 4/8] ci: permissions for testing --- .github/workflows/triage-issue.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml index 8cd2c809..4cadd82d 100644 --- a/.github/workflows/triage-issue.yml +++ b/.github/workflows/triage-issue.yml @@ -18,6 +18,7 @@ jobs: runs-on: ubuntu-latest permissions: issues: write + pull-requests: write contents: read steps: - name: Checkout code From 9146a206e4ecfb721b862b04fb2702c795e5c1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Thu, 5 Feb 2026 19:22:05 +0100 Subject: [PATCH 5/8] ci: priority labels --- .github/scripts/triage_issue.py | 8 -------- .github/workflows/triage-issue.yml | 5 +++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/scripts/triage_issue.py b/.github/scripts/triage_issue.py index ba916304..a01cd803 100644 --- a/.github/scripts/triage_issue.py +++ b/.github/scripts/triage_issue.py @@ -60,14 +60,6 @@ def main(): - priority: p2: Normal priority bugs or improvements. - priority: p3: Minor enhancements or non-critical fixes. - priority: p4: Low priority, nice-to-have eventually. - - Type: - - type: docs: Documentation issues or requests. - - type: typo: Mentioning typos in the codebase or UI. - - type: test: Issues related to testing. - - type: feature: Feature requests. - - type: bug: Bug reports. - - Environment: - - environment: no-google-play: If the issue specifically mentions devices without Google Play services, Huawei devices, or microG. Return a JSON object with a 'labels' key containing an array of suggested label names. The response MUST be valid JSON. diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml index 4cadd82d..07360471 100644 --- a/.github/workflows/triage-issue.yml +++ b/.github/workflows/triage-issue.yml @@ -48,9 +48,10 @@ jobs: # Convert comma-separated labels to gh command arguments IFS=',' read -ra ADDR <<< "${{ steps.run_script.outputs.labels }}" for i in "${ADDR[@]}"; do - # Trim whitespace and only add if not empty + # Trim whitespace label=$(echo "$i" | xargs) - if [ -n "$label" ]; then + # Only add priority labels as requested + if [[ "$label" == priority:* ]]; then gh issue edit "$ISSUE_NUMBER" --add-label "$label" fi done From ffe590ef3b8f58bd89edce11f969a51fb41fee73 Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:38:58 -0700 Subject: [PATCH 6/8] feat(triage): enhance automated issue triaging script and workflow --- .github/scripts/triage_issue.py | 19 +++++++++++-------- .github/workflows/triage-issue.yml | 3 ++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/scripts/triage_issue.py b/.github/scripts/triage_issue.py index 41b137fe..d3976103 100644 --- a/.github/scripts/triage_issue.py +++ b/.github/scripts/triage_issue.py @@ -4,7 +4,7 @@ import sys def get_gemini_response(api_key, prompt): - url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={api_key}" + url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}" headers = {'Content-Type': 'application/json'} data = { "contents": [{ @@ -34,6 +34,7 @@ def get_gemini_response(api_key, prompt): def main(): api_key = os.getenv("GEMINI_API_KEY") + print(f"DEBUG: Using API Key: {api_key}", file=sys.stderr) issue_title = os.getenv("ISSUE_TITLE") issue_body = os.getenv("ISSUE_BODY") @@ -47,7 +48,7 @@ def main(): prompt = f""" You are an expert software engineer and triage assistant. - Analyze the following GitHub Issue details and suggest appropriate labels. + Analyze the following GitHub Issue details and suggest appropriate labels and a brief reasoning. Issue Title: {issue_title} Issue Description: {issue_body} @@ -68,9 +69,12 @@ def main(): - Environment: - environment: no-google-play: If the issue specifically mentions devices without Google Play services, Huawei devices, or microG. - Return a JSON object with a 'labels' key containing an array of suggested label names. + Return a JSON object with two keys: + 1. "labels": an array of suggested label names. + 2. "reasoning": a brief, one-sentence explanation for your choice of labels, particularly the priority. + The response MUST be valid JSON. - Example: {{"labels": ["priority: p2", "type: bug"]}} + Example: {{"labels": ["priority: p0", "type: bug"], "reasoning": "The issue describes a fatal exception and includes a stack trace, indicating a critical crash that should be addressed immediately."}} """ response_text = get_gemini_response(api_key, prompt) @@ -80,10 +84,9 @@ def main(): if response_text.startswith("```json"): response_text = response_text.replace("```json", "", 1).replace("```", "", 1).strip() - result = json.loads(response_text) - labels = result.get("labels", []) - # Print labels as a comma-separated string for GitHub Actions - print(",".join(labels)) + # Validate that the response is valid JSON before printing + json.loads(response_text) + print(response_text) except Exception as e: print(f"Error parsing Gemini response: {e}", file=sys.stderr) print(f"Raw response: {response_text}", file=sys.stderr) diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml index 455ba29f..9d12a6af 100644 --- a/.github/workflows/triage-issue.yml +++ b/.github/workflows/triage-issue.yml @@ -42,6 +42,7 @@ jobs: if: steps.run_script.outputs.labels != '' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EVENT_NUMBER: ${{ github.event.number }} run: | # Convert comma-separated labels to gh command arguments IFS=',' read -ra ADDR <<< "${{ steps.run_script.outputs.labels }}" @@ -49,6 +50,6 @@ jobs: # Trim whitespace and only add if not empty label=$(echo "$i" | xargs) if [ -n "$label" ]; then - gh issue edit ${{ github.event.issue.number }} --add-label "$label" + gh issue edit ${{ env.EVENT_NUMBER }} --add-label "$label" fi done From 47e5da68652e86ba1b18a2db9c95605593889960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Fri, 6 Feb 2026 08:48:45 +0100 Subject: [PATCH 7/8] ci: priority labels --- .github/scripts/triage_issue.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/scripts/triage_issue.py b/.github/scripts/triage_issue.py index c992a632..d9f1c91c 100644 --- a/.github/scripts/triage_issue.py +++ b/.github/scripts/triage_issue.py @@ -35,7 +35,6 @@ def get_gemini_response(api_key, prompt): def main(): api_key = os.getenv("GEMINI_API_KEY") - print(f"DEBUG: Using API Key: {api_key}", file=sys.stderr) issue_title = os.getenv("ISSUE_TITLE") issue_body = os.getenv("ISSUE_BODY") @@ -49,7 +48,7 @@ def main(): prompt = f""" You are an expert software engineer and triage assistant. - Analyze the following GitHub Issue details and suggest appropriate labels and a brief reasoning. + Analyze the following GitHub Issue details and suggest appropriate labels. Issue Title: {issue_title} Issue Description: {issue_body} @@ -62,12 +61,9 @@ def main(): - priority: p3: Minor enhancements or non-critical fixes. - priority: p4: Low priority, nice-to-have eventually. - Return a JSON object with two keys: - 1. "labels": an array of suggested label names. - 2. "reasoning": a brief, one-sentence explanation for your choice of labels, particularly the priority. - + Return a JSON object with a 'labels' key containing an array of suggested label names. The response MUST be valid JSON. - Example: {{"labels": ["priority: p0", "type: bug"], "reasoning": "The issue describes a fatal exception and includes a stack trace, indicating a critical crash that should be addressed immediately."}} + Example: {{"labels": ["priority: p2", "type: bug"]}} """ response_text = get_gemini_response(api_key, prompt) @@ -77,9 +73,10 @@ def main(): if response_text.startswith("```json"): response_text = response_text.replace("```json", "", 1).replace("```", "", 1).strip() - # Validate that the response is valid JSON before printing - json.loads(response_text) - print(response_text) + result = json.loads(response_text) + labels = result.get("labels", []) + # Print labels as a comma-separated string for GitHub Actions + print(",".join(labels)) except Exception as e: print(f"Error parsing Gemini response: {e}", file=sys.stderr) print(f"Raw response: {response_text}", file=sys.stderr) From a7f1f097328640d8b119095507e90204efc7969a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Fri, 6 Feb 2026 20:55:26 +0100 Subject: [PATCH 8/8] ci: restrict triage to issues only --- .github/workflows/triage-issue.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml index 1fc46c40..c37de249 100644 --- a/.github/workflows/triage-issue.yml +++ b/.github/workflows/triage-issue.yml @@ -3,7 +3,6 @@ name: Issue Triage with Gemini on: issues: types: [opened, edited] - pull_request: workflow_dispatch: inputs: title: @@ -18,7 +17,6 @@ jobs: runs-on: ubuntu-latest permissions: issues: write - pull-requests: write contents: read steps: - name: Checkout code @@ -33,17 +31,17 @@ jobs: id: run_script env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} - ISSUE_TITLE: ${{ github.event.issue.title || github.event.pull_request.title || github.event.inputs.title }} - ISSUE_BODY: ${{ github.event.issue.body || github.event.pull_request.body || github.event.inputs.body }} + ISSUE_TITLE: ${{ github.event.issue.title || github.event.inputs.title }} + ISSUE_BODY: ${{ github.event.issue.body || github.event.inputs.body }} run: | labels=$(python .github/scripts/triage_issue.py) echo "labels=$labels" >> $GITHUB_OUTPUT - name: Apply Labels - if: steps.run_script.outputs.labels != '' && (github.event.issue.number || github.event.pull_request.number) + if: steps.run_script.outputs.labels != '' && (github.event.issue.number) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} + ISSUE_NUMBER: ${{ github.event.issue.number }} run: | # Convert comma-separated labels to gh command arguments IFS=',' read -ra ADDR <<< "${{ steps.run_script.outputs.labels }}"