📋 Gemini Scheduled Issue Triage #2387
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: 7 | |
| 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/[email protected] | |
| 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 labels = []; | |
| for await (const response of github.paginate.iterator(github.rest.issues.listLabelsForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| per_page: 100, // Maximum per page to reduce API calls | |
| })) { | |
| labels.push(...response.data); | |
| } | |
| 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 || github.token }}" | |
| run: |- | |
| echo '🔍 Finding unlabeled issues and issues marked for triage...' | |
| ISSUES="$(gh issue list \ | |
| --state 'open' \ | |
| --search 'no:label label:"status/needs-triage"' \ | |
| --json number,title,body \ | |
| --limit '100' \ | |
| --repo "${GITHUB_REPOSITORY}" | |
| )" | |
| echo '📝 Setting output for GitHub Actions...' | |
| echo "issues_to_triage=${ISSUES}" >> "${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 != '[]' }} | |
| 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: | |
| gcp_location: "${{ vars.GOOGLE_CLOUD_LOCATION }}" | |
| gcp_project_id: "${{ vars.GOOGLE_CLOUD_PROJECT }}" | |
| gcp_service_account: "${{ vars.SERVICE_ACCOUNT_EMAIL }}" | |
| gcp_workload_identity_provider: "${{ vars.GCP_WIF_PROVIDER }}" | |
| gemini_api_key: "${{ secrets.GEMINI_API_KEY }}" | |
| gemini_cli_version: "${{ vars.GEMINI_CLI_VERSION }}" | |
| gemini_debug: "${{ fromJSON(vars.GEMINI_DEBUG || vars.ACTIONS_STEP_DEBUG || false) }}" | |
| gemini_model: "${{ vars.GEMINI_MODEL }}" | |
| google_api_key: "${{ secrets.GOOGLE_API_KEY }}" | |
| use_gemini_code_assist: "${{ vars.GOOGLE_GENAI_USE_GCA }}" | |
| use_vertex_ai: "${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}" | |
| upload_artifacts: "${{ vars.UPLOAD_ARTIFACTS }}" | |
| workflow_name: "gemini-scheduled-triage" | |
| settings: |- | |
| { | |
| "model": { | |
| "maxSessionTurns": 25 | |
| }, | |
| "telemetry": { | |
| "enabled": true, | |
| "target": "local", | |
| "outfile": ".gemini/telemetry.log" | |
| }, | |
| "tools": { | |
| "core": [ | |
| "run_shell_command(echo)", | |
| "run_shell_command(jq)", | |
| "run_shell_command(printenv)" | |
| ] | |
| } | |
| } | |
| prompt: "/gemini-scheduled-triage" | |
| 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/[email protected] | |
| 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, | |
| }); | |
| } |