Skip to content

📋 Gemini Scheduled Issue Triage #1797

📋 Gemini Scheduled Issue Triage

📋 Gemini Scheduled Issue Triage #1797

name: '📋 Gemini Scheduled Issue Triage'
on:
schedule:
- cron: '0 * * * *' # Runs every hour
pull_request:
branches:
- 'main'
- 'release/**/*'
paths:
- '.github/workflows/gemini-scheduled-triage.yml'
push:
branches:
- 'main'
- 'release/**/*'
paths:
- '.github/workflows/gemini-scheduled-triage.yml'
workflow_dispatch:
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
triage:
runs-on: 'ubuntu-latest'
timeout-minutes: 20
permissions:
contents: 'read'
id-token: 'write'
issues: 'read'
pull-requests: 'read'
outputs:
available_labels: '${{ steps.get_labels.outputs.available_labels }}'
triaged_issues: '${{ env.TRIAGED_ISSUES }}'
steps:
- name: 'Get repository labels'
id: 'get_labels'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v7.0.1
with:
# NOTE: we intentionally do not use the minted token. The default
# GITHUB_TOKEN provided by the action has enough permissions to read
# the labels.
script: |-
const { data: labels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
});
if (!labels || labels.length === 0) {
core.setFailed('There are no issue labels in this repository.')
}
const labelNames = labels.map(label => label.name).sort();
core.setOutput('available_labels', labelNames.join(','));
core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
return labelNames;
- name: 'Find untriaged issues'
id: 'find_issues'
env:
GITHUB_REPOSITORY: '${{ github.repository }}'
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |-
export HAS_ISSUES=$(gh api repos/${GITHUB_REPOSITORY} --jq '.has_issues')
if [[ "$HAS_ISSUES" == "false" ]]; then
echo '📝 GitHub repo does not have Issues enabled. Skipping issue triage.'
echo "issues_to_triage=[]" >> "${GITHUB_OUTPUT}"
exit
fi
echo '🔍 Finding unlabeled issues and issues marked for triage...'
UNLABELED_ISSUES="$(gh issue list \
--state 'open' \
--search 'no:label' \
--json number,title,body \
--limit '20' \
--repo "${GITHUB_REPOSITORY}"
)"
NEEDS_TRIAGE_ISSUES="$(gh issue list \
--state 'open' \
--search 'label:"status/needs-triage"' \
--json number,title,body \
--limit '20' \
--repo "${GITHUB_REPOSITORY}"
)"
ISSUES="$(echo "${UNLABELED_ISSUES}" "${NEEDS_TRIAGE_ISSUES}" | jq -s 'add | unique_by(.number)')"
echo "${ISSUES}" | jq . || { echo "ERROR: Output is not valid JSON"; exit 1; }
echo '📝 Setting output for GitHub Actions...'
echo "issues_to_triage<<EOF" >> "${GITHUB_OUTPUT}"
echo "${ISSUES}" >> "${GITHUB_OUTPUT}"
echo "EOF" >> "${GITHUB_OUTPUT}"
ISSUE_COUNT="$(echo "${ISSUES}" | jq 'length')"
echo "✅ Found ${ISSUE_COUNT} issue(s) to triage! 🎯"
- name: 'Run Gemini Issue Analysis'
id: 'gemini_issue_analysis'
if: |-
${{ steps.find_issues.outputs.issues_to_triage != '[]' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
uses: 'google-github-actions/run-gemini-cli@v0' # ratchet:exclude
env:
GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
ISSUES_TO_TRIAGE: '${{ steps.find_issues.outputs.issues_to_triage }}'
REPOSITORY: '${{ github.repository }}'
AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
with:
gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}'
gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}'
gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}'
google_api_key: '${{ secrets.GOOGLE_API_KEY }}'
use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
gemini_debug: '${{ fromJSON(vars.DEBUG || vars.ACTIONS_STEP_DEBUG || false) }}'
gemini_model: '${{ vars.GEMINI_MODEL }}'
settings: |-
{
"maxSessionTurns": 25,
"telemetry": {
"enabled": ${{ vars.GOOGLE_CLOUD_PROJECT != '' }},
"target": "gcp"
},
"coreTools": [
"run_shell_command(echo)",
"run_shell_command(jq)",
"run_shell_command(printenv)"
]
}
prompt: |-
You are an expert Issue Triage Engineer. Your task is to analyze a list of GitHub issues and assign the most relevant labels.
**Instructions:**
1. You will be provided with a list of available labels and a list of issues to triage from the environment.
2. For each issue, analyze its title and body to determine the most appropriate labels from the available list.
3. Produce a single, minified JSON array as your output.
4. The JSON array should contain objects, each with three keys: `issue_number` (integer), `labels_to_set` (array of strings), and `explanation` (a brief string justification).
5. Only use labels from the provided list. Do not invent new labels.
6. If no labels are a good match for an issue, exclude it from the output.
7. Your final output to STDOUT must only be the JSON array. Do not include any other text, markdown, or explanations.
**Example Output:**
[{"issue_number":123,"labels_to_set":["kind/bug","priority/p2"],"explanation":"The issue describes a critical error in the login functionality."},{"issue_number":456,"labels_to_set":["kind/enhancement"],"explanation":"The user is requesting a new export feature."}]
label:
runs-on: 'ubuntu-latest'
needs:
- 'triage'
if: |-
needs.triage.outputs.available_labels != '' &&
needs.triage.outputs.available_labels != '[]' &&
needs.triage.outputs.triaged_issues != '' &&
needs.triage.outputs.triaged_issues != '[]'
permissions:
contents: 'read'
issues: 'write'
pull-requests: 'write'
steps:
- name: 'Mint identity token'
id: 'mint_identity_token'
if: |-
${{ vars.APP_ID }}
uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
with:
app-id: '${{ vars.APP_ID }}'
private-key: '${{ secrets.APP_PRIVATE_KEY }}'
permission-contents: 'read'
permission-issues: 'write'
permission-pull-requests: 'write'
- name: 'Apply labels'
env:
AVAILABLE_LABELS: '${{ needs.triage.outputs.available_labels }}'
TRIAGED_ISSUES: '${{ needs.triage.outputs.triaged_issues }}'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v7.0.1
with:
# Use the provided token so that the "gemini-cli" is the actor in the
# log for what changed the labels.
github-token: '${{ steps.mint_identity_token.outputs.token || secrets.GITHUB_TOKEN || github.token }}'
script: |-
// Parse the available labels
const availableLabels = (process.env.AVAILABLE_LABELS || '').split(',')
.map((label) => label.trim())
.sort()
// Parse out the triaged issues
const triagedIssues = (JSON.parse(process.env.TRIAGED_ISSUES || '{}'))
.sort((a, b) => a.issue_number - b.issue_number)
core.debug(`Triaged issues: ${JSON.stringify(triagedIssues)}`);
// Iterate over each label
for (const issue of triagedIssues) {
if (!issue) {
core.debug(`Skipping empty issue: ${JSON.stringify(issue)}`);
continue;
}
const issueNumber = issue.issue_number;
if (!issueNumber) {
core.debug(`Skipping issue with no data: ${JSON.stringify(issue)}`);
continue;
}
// Extract and reject invalid labels - we do this just in case
// someone was able to prompt inject malicious labels.
let labelsToSet = (issue.labels_to_set || [])
.map((label) => label.trim())
.filter((label) => availableLabels.includes(label))
.sort()
core.debug(`Identified labels to set: ${JSON.stringify(labelsToSet)}`);
if (labelsToSet.length === 0) {
core.info(`Skipping issue #${issueNumber} - no labels to set.`)
continue;
}
core.debug(`Setting labels on issue #${issueNumber} to ${labelsToSet.join(', ')} (${issue.explanation || 'no explanation'})`)
await github.rest.issues.setLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: labelsToSet,
});
}