summaryrefslogtreecommitdiff
path: root/.github/workflows
diff options
context:
space:
mode:
Diffstat (limited to '.github/workflows')
-rw-r--r--.github/workflows/gemini-dispatch.yml204
-rw-r--r--.github/workflows/gemini-invoke.yml122
-rw-r--r--.github/workflows/gemini-review.yml110
-rw-r--r--.github/workflows/gemini-scheduled-triage.yml214
-rw-r--r--.github/workflows/gemini-triage.yml158
5 files changed, 808 insertions, 0 deletions
diff --git a/.github/workflows/gemini-dispatch.yml b/.github/workflows/gemini-dispatch.yml
new file mode 100644
index 0000000..22d0b27
--- /dev/null
+++ b/.github/workflows/gemini-dispatch.yml
@@ -0,0 +1,204 @@
+name: '🔀 Gemini Dispatch'
+
+on:
+ pull_request_review_comment:
+ types:
+ - 'created'
+ pull_request_review:
+ types:
+ - 'submitted'
+ pull_request:
+ types:
+ - 'opened'
+ issues:
+ types:
+ - 'opened'
+ - 'reopened'
+ issue_comment:
+ types:
+ - 'created'
+
+defaults:
+ run:
+ shell: 'bash'
+
+jobs:
+ debugger:
+ if: |-
+ ${{ fromJSON(vars.DEBUG || vars.ACTIONS_STEP_DEBUG || false) }}
+ runs-on: 'ubuntu-latest'
+ permissions:
+ contents: 'read'
+ steps:
+ - name: 'Print context for debugging'
+ env:
+ DEBUG_event_name: '${{ github.event_name }}'
+ DEBUG_event__action: '${{ github.event.action }}'
+ DEBUG_event__comment__author_association: '${{ github.event.comment.author_association }}'
+ DEBUG_event__issue__author_association: '${{ github.event.issue.author_association }}'
+ DEBUG_event__pull_request__author_association: '${{ github.event.pull_request.author_association }}'
+ DEBUG_event__review__author_association: '${{ github.event.review.author_association }}'
+ DEBUG_event: '${{ toJSON(github.event) }}'
+ run: |-
+ env | grep '^DEBUG_'
+
+ dispatch:
+ # For PRs: only if not from a fork
+ # For issues: only on open/reopen
+ # For comments: only if user types @gemini-cli and is OWNER/MEMBER/COLLABORATOR
+ if: |-
+ (
+ github.event_name == 'pull_request' &&
+ github.event.pull_request.head.repo.fork == false
+ ) || (
+ github.event_name == 'issues' &&
+ contains(fromJSON('["opened", "reopened"]'), github.event.action)
+ ) || (
+ github.event.sender.type == 'User' &&
+ startsWith(github.event.comment.body || github.event.review.body || github.event.issue.body, '@gemini-cli') &&
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association || github.event.review.author_association || github.event.issue.author_association)
+ )
+ runs-on: 'ubuntu-latest'
+ permissions:
+ contents: 'read'
+ issues: 'write'
+ pull-requests: 'write'
+ outputs:
+ command: '${{ steps.extract_command.outputs.command }}'
+ request: '${{ steps.extract_command.outputs.request }}'
+ additional_context: '${{ steps.extract_command.outputs.additional_context }}'
+ issue_number: '${{ github.event.pull_request.number || github.event.issue.number }}'
+ 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: 'Extract command'
+ id: 'extract_command'
+ uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v7
+ env:
+ EVENT_TYPE: '${{ github.event_name }}.${{ github.event.action }}'
+ REQUEST: '${{ github.event.comment.body || github.event.review.body || github.event.issue.body }}'
+ with:
+ script: |
+ const eventType = process.env.EVENT_TYPE;
+ const request = process.env.REQUEST;
+ core.setOutput('request', request);
+
+ if (eventType === 'pull_request.opened') {
+ core.setOutput('command', 'review');
+ } else if (['issues.opened', 'issues.reopened'].includes(eventType)) {
+ core.setOutput('command', 'triage');
+ } else if (request.startsWith("@gemini-cli /review")) {
+ core.setOutput('command', 'review');
+ const additionalContext = request.replace(/^@gemini-cli \/review/, '').trim();
+ core.setOutput('additional_context', additionalContext);
+ } else if (request.startsWith("@gemini-cli /triage")) {
+ core.setOutput('command', 'triage');
+ } else if (request.startsWith("@gemini-cli")) {
+ const additionalContext = request.replace(/^@gemini-cli/, '').trim();
+ core.setOutput('command', 'invoke');
+ core.setOutput('additional_context', additionalContext);
+ } else {
+ core.setOutput('command', 'fallthrough');
+ }
+
+ - name: 'Acknowledge request'
+ env:
+ GITHUB_TOKEN: '${{ steps.mint_identity_token.outputs.token || secrets.GITHUB_TOKEN || github.token }}'
+ ISSUE_NUMBER: '${{ github.event.pull_request.number || github.event.issue.number }}'
+ MESSAGE: |-
+ 🤖 Hi @${{ github.actor }}, I've received your request, and I'm working on it now! You can track my progress [in the logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details.
+ REPOSITORY: '${{ github.repository }}'
+ run: |-
+ gh issue comment "${ISSUE_NUMBER}" \
+ --body "${MESSAGE}" \
+ --repo "${REPOSITORY}"
+
+ review:
+ needs: 'dispatch'
+ if: |-
+ ${{ needs.dispatch.outputs.command == 'review' }}
+ uses: './.github/workflows/gemini-review.yml'
+ permissions:
+ contents: 'read'
+ id-token: 'write'
+ issues: 'write'
+ pull-requests: 'write'
+ with:
+ additional_context: '${{ needs.dispatch.outputs.additional_context }}'
+ secrets: 'inherit'
+
+ triage:
+ needs: 'dispatch'
+ if: |-
+ ${{ needs.dispatch.outputs.command == 'triage' }}
+ uses: './.github/workflows/gemini-triage.yml'
+ permissions:
+ contents: 'read'
+ id-token: 'write'
+ issues: 'write'
+ pull-requests: 'write'
+ with:
+ additional_context: '${{ needs.dispatch.outputs.additional_context }}'
+ secrets: 'inherit'
+
+ invoke:
+ needs: 'dispatch'
+ if: |-
+ ${{ needs.dispatch.outputs.command == 'invoke' }}
+ uses: './.github/workflows/gemini-invoke.yml'
+ permissions:
+ contents: 'read'
+ id-token: 'write'
+ issues: 'write'
+ pull-requests: 'write'
+ with:
+ additional_context: '${{ needs.dispatch.outputs.additional_context }}'
+ secrets: 'inherit'
+
+ fallthrough:
+ needs:
+ - 'dispatch'
+ - 'review'
+ - 'triage'
+ - 'invoke'
+ if: |-
+ ${{ always() && !cancelled() && (failure() || needs.dispatch.outputs.command == 'fallthrough') }}
+ runs-on: 'ubuntu-latest'
+ 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: 'Send failure comment'
+ env:
+ GITHUB_TOKEN: '${{ steps.mint_identity_token.outputs.token || secrets.GITHUB_TOKEN || github.token }}'
+ ISSUE_NUMBER: '${{ github.event.pull_request.number || github.event.issue.number }}'
+ MESSAGE: |-
+ 🤖 I'm sorry @${{ github.actor }}, but I was unable to process your request. Please [see the logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details.
+ REPOSITORY: '${{ github.repository }}'
+ run: |-
+ gh issue comment "${ISSUE_NUMBER}" \
+ --body "${MESSAGE}" \
+ --repo "${REPOSITORY}"
diff --git a/.github/workflows/gemini-invoke.yml b/.github/workflows/gemini-invoke.yml
new file mode 100644
index 0000000..e59e55d
--- /dev/null
+++ b/.github/workflows/gemini-invoke.yml
@@ -0,0 +1,122 @@
+name: '▶️ Gemini Invoke'
+
+on:
+ workflow_call:
+ inputs:
+ additional_context:
+ type: 'string'
+ description: 'Any additional context from the request'
+ required: false
+
+concurrency:
+ group: '${{ github.workflow }}-invoke-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.issue.number }}'
+ cancel-in-progress: false
+
+defaults:
+ run:
+ shell: 'bash'
+
+jobs:
+ invoke:
+ runs-on: 'ubuntu-latest'
+ permissions:
+ contents: 'read'
+ id-token: 'write'
+ 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: 'Run Gemini CLI'
+ id: 'run_gemini'
+ uses: 'google-github-actions/run-gemini-cli@v0' # ratchet:exclude
+ env:
+ TITLE: '${{ github.event.pull_request.title || github.event.issue.title }}'
+ DESCRIPTION: '${{ github.event.pull_request.body || github.event.issue.body }}'
+ EVENT_NAME: '${{ github.event_name }}'
+ GITHUB_TOKEN: '${{ steps.mint_identity_token.outputs.token || secrets.GITHUB_TOKEN || github.token }}'
+ IS_PULL_REQUEST: '${{ !!github.event.pull_request }}'
+ ISSUE_NUMBER: '${{ github.event.pull_request.number || github.event.issue.number }}'
+ REPOSITORY: '${{ github.repository }}'
+ ADDITIONAL_CONTEXT: '${{ inputs.additional_context }}'
+ 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-invoke'
+ settings: |-
+ {
+ "model": {
+ "maxSessionTurns": 25
+ },
+ "telemetry": {
+ "enabled": true,
+ "target": "local",
+ "outfile": ".gemini/telemetry.log"
+ },
+ "mcpServers": {
+ "github": {
+ "command": "docker",
+ "args": [
+ "run",
+ "-i",
+ "--rm",
+ "-e",
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
+ "ghcr.io/github/github-mcp-server:v0.18.0"
+ ],
+ "includeTools": [
+ "add_issue_comment",
+ "get_issue",
+ "get_issue_comments",
+ "list_issues",
+ "search_issues",
+ "create_pull_request",
+ "pull_request_read",
+ "list_pull_requests",
+ "search_pull_requests",
+ "create_branch",
+ "create_or_update_file",
+ "delete_file",
+ "fork_repository",
+ "get_commit",
+ "get_file_contents",
+ "list_commits",
+ "push_files",
+ "search_code"
+ ],
+ "env": {
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
+ }
+ }
+ },
+ "tools": {
+ "core": [
+ "run_shell_command(cat)",
+ "run_shell_command(echo)",
+ "run_shell_command(grep)",
+ "run_shell_command(head)",
+ "run_shell_command(tail)"
+ ]
+ }
+ }
+ prompt: '/gemini-invoke'
diff --git a/.github/workflows/gemini-review.yml b/.github/workflows/gemini-review.yml
new file mode 100644
index 0000000..d3b43a1
--- /dev/null
+++ b/.github/workflows/gemini-review.yml
@@ -0,0 +1,110 @@
+name: '🔎 Gemini Review'
+
+on:
+ workflow_call:
+ inputs:
+ additional_context:
+ type: 'string'
+ description: 'Any additional context from the request'
+ required: false
+
+concurrency:
+ group: '${{ github.workflow }}-review-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.issue.number }}'
+ cancel-in-progress: true
+
+defaults:
+ run:
+ shell: 'bash'
+
+jobs:
+ review:
+ runs-on: 'ubuntu-latest'
+ timeout-minutes: 7
+ permissions:
+ contents: 'read'
+ id-token: 'write'
+ 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: 'Checkout repository'
+ uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
+
+ - name: 'Run Gemini pull request review'
+ uses: 'google-github-actions/run-gemini-cli@v0' # ratchet:exclude
+ id: 'gemini_pr_review'
+ env:
+ GITHUB_TOKEN: '${{ steps.mint_identity_token.outputs.token || secrets.GITHUB_TOKEN || github.token }}'
+ ISSUE_TITLE: '${{ github.event.pull_request.title || github.event.issue.title }}'
+ ISSUE_BODY: '${{ github.event.pull_request.body || github.event.issue.body }}'
+ PULL_REQUEST_NUMBER: '${{ github.event.pull_request.number || github.event.issue.number }}'
+ REPOSITORY: '${{ github.repository }}'
+ ADDITIONAL_CONTEXT: '${{ inputs.additional_context }}'
+ 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-review'
+ settings: |-
+ {
+ "model": {
+ "maxSessionTurns": 25
+ },
+ "telemetry": {
+ "enabled": true,
+ "target": "local",
+ "outfile": ".gemini/telemetry.log"
+ },
+ "mcpServers": {
+ "github": {
+ "command": "docker",
+ "args": [
+ "run",
+ "-i",
+ "--rm",
+ "-e",
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
+ "ghcr.io/github/github-mcp-server:v0.18.0"
+ ],
+ "includeTools": [
+ "add_comment_to_pending_review",
+ "create_pending_pull_request_review",
+ "pull_request_read",
+ "submit_pending_pull_request_review"
+ ],
+ "env": {
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
+ }
+ }
+ },
+ "tools": {
+ "core": [
+ "run_shell_command(cat)",
+ "run_shell_command(echo)",
+ "run_shell_command(grep)",
+ "run_shell_command(head)",
+ "run_shell_command(tail)"
+ ]
+ }
+ }
+ prompt: '/gemini-review'
diff --git a/.github/workflows/gemini-scheduled-triage.yml b/.github/workflows/gemini-scheduled-triage.yml
new file mode 100644
index 0000000..46bb71f
--- /dev/null
+++ b/.github/workflows/gemini-scheduled-triage.yml
@@ -0,0 +1,214 @@
+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/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 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.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,
+ });
+ }
diff --git a/.github/workflows/gemini-triage.yml b/.github/workflows/gemini-triage.yml
new file mode 100644
index 0000000..581acbb
--- /dev/null
+++ b/.github/workflows/gemini-triage.yml
@@ -0,0 +1,158 @@
+name: '🔀 Gemini Triage'
+
+on:
+ workflow_call:
+ inputs:
+ additional_context:
+ type: 'string'
+ description: 'Any additional context from the request'
+ required: false
+
+concurrency:
+ group: '${{ github.workflow }}-triage-${{ github.event_name }}-${{ github.event.pull_request.number || github.event.issue.number }}'
+ cancel-in-progress: true
+
+defaults:
+ run:
+ shell: 'bash'
+
+jobs:
+ triage:
+ runs-on: 'ubuntu-latest'
+ timeout-minutes: 7
+ outputs:
+ available_labels: '${{ steps.get_labels.outputs.available_labels }}'
+ selected_labels: '${{ env.SELECTED_LABELS }}'
+ permissions:
+ contents: 'read'
+ id-token: 'write'
+ issues: 'read'
+ pull-requests: 'read'
+ 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 given 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: 'Run Gemini issue analysis'
+ id: 'gemini_analysis'
+ if: |-
+ ${{ steps.get_labels.outputs.available_labels != '' }}
+ uses: 'google-github-actions/run-gemini-cli@v0' # ratchet:exclude
+ env:
+ GITHUB_TOKEN: '' # Do NOT pass any auth tokens here since this runs on untrusted inputs
+ ISSUE_TITLE: '${{ github.event.issue.title }}'
+ ISSUE_BODY: '${{ github.event.issue.body }}'
+ 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-triage'
+ settings: |-
+ {
+ "model": {
+ "maxSessionTurns": 25
+ },
+ "telemetry": {
+ "enabled": true,
+ "target": "local",
+ "outfile": ".gemini/telemetry.log"
+ },
+ "tools": {
+ "core": [
+ "run_shell_command(echo)"
+ ]
+ }
+ }
+ prompt: '/gemini-triage'
+
+ label:
+ runs-on: 'ubuntu-latest'
+ needs:
+ - 'triage'
+ if: |-
+ ${{ needs.triage.outputs.selected_labels != '' }}
+ 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:
+ ISSUE_NUMBER: '${{ github.event.issue.number }}'
+ AVAILABLE_LABELS: '${{ needs.triage.outputs.available_labels }}'
+ SELECTED_LABELS: '${{ needs.triage.outputs.selected_labels }}'
+ 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 the label as a CSV, reject invalid ones - we do this just
+ // in case someone was able to prompt inject malicious labels.
+ const selectedLabels = (process.env.SELECTED_LABELS || '').split(',')
+ .map((label) => label.trim())
+ .filter((label) => availableLabels.includes(label))
+ .sort()
+
+ // Set the labels
+ const issueNumber = process.env.ISSUE_NUMBER;
+ if (selectedLabels && selectedLabels.length > 0) {
+ await github.rest.issues.setLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: issueNumber,
+ labels: selectedLabels,
+ });
+ core.info(`Successfully set labels: ${selectedLabels.join(',')}`);
+ } else {
+ core.info(`Failed to determine labels to set. There may not be enough information in the issue or pull request.`)
+ }