diff --git a/.github/workflows/gemini-cli.yml b/.github/workflows/gemini-cli.yml new file mode 100644 index 0000000..fb1f71c --- /dev/null +++ b/.github/workflows/gemini-cli.yml @@ -0,0 +1,17 @@ +name: Run gemini-cli + +on: + issue_comment: + types: [created] + +permissions: write-all + +jobs: + gemini-code-review: + runs-on: ubuntu-latest + if: | + github.event.issue.pull_request && + contains(github.event.comment.body, '/gemini-cli') + steps: + - name: Run Gemini CLI + uses: google-github-actions/run-gemini-cli@v0.1.16 diff --git a/.github/workflows/gemini-dispatch.yml b/.github/workflows/gemini-dispatch.yml new file mode 100644 index 0000000..e44b96e --- /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..a1c85b4 --- /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..d87af24 --- /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..ba88aab --- /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..4515be7 --- /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.`) + } diff --git a/.gitignore b/.gitignore index eebea21..726d9a1 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,11 @@ yarn-error.log* # Vuepress .cache -.temp \ No newline at end of file +.temp + + +# gemini-cli settings +.gemini/ + +# GitHub App credentials +gha-creds-*.json \ No newline at end of file