π Gemini Scheduled Issue Triage #1528
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' | |
| # Allow the Gemini CLI to run in CI's non-interactive checkout. Without this it treats the | |
| # workspace as "untrusted", refuses to start, and exits 1 (failing the check). See | |
| # https://geminicli.com/docs/cli/trusted-folders/#headless-and-automated-environments | |
| env: | |
| GEMINI_CLI_TRUST_WORKSPACE: 'true' | |
| 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/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 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...' | |
| ISSUES_NO_LABEL="$(gh issue list \ | |
| --state 'open' \ | |
| --search 'no:label' \ | |
| --json number,title,body \ | |
| --limit '100' \ | |
| --repo "${GITHUB_REPOSITORY}" || echo "[]")" | |
| echo 'π Finding issues marked for triage...' | |
| ISSUES_NEEDS_TRIAGE="$(gh issue list \ | |
| --state 'open' \ | |
| --search 'label:"status/needs-triage"' \ | |
| --json number,title,body \ | |
| --limit '100' \ | |
| --repo "${GITHUB_REPOSITORY}" || echo "[]")" | |
| echo 'π Combining and setting output...' | |
| # Combine arrays and deduplicate by number just in case | |
| ISSUES="$(echo "${ISSUES_NO_LABEL}" "${ISSUES_NEEDS_TRIAGE}" | jq -s -c 'add | unique_by(.number)')" | |
| 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.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/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, | |
| }); | |
| } |