google-gemini/gemini-cli

41 workflows · maturity 83% · 10 patterns · GitHub ↗

Security 23.15/100

Practices

✓ Matrix✓ Permissions✓ Security scan○ AI review✓ Cache✓ Concurrency○ Reusable workflows

Detected patterns

Security dimensions

permissions
7.3
security scan
8.3
supply chain
0
secret handling
7.5
harden runner
0

Tools: github/codeql-action/analyze, github/codeql-action/init

Workflows (41)

chained_e2e matrix perms .github/workflows/chained_e2e.yml
Triggers
push, merge_group, workflow_run, workflow_dispatch
Runs on
gemini-cli-ubuntu-16-core, gemini-cli-ubuntu-16-core, gemini-cli-ubuntu-16-core, gemini-cli-ubuntu-16-core, gemini-cli-ubuntu-16-core, macos-latest, gemini-cli-windows-16-core, gemini-cli-ubuntu-16-core, gemini-cli-ubuntu-16-core, gemini-cli-ubuntu-16-core
Jobs
merge_queue_skipper, download_repo_name, parse_run_context, set_pending_status, e2e_linux, e2e_mac, e2e_windows, evals, e2e, set_workflow_status
Matrix
node-version, sandbox→ 20.x, sandbox:docker, sandbox:none
Actions
cariad-tech/merge-queue-ci-skipper, myrotvorets/set-commit-status-action, docker/setup-buildx-action, myrotvorets/set-commit-status-action
Commands
  • mkdir -p ./pr echo "${REPO_NAME}" > ./pr/repo_name
  • echo "REPO=$REPO" >> "$GITHUB_OUTPUT" echo "SHA=$SHA" >> "$GITHUB_OUTPUT"
  • npm ci
  • npm run build
  • if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then npm run test:integration:sandbox:docker else npm run test:integration:sandbox:none fi
  • npm ci
  • npm run build
  • npm cache clean --force
View raw YAML
name: 'Testing: E2E (Chained)'

on:
  push:
    branches:
      - 'main'
  merge_group:
  workflow_run:
    workflows: ['Trigger E2E']
    types: ['completed']
  workflow_dispatch:
    inputs:
      head_sha:
        description: 'SHA of the commit to test'
        required: true
      repo_name:
        description: 'Repository name (e.g., owner/repo)'
        required: true

concurrency:
  group: '${{ github.workflow }}-${{ github.head_ref || github.event.workflow_run.head_branch || github.ref }}'
  cancel-in-progress: |-
    ${{ github.event_name != 'push' && github.event_name != 'merge_group' }}

permissions:
  contents: 'read'
  statuses: 'write'

jobs:
  merge_queue_skipper:
    name: 'Merge Queue Skipper'
    permissions: 'read-all'
    runs-on: 'gemini-cli-ubuntu-16-core'
    if: "github.repository == 'google-gemini/gemini-cli'"
    outputs:
      skip: '${{ steps.merge-queue-e2e-skipper.outputs.skip-check }}'
    steps:
      - id: 'merge-queue-e2e-skipper'
        uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
        with:
          secret: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
    continue-on-error: true

  download_repo_name:
    runs-on: 'gemini-cli-ubuntu-16-core'
    if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run')"
    outputs:
      repo_name: '${{ steps.output-repo-name.outputs.repo_name }}'
      head_sha: '${{ steps.output-repo-name.outputs.head_sha }}'
    steps:
      - name: 'Mock Repo Artifact'
        if: "${{ github.event_name == 'workflow_dispatch' }}"
        env:
          REPO_NAME: '${{ github.event.inputs.repo_name }}'
        run: |
          mkdir -p ./pr
          echo "${REPO_NAME}" > ./pr/repo_name
      - uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
        with:
          name: 'repo_name'
          path: 'pr/'
      - name: 'Download the repo_name artifact'
        uses: 'actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0' # ratchet:actions/download-artifact@v5
        env:
          RUN_ID: "${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || github.run_id  }}"
        with:
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          name: 'repo_name'
          run-id: '${{ env.RUN_ID }}'
          path: '${{ runner.temp }}/artifacts'
      - name: 'Output Repo Name and SHA'
        id: 'output-repo-name'
        uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # ratchet:actions/github-script@v8
        with:
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          script: |
            const fs = require('fs');
            const path = require('path');
            const temp = '${{ runner.temp }}/artifacts';
            const repoPath = path.join(temp, 'repo_name');
            if (fs.existsSync(repoPath)) {
              const repo_name = String(fs.readFileSync(repoPath)).trim();
              core.setOutput('repo_name', repo_name);
            }
            const shaPath = path.join(temp, 'head_sha');
            if (fs.existsSync(shaPath)) {
              const head_sha = String(fs.readFileSync(shaPath)).trim();
              core.setOutput('head_sha', head_sha);
            }

  parse_run_context:
    name: 'Parse run context'
    runs-on: 'gemini-cli-ubuntu-16-core'
    needs: 'download_repo_name'
    if: "github.repository == 'google-gemini/gemini-cli' && always()"
    outputs:
      repository: '${{ steps.set_context.outputs.REPO }}'
      sha: '${{ steps.set_context.outputs.SHA }}'
    steps:
      - id: 'set_context'
        name: 'Set dynamic repository and SHA'
        env:
          REPO: '${{ needs.download_repo_name.outputs.repo_name || github.repository }}'
          SHA: '${{ needs.download_repo_name.outputs.head_sha || github.event.inputs.head_sha || github.event.workflow_run.head_sha || github.sha }}'
        shell: 'bash'
        run: |
          echo "REPO=$REPO" >> "$GITHUB_OUTPUT"
          echo "SHA=$SHA" >> "$GITHUB_OUTPUT"

  set_pending_status:
    runs-on: 'gemini-cli-ubuntu-16-core'
    permissions: 'write-all'
    needs:
      - 'parse_run_context'
    if: "github.repository == 'google-gemini/gemini-cli' && always()"
    steps:
      - name: 'Set pending status'
        uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
        if: "github.repository == 'google-gemini/gemini-cli' && always()"
        with:
          allowForks: 'true'
          repo: '${{ github.repository }}'
          sha: '${{ needs.parse_run_context.outputs.sha }}'
          token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          status: 'pending'
          context: 'E2E (Chained)'

  e2e_linux:
    name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
    needs:
      - 'merge_queue_skipper'
      - 'parse_run_context'
    runs-on: 'gemini-cli-ubuntu-16-core'
    if: |
      github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
    strategy:
      fail-fast: false
      matrix:
        sandbox:
          - 'sandbox:none'
          - 'sandbox:docker'
        node-version:
          - '20.x'

    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
        with:
          ref: '${{ needs.parse_run_context.outputs.sha }}'
          repository: '${{ needs.parse_run_context.outputs.repository }}'

      - name: 'Set up Node.js ${{ matrix.node-version }}'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
        with:
          node-version: '${{ matrix.node-version }}'

      - name: 'Install dependencies'
        run: 'npm ci'

      - name: 'Build project'
        run: 'npm run build'

      - name: 'Set up Docker'
        if: "${{matrix.sandbox == 'sandbox:docker'}}"
        uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3

      - name: 'Run E2E tests'
        env:
          GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
          KEEP_OUTPUT: 'true'
          VERBOSE: 'true'
          BUILD_SANDBOX_FLAGS: '--cache-from type=gha --cache-to type=gha,mode=max'
        shell: 'bash'
        run: |
          if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
            npm run test:integration:sandbox:docker
          else
            npm run test:integration:sandbox:none
          fi

  e2e_mac:
    name: 'E2E Test (macOS)'
    needs:
      - 'merge_queue_skipper'
      - 'parse_run_context'
    runs-on: 'macos-latest'
    if: |
      github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
        with:
          ref: '${{ needs.parse_run_context.outputs.sha }}'
          repository: '${{ needs.parse_run_context.outputs.repository }}'

      - name: 'Set up Node.js 20.x'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
        with:
          node-version: '20.x'

      - name: 'Install dependencies'
        run: 'npm ci'

      - name: 'Build project'
        run: 'npm run build'

      - name: 'Fix rollup optional dependencies on macOS'
        if: "${{runner.os == 'macOS'}}"
        run: |
          npm cache clean --force
      - name: 'Run E2E tests (non-Windows)'
        if: "${{runner.os != 'Windows'}}"
        env:
          GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
          KEEP_OUTPUT: 'true'
          SANDBOX: 'sandbox:none'
          VERBOSE: 'true'
        run: 'npm run test:integration:sandbox:none'

  e2e_windows:
    name: 'Slow E2E - Win'
    needs:
      - 'merge_queue_skipper'
      - 'parse_run_context'
    if: |
      github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
    runs-on: 'gemini-cli-windows-16-core'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
        with:
          ref: '${{ needs.parse_run_context.outputs.sha }}'
          repository: '${{ needs.parse_run_context.outputs.repository }}'

      - name: 'Set up Node.js 20.x'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
        with:
          node-version: '20.x'
          cache: 'npm'

      - name: 'Configure Windows Defender exclusions'
        run: |
          Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
          Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
          Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
          Add-MpPreference -ExclusionPath "$env:TEMP" -Force
        shell: 'pwsh'

      - name: 'Configure npm for Windows performance'
        run: |
          npm config set progress false
          npm config set audit false
          npm config set fund false
          npm config set loglevel error
          npm config set maxsockets 32
          npm config set registry https://registry.npmjs.org/
        shell: 'pwsh'

      - name: 'Install dependencies'
        run: 'npm ci'
        shell: 'pwsh'

      - name: 'Build project'
        run: 'npm run build'
        shell: 'pwsh'

      - name: 'Ensure Chrome is available'
        shell: 'pwsh'
        run: |
          $chromePaths = @(
            "${env:ProgramFiles}\Google\Chrome\Application\chrome.exe",
            "${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe"
          )
          $chromeExists = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
          if (-not $chromeExists) {
            Write-Host 'Chrome not found, installing via Chocolatey...'
            choco install googlechrome -y --no-progress --ignore-checksums
          }
          $installed = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
          if ($installed) {
            Write-Host "Chrome found at: $installed"
            & $installed --version
          } else {
            Write-Error 'Chrome installation failed'
            exit 1
          }

      - name: 'Run E2E tests'
        env:
          GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
          KEEP_OUTPUT: 'true'
          SANDBOX: 'sandbox:none'
          VERBOSE: 'true'
          NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
          UV_THREADPOOL_SIZE: '32'
          NODE_ENV: 'test'
        shell: 'pwsh'
        run: 'npm run test:integration:sandbox:none'

  evals:
    name: 'Evals (ALWAYS_PASSING)'
    needs:
      - 'merge_queue_skipper'
      - 'parse_run_context'
    runs-on: 'gemini-cli-ubuntu-16-core'
    if: |
      github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
        with:
          ref: '${{ needs.parse_run_context.outputs.sha }}'
          repository: '${{ needs.parse_run_context.outputs.repository }}'
          fetch-depth: 0

      - name: 'Set up Node.js 20.x'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
        with:
          node-version: '20.x'

      - name: 'Install dependencies'
        run: 'npm ci'

      - name: 'Build project'
        run: 'npm run build'

      - name: 'Check if evals should run'
        id: 'check_evals'
        run: |
          SHOULD_RUN=$(node scripts/changed_prompt.js)
          echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"

      - name: 'Run Evals (Required to pass)'
        if: "${{ steps.check_evals.outputs.should_run == 'true' }}"
        env:
          GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
          GEMINI_MODEL: 'gemini-3-pro-preview'
          # Disable Vitest internal retries to avoid double-retrying;
          # custom retry logic is handled in evals/test-helper.ts
          VITEST_RETRY: 0
        run: 'npm run test:always_passing_evals'

      - name: 'Upload Reliability Logs'
        if: "always() && steps.check_evals.outputs.should_run == 'true'"
        uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
        with:
          name: 'eval-logs-${{ github.run_id }}-${{ github.run_attempt }}'
          path: 'evals/logs/api-reliability.jsonl'
          retention-days: 7

  e2e:
    name: 'E2E'
    if: |
      github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
    needs:
      - 'e2e_linux'
      - 'e2e_mac'
      - 'e2e_windows'
      - 'evals'
      - 'merge_queue_skipper'
    runs-on: 'gemini-cli-ubuntu-16-core'
    steps:
      - name: 'Check E2E test results'
        run: |
          if [[ ${NEEDS_E2E_LINUX_RESULT} != 'success' || \
               ${NEEDS_E2E_MAC_RESULT} != 'success' || \
               ${NEEDS_E2E_WINDOWS_RESULT} != 'success' || \
               ${NEEDS_EVALS_RESULT} != 'success' ]]; then
            echo "One or more E2E jobs failed."
            exit 1
          fi
          echo "All required E2E jobs passed!"
        env:
          NEEDS_E2E_LINUX_RESULT: '${{ needs.e2e_linux.result }}'
          NEEDS_E2E_MAC_RESULT: '${{ needs.e2e_mac.result }}'
          NEEDS_E2E_WINDOWS_RESULT: '${{ needs.e2e_windows.result }}'
          NEEDS_EVALS_RESULT: '${{ needs.evals.result }}'

  set_workflow_status:
    runs-on: 'gemini-cli-ubuntu-16-core'
    permissions: 'write-all'
    if: "github.repository == 'google-gemini/gemini-cli' && always()"
    needs:
      - 'parse_run_context'
      - 'e2e'
    steps:
      - name: 'Set workflow status'
        uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
        if: "github.repository == 'google-gemini/gemini-cli' && always()"
        with:
          allowForks: 'true'
          repo: '${{ github.repository }}'
          sha: '${{ needs.parse_run_context.outputs.sha }}'
          token: '${{ secrets.GITHUB_TOKEN }}'
          status: '${{ needs.e2e.result }}'
          context: 'E2E (Chained)'
ci matrix perms security .github/workflows/ci.yml
Triggers
push, pull_request, merge_group, workflow_dispatch
Runs on
gemini-cli-ubuntu-16-core, gemini-cli-ubuntu-16-core, ubuntu-latest, gemini-cli-ubuntu-16-core, macos-latest, gemini-cli-ubuntu-16-core, gemini-cli-ubuntu-16-core, gemini-cli-windows-16-core, gemini-cli-ubuntu-16-core
Jobs
merge_queue_skipper, lint, link_checker, test_linux, test_mac, codeql, bundle_size, test_windows, ci
Matrix
node-version, shard→ 20.x, 22.x, 24.x, cli, others
Actions
cariad-tech/merge-queue-ci-skipper, lycheeverse/lychee-action, dorny/test-reporter, dorny/test-reporter, github/codeql-action/init, github/codeql-action/analyze, preactjs/compressed-size-action
Commands
  • npm ci
  • git diff --exit-code packages/vscode-ide-companion/NOTICES.txt
  • npm run check:lockfile
  • node scripts/lint.js --setup
  • node scripts/lint.js --eslint
  • node scripts/lint.js --actionlint
  • node scripts/lint.js --shellcheck
  • node scripts/lint.js --yamllint
View raw YAML
name: 'Testing: CI'

on:
  push:
    branches:
      - 'main'
      - 'release/**'
  pull_request:
    branches:
      - 'main'
      - 'release/**'
  merge_group:
  workflow_dispatch:
    inputs:
      branch_ref:
        description: 'Branch to run on'
        required: true
        default: 'main'
        type: 'string'

concurrency:
  group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
  cancel-in-progress: |-
    ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}

permissions:
  checks: 'write'
  contents: 'read'
  statuses: 'write'

defaults:
  run:
    shell: 'bash'

jobs:
  merge_queue_skipper:
    permissions: 'read-all'
    name: 'Merge Queue Skipper'
    runs-on: 'gemini-cli-ubuntu-16-core'
    if: "github.repository == 'google-gemini/gemini-cli'"
    outputs:
      skip: '${{ steps.merge-queue-ci-skipper.outputs.skip-check }}'
    steps:
      - id: 'merge-queue-ci-skipper'
        uses: 'cariad-tech/merge-queue-ci-skipper@1032489e59437862c90a08a2c92809c903883772' # ratchet:cariad-tech/merge-queue-ci-skipper@main
        with:
          secret: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'

  lint:
    name: 'Lint'
    runs-on: 'gemini-cli-ubuntu-16-core'
    needs: 'merge_queue_skipper'
    if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
    env:
      GEMINI_LINT_TEMP_DIR: '${{ github.workspace }}/.gemini-linters'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
        with:
          ref: '${{ github.event.inputs.branch_ref || github.ref }}'
          fetch-depth: 0

      - name: 'Set up Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'Cache Linters'
        uses: 'actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830' # ratchet:actions/cache@v4
        with:
          path: '${{ env.GEMINI_LINT_TEMP_DIR }}'
          key: "${{ runner.os }}-${{ runner.arch }}-linters-${{ hashFiles('scripts/lint.js') }}"

      - name: 'Install dependencies'
        run: 'npm ci'

      - name: 'Cache ESLint'
        uses: 'actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830' # ratchet:actions/cache@v4
        with:
          path: '.eslintcache'
          key: "${{ runner.os }}-eslint-${{ hashFiles('package-lock.json', 'eslint.config.js') }}"

      - name: 'Validate NOTICES.txt'
        run: 'git diff --exit-code packages/vscode-ide-companion/NOTICES.txt'

      - name: 'Check lockfile'
        run: 'npm run check:lockfile'

      - name: 'Install linters'
        run: 'node scripts/lint.js --setup'

      - name: 'Run ESLint'
        run: 'node scripts/lint.js --eslint'

      - name: 'Run actionlint'
        run: 'node scripts/lint.js --actionlint'

      - name: 'Run shellcheck'
        run: 'node scripts/lint.js --shellcheck'

      - name: 'Run yamllint'
        run: 'node scripts/lint.js --yamllint'

      - name: 'Run Prettier'
        run: 'node scripts/lint.js --prettier'

      - name: 'Build docs prerequisites'
        run: 'npm run predocs:settings'

      - name: 'Verify settings docs'
        run: 'npm run docs:settings -- --check'

      - name: 'Run sensitive keyword linter'
        run: 'node scripts/lint.js --sensitive-keywords'

      - name: 'Run GitHub Actions pinning linter'
        run: 'node scripts/lint.js --check-github-actions-pinning'

  link_checker:
    name: 'Link Checker'
    runs-on: 'ubuntu-latest'
    if: "github.repository == 'google-gemini/gemini-cli'"
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
      - name: 'Link Checker'
        uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
        with:
          args: '--verbose --accept 200,503 ./**/*.md'
          fail: true
  test_linux:
    name: 'Test (Linux) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
    runs-on: 'gemini-cli-ubuntu-16-core'
    needs:
      - 'merge_queue_skipper'
    if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
    permissions:
      contents: 'read'
      checks: 'write'
      pull-requests: 'write'
    strategy:
      matrix:
        node-version:
          - '20.x'
          - '22.x'
          - '24.x'
        shard:
          - 'cli'
          - 'others'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Set up Node.js ${{ matrix.node-version }}'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version: '${{ matrix.node-version }}'
          cache: 'npm'

      - name: 'Build project'
        run: 'npm run build'

      - name: 'Install system dependencies'
        run: |
          sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
          # Ubuntu 24.04+ requires this to allow bwrap to function in CI
          sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true

      - name: 'Install dependencies for testing'
        run: 'npm ci'

      - name: 'Run tests and generate reports'
        env:
          NO_COLOR: true
        run: |
          if [[ "${{ matrix.shard }}" == "cli" ]]; then
            npm run test:ci --workspace @google/gemini-cli
          else
            # Explicitly list non-cli packages to ensure they are sharded correctly
            npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
            npm run test:scripts
          fi

      - name: 'Bundle'
        run: 'npm run bundle'

      - name: 'Smoke test bundle'
        run: 'node ./bundle/gemini.js --version'

      - name: 'Smoke test npx installation'
        run: |
          # 1. Package the project into a tarball
          TARBALL=$(npm pack | tail -n 1)

          # 2. Move to a fresh directory for isolation
          mkdir -p ../smoke-test-dir
          mv "$TARBALL" ../smoke-test-dir/
          cd ../smoke-test-dir

          # 3. Run npx from the tarball
          npx "./$TARBALL" --version

      - name: 'Wait for file system sync'
        run: 'sleep 2'

      - name: 'Publish Test Report (for non-forks)'
        if: |-
          ${{ always() && (github.event.pull_request.head.repo.full_name == github.repository) }}
        uses: 'dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3' # ratchet:dorny/test-reporter@v2
        with:
          name: 'Test Results (Node ${{ runner.os }}, ${{ matrix.node-version }}, ${{ matrix.shard }})'
          path: 'packages/*/junit.xml'
          reporter: 'java-junit'
          fail-on-error: 'false'

      - name: 'Upload Test Results Artifact (for forks)'
        if: |-
          ${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
        uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
        with:
          name: 'test-results-fork-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
          path: 'packages/*/junit.xml'

  test_mac:
    name: 'Test (Mac) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
    runs-on: 'macos-latest'
    needs:
      - 'merge_queue_skipper'
    if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
    permissions:
      contents: 'read'
      checks: 'write'
      pull-requests: 'write'
    continue-on-error: true
    strategy:
      matrix:
        node-version:
          - '20.x'
          - '22.x'
          - '24.x'
        shard:
          - 'cli'
          - 'others'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Set up Node.js ${{ matrix.node-version }}'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version: '${{ matrix.node-version }}'
          cache: 'npm'

      - name: 'Build project'
        run: 'npm run build'

      - name: 'Install dependencies for testing'
        run: 'npm ci'

      - name: 'Run tests and generate reports'
        env:
          NO_COLOR: true
        run: |
          if [[ "${{ matrix.shard }}" == "cli" ]]; then
            npm run test:ci --workspace @google/gemini-cli -- --coverage.enabled=false
          else
            # Explicitly list non-cli packages to ensure they are sharded correctly
            npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
            npm run test:scripts
          fi

      - name: 'Bundle'
        run: 'npm run bundle'

      - name: 'Smoke test bundle'
        run: 'node ./bundle/gemini.js --version'

      - name: 'Smoke test npx installation'
        run: |
          # 1. Package the project into a tarball
          TARBALL=$(npm pack | tail -n 1)

          # 2. Move to a fresh directory for isolation
          mkdir -p ../smoke-test-dir
          mv "$TARBALL" ../smoke-test-dir/
          cd ../smoke-test-dir

          # 3. Run npx from the tarball
          npx "./$TARBALL" --version

      - name: 'Wait for file system sync'
        run: 'sleep 2'

      - name: 'Publish Test Report (for non-forks)'
        if: |-
          ${{ always() && (github.event.pull_request.head.repo.full_name == github.repository) }}
        uses: 'dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3' # ratchet:dorny/test-reporter@v2
        with:
          name: 'Test Results (Node ${{ runner.os }}, ${{ matrix.node-version }}, ${{ matrix.shard }})'
          path: 'packages/*/junit.xml'
          reporter: 'java-junit'
          fail-on-error: 'false'

      - name: 'Upload Test Results Artifact (for forks)'
        if: |-
          ${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
        uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
        with:
          name: 'test-results-fork-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
          path: 'packages/*/junit.xml'

      - name: 'Upload coverage reports'
        if: |-
          ${{ always() }}
        uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
        with:
          name: 'coverage-reports-${{ runner.os }}-${{ matrix.node-version }}-${{ matrix.shard }}'
          path: 'packages/*/coverage'

  codeql:
    name: 'CodeQL'
    runs-on: 'gemini-cli-ubuntu-16-core'
    needs: 'merge_queue_skipper'
    if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
    permissions:
      actions: 'read'
      contents: 'read'
      security-events: 'write'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
        with:
          ref: '${{ github.event.inputs.branch_ref || github.ref }}'

      - name: 'Initialize CodeQL'
        uses: 'github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2' # ratchet:github/codeql-action/init@v3
        with:
          languages: 'javascript'

      - name: 'Perform CodeQL Analysis'
        uses: 'github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2' # ratchet:github/codeql-action/analyze@v3

  # Check for changes in bundle size.
  bundle_size:
    name: 'Check Bundle Size'
    needs: 'merge_queue_skipper'
    if: "github.repository == 'google-gemini/gemini-cli' && github.event_name == 'pull_request' && needs.merge_queue_skipper.outputs.skip == 'false'"
    runs-on: 'gemini-cli-ubuntu-16-core'
    permissions:
      contents: 'read' # For checkout
      pull-requests: 'write' # For commenting

    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
        with:
          ref: '${{ github.event.inputs.branch_ref || github.ref }}'
          fetch-depth: 1

      - uses: 'preactjs/compressed-size-action@946a292cd35bd1088e0d7eb92b69d1a8d5b5d76a'
        with:
          repo-token: '${{ secrets.GITHUB_TOKEN }}'
          pattern: './bundle/**/*.{js,sb}'
          minimum-change-threshold: '1000'
          compression: 'none'
          clean-script: 'clean'

  test_windows:
    name: 'Slow Test - Win - ${{ matrix.shard }}'
    runs-on: 'gemini-cli-windows-16-core'
    needs: 'merge_queue_skipper'
    if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
    timeout-minutes: 60
    strategy:
      matrix:
        shard:
          - 'cli'
          - 'others'

    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
        with:
          ref: '${{ github.event.inputs.branch_ref || github.ref }}'

      - name: 'Set up Node.js 20.x'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version: '20.x'
          cache: 'npm'

      - name: 'Configure Windows Defender exclusions'
        run: |
          Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
          Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
          Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
          Add-MpPreference -ExclusionPath "$env:TEMP" -Force
        shell: 'pwsh'

      - name: 'Configure npm for Windows performance'
        run: |
          npm config set progress false
          npm config set audit false
          npm config set fund false
          npm config set loglevel error
          npm config set maxsockets 32
          npm config set registry https://registry.npmjs.org/
        shell: 'pwsh'

      - name: 'Install dependencies'
        run: 'npm ci'
        shell: 'pwsh'

      - name: 'Build project'
        run: 'npm run build'
        shell: 'pwsh'
        env:
          NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
          UV_THREADPOOL_SIZE: '32'
          NODE_ENV: 'production'

      - name: 'Run tests and generate reports'
        env:
          GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
          NO_COLOR: true
          NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
          UV_THREADPOOL_SIZE: '32'
          NODE_ENV: 'test'
        run: |
          if ("${{ matrix.shard }}" -eq "cli") {
            npm run test:ci --workspace @google/gemini-cli -- --coverage.enabled=false
          } else {
            # Explicitly list non-cli packages to ensure they are sharded correctly
            npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
            npm run test:scripts
          }
        shell: 'pwsh'

      - name: 'Bundle'
        run: 'npm run bundle'
        shell: 'pwsh'

      - name: 'Smoke test bundle'
        run: 'node ./bundle/gemini.js --version'
        shell: 'pwsh'

      - name: 'Smoke test npx installation'
        run: |
          # 1. Package the project into a tarball
          $PACK_OUTPUT = npm pack
          $TARBALL = $PACK_OUTPUT[-1]

          # 2. Move to a fresh directory for isolation
          New-Item -ItemType Directory -Force -Path ../smoke-test-dir
          Move-Item $TARBALL ../smoke-test-dir/
          Set-Location ../smoke-test-dir

          # 3. Run npx from the tarball
          npx "./$TARBALL" --version
        shell: 'pwsh'

  ci:
    name: 'CI'
    if: "github.repository == 'google-gemini/gemini-cli' && always()"
    needs:
      - 'lint'
      - 'link_checker'
      - 'test_linux'
      - 'test_mac'
      - 'test_windows'
      - 'codeql'
      - 'bundle_size'
    runs-on: 'gemini-cli-ubuntu-16-core'
    steps:
      - name: 'Check all job results'
        run: |
          if [[ (${NEEDS_LINT_RESULT} != 'success' && ${NEEDS_LINT_RESULT} != 'skipped') || \
               (${NEEDS_LINK_CHECKER_RESULT} != 'success' && ${NEEDS_LINK_CHECKER_RESULT} != 'skipped') || \
               (${NEEDS_TEST_LINUX_RESULT} != 'success' && ${NEEDS_TEST_LINUX_RESULT} != 'skipped') || \
               (${NEEDS_TEST_MAC_RESULT} != 'success' && ${NEEDS_TEST_MAC_RESULT} != 'skipped') || \
               (${NEEDS_TEST_WINDOWS_RESULT} != 'success' && ${NEEDS_TEST_WINDOWS_RESULT} != 'skipped') || \
               (${NEEDS_CODEQL_RESULT} != 'success' && ${NEEDS_CODEQL_RESULT} != 'skipped') || \
               (${NEEDS_BUNDLE_SIZE_RESULT} != 'success' && ${NEEDS_BUNDLE_SIZE_RESULT} != 'skipped') ]]; then
            echo "One or more CI jobs failed."
            exit 1
          fi
          echo "All CI jobs passed!"
        env:
          NEEDS_LINT_RESULT: '${{ needs.lint.result }}'
          NEEDS_LINK_CHECKER_RESULT: '${{ needs.link_checker.result }}'
          NEEDS_TEST_LINUX_RESULT: '${{ needs.test_linux.result }}'
          NEEDS_TEST_MAC_RESULT: '${{ needs.test_mac.result }}'
          NEEDS_TEST_WINDOWS_RESULT: '${{ needs.test_windows.result }}'
          NEEDS_CODEQL_RESULT: '${{ needs.codeql.result }}'
          NEEDS_BUNDLE_SIZE_RESULT: '${{ needs.bundle_size.result }}'
community-report .github/workflows/community-report.yml
Triggers
schedule, workflow_dispatch
Runs on
ubuntu-latest
Jobs
generate-report
Actions
actions/create-github-app-token, google-github-actions/run-gemini-cli
Commands
  • set -e START_DATE="$(date -u -d "$DAYS days ago" +'%Y-%m-%d')" END_DATE="$(date -u +'%Y-%m-%d')" echo "⏳ Generating report for contributions from ${START_DATE} to ${END_DATE}..." declare -A author_is_googler check_googler_status() { local author="$1" if [[ "${author}" == *"[bot]" ]]; then author_is_googler[${author}]=1 return 1 fi if [[ -v "author_is_googler[${author}]" ]]; then return "${author_is_googler[${author}]}" fi if gh api "orgs/googlers/members/${author}" --silent 2>/dev/null; then echo "🧑‍💻 ${author} is a Googler." author_is_googler[${author}]=0 else echo "🌍 ${author} is a community contributor." author_is_googler[${author}]=1 fi return "${author_is_googler[${author}]}" } googler_issues=0 non_googler_issues=0 googler_prs=0 non_googler_prs=0 echo "🔎 Fetching issues and pull requests..." ITEMS_JSON="$(gh search issues --repo "${REPO}" "created:>${START_DATE}" --json author,isPullRequest --limit 1000)" for row in $(echo "${ITEMS_JSON}" | jq -r '.[] | @base64'); do _jq() { echo "${row}" | base64 --decode | jq -r "${1}" } author="$(_jq '.author.login')" is_pr="$(_jq '.isPullRequest')" if [[ -z "${author}" || "${author}" == "null" ]]; then continue fi if check_googler_status "${author}"; then if [[ "${is_pr}" == "true" ]]; then ((googler_prs++)) else ((googler_issues++)) fi else if [[ "${is_pr}" == "true" ]]; then ((non_googler_prs++)) else ((non_googler_issues++)) fi fi done googler_discussions=0 non_googler_discussions=0 echo "🗣️ Fetching discussions..." DISCUSSION_QUERY=''' query($q: String!) { search(query: $q, type: DISCUSSION, first: 100) { nodes { ... on Discussion { author { login } } } } }''' DISCUSSIONS_JSON="$(gh api graphql -f q="repo:${REPO} created:>${START_DATE}" -f query="${DISCUSSION_QUERY}")" for row in $(echo "${DISCUSSIONS_JSON}" | jq -r '.data.search.nodes[] | @base64'); do _jq() { echo "${row}" | base64 --decode | jq -r "${1}" } author="$(_jq '.author.login')" if [[ -z "${author}" || "${author}" == "null" ]]; then continue fi if check_googler_status "${author}"; then ((googler_discussions++)) else ((non_googler_discussions++)) fi done echo "✍️ Generating report content..." TOTAL_ISSUES=$((googler_issues + non_googler_issues)) TOTAL_PRS=$((googler_prs + non_googler_prs)) TOTAL_DISCUSSIONS=$((googler_discussions + non_googler_discussions)) REPORT_BODY=$(cat <<EOF ### 💖 Community Contribution Report **Period:** ${START_DATE} to ${END_DATE} | Category | Googlers | Community | Total | |---|---:|---:|---:| | **Issues** | $googler_issues | $non_googler_issues | **$TOTAL_ISSUES** | | **Pull Requests** | $googler_prs | $non_googler_prs | **$TOTAL_PRS** | | **Discussions** | $googler_discussions | $non_googler_discussions | **$TOTAL_DISCUSSIONS** | _This report was generated automatically by a GitHub Action._ EOF ) echo "report_body<<EOF" >> "${GITHUB_OUTPUT}" echo "${REPORT_BODY}" >> "${GITHUB_OUTPUT}" echo "EOF" >> "${GITHUB_OUTPUT}" echo "📊 Community Contribution Report:" echo "${REPORT_BODY}"
View raw YAML
name: 'Generate Weekly Community Report 📊'

on:
  schedule:
    - cron: '0 12 * * 1' # Run at 12:00 UTC on Monday
  workflow_dispatch:
    inputs:
      days:
        description: 'Number of days to look back for the report'
        required: true
        default: '7'

jobs:
  generate-report:
    name: 'Generate Report 📝'
    if: |-
      ${{ github.repository == 'google-gemini/gemini-cli' }}
    runs-on: 'ubuntu-latest'
    permissions:
      issues: 'write'
      pull-requests: 'read'
      discussions: 'read'
      contents: 'read'
      id-token: 'write'

    steps:
      - name: 'Generate GitHub App Token 🔑'
        id: 'generate_token'
        uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'
          permission-issues: 'write'
          permission-pull-requests: 'read'
          permission-discussions: 'read'
          permission-contents: 'read'

      - name: 'Generate Report 📜'
        id: 'report'
        env:
          GH_TOKEN: '${{ steps.generate_token.outputs.token }}'
          REPO: '${{ github.repository }}'
          DAYS: '${{ github.event.inputs.days || 7 }}'
        run: |-
          set -e

          START_DATE="$(date -u -d "$DAYS days ago" +'%Y-%m-%d')"
          END_DATE="$(date -u +'%Y-%m-%d')"
          echo "⏳ Generating report for contributions from ${START_DATE} to ${END_DATE}..."

          declare -A author_is_googler
          check_googler_status() {
              local author="$1"
              if [[ "${author}" == *"[bot]" ]]; then
                  author_is_googler[${author}]=1
                  return 1
              fi
              if [[ -v "author_is_googler[${author}]" ]]; then
                  return "${author_is_googler[${author}]}"
              fi

              if gh api "orgs/googlers/members/${author}" --silent 2>/dev/null; then
                  echo "🧑‍💻 ${author} is a Googler."
                  author_is_googler[${author}]=0
              else
                  echo "🌍 ${author} is a community contributor."
                  author_is_googler[${author}]=1
              fi
              return "${author_is_googler[${author}]}"
          }

          googler_issues=0
          non_googler_issues=0
          googler_prs=0
          non_googler_prs=0

          echo "🔎 Fetching issues and pull requests..."
          ITEMS_JSON="$(gh search issues --repo "${REPO}" "created:>${START_DATE}" --json author,isPullRequest --limit 1000)"

          for row in $(echo "${ITEMS_JSON}" | jq -r '.[] | @base64'); do
              _jq() {
                  echo "${row}" | base64 --decode | jq -r "${1}"
              }
              author="$(_jq '.author.login')"
              is_pr="$(_jq '.isPullRequest')"

              if [[ -z "${author}" || "${author}" == "null" ]]; then
                continue
              fi

              if check_googler_status "${author}"; then
                  if [[ "${is_pr}" == "true" ]]; then
                      ((googler_prs++))
                  else
                      ((googler_issues++))
                  fi
              else
                  if [[ "${is_pr}" == "true" ]]; then
                      ((non_googler_prs++))
                  else
                      ((non_googler_issues++))
                  fi
              fi
          done

          googler_discussions=0
          non_googler_discussions=0

          echo "🗣️ Fetching discussions..."
          DISCUSSION_QUERY='''
          query($q: String!) {
            search(query: $q, type: DISCUSSION, first: 100) {
              nodes {
                ... on Discussion {
                  author {
                    login
                  }
                }
              }
            }
          }'''
          DISCUSSIONS_JSON="$(gh api graphql -f q="repo:${REPO} created:>${START_DATE}" -f query="${DISCUSSION_QUERY}")"

          for row in $(echo "${DISCUSSIONS_JSON}" | jq -r '.data.search.nodes[] | @base64'); do
              _jq() {
                  echo "${row}" | base64 --decode | jq -r "${1}"
              }
              author="$(_jq '.author.login')"

              if [[ -z "${author}" || "${author}" == "null" ]]; then
                continue
              fi

              if check_googler_status "${author}"; then
                  ((googler_discussions++))
              else
                  ((non_googler_discussions++))
              fi
          done

          echo "✍️ Generating report content..."
          TOTAL_ISSUES=$((googler_issues + non_googler_issues))
          TOTAL_PRS=$((googler_prs + non_googler_prs))
          TOTAL_DISCUSSIONS=$((googler_discussions + non_googler_discussions))

          REPORT_BODY=$(cat <<EOF
          ### 💖 Community Contribution Report

          **Period:** ${START_DATE} to ${END_DATE}

          | Category | Googlers | Community | Total |
          |---|---:|---:|---:|
          | **Issues** | $googler_issues | $non_googler_issues | **$TOTAL_ISSUES** |
          | **Pull Requests** | $googler_prs | $non_googler_prs | **$TOTAL_PRS** |
          | **Discussions** | $googler_discussions | $non_googler_discussions | **$TOTAL_DISCUSSIONS** |

          _This report was generated automatically by a GitHub Action._
          EOF
          )

          echo "report_body<<EOF" >> "${GITHUB_OUTPUT}"
          echo "${REPORT_BODY}" >> "${GITHUB_OUTPUT}"
          echo "EOF" >> "${GITHUB_OUTPUT}"

          echo "📊 Community Contribution Report:"
          echo "${REPORT_BODY}"

      - name: '🤖 Get Insights from Report'
        if: |-
          ${{ steps.report.outputs.report_body != '' }}
        uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
        env:
          GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
          REPOSITORY: '${{ github.repository }}'
        with:
          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 }}'
          use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
          settings: |-
            {
              "coreTools": [
                "run_shell_command(gh issue list)",
                "run_shell_command(gh pr list)",
                "run_shell_command(gh search issues)",
                "run_shell_command(gh search prs)"
              ]
            }
          prompt: |-
            You are a helpful assistant that analyzes community contribution reports.
            Based on the following report, please provide a brief summary and highlight any interesting trends or potential areas for improvement.

            Report:
            ${{ steps.report.outputs.report_body }}
deflake matrix .github/workflows/deflake.yml
Triggers
workflow_dispatch
Runs on
gemini-cli-ubuntu-16-core, macos-latest, gemini-cli-windows-16-core
Jobs
deflake_e2e_linux, deflake_e2e_mac, deflake_e2e_windows
Matrix
node-version, sandbox→ 20.x, sandbox:docker, sandbox:none
Actions
docker/setup-buildx-action
Commands
  • npm ci
  • npm run build
  • if [[ "${IS_DOCKER}" == "true" ]]; then npm run deflake:test:integration:sandbox:docker -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'" else npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'" fi
  • npm ci
  • npm run build
  • npm cache clean --force
  • npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
  • Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force Add-MpPreference -ExclusionPath "$env:TEMP" -Force
View raw YAML
name: 'Deflake E2E'

on:
  workflow_dispatch:
    inputs:
      branch_ref:
        description: 'Branch to run on'
        required: true
        default: 'main'
        type: 'string'
      test_name_pattern:
        description: 'The test name pattern to use'
        required: false
        type: 'string'
      runs:
        description: 'The number of runs'
        required: false
        default: 5
        type: 'number'

concurrency:
  group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
  cancel-in-progress: |-
    ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}

jobs:
  deflake_e2e_linux:
    name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
    runs-on: 'gemini-cli-ubuntu-16-core'
    if: "github.repository == 'google-gemini/gemini-cli'"
    strategy:
      fail-fast: false
      matrix:
        sandbox:
          - 'sandbox:none'
          - 'sandbox:docker'
        node-version:
          - '20.x'

    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
        with:
          ref: '${{ github.event.pull_request.head.sha }}'
          repository: '${{ github.repository }}'

      - name: 'Set up Node.js ${{ matrix.node-version }}'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
        with:
          node-version: '${{ matrix.node-version }}'

      - name: 'Install dependencies'
        run: 'npm ci'

      - name: 'Build project'
        run: 'npm run build'

      - name: 'Set up Docker'
        if: "matrix.sandbox == 'sandbox:docker'"
        uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3

      - name: 'Run E2E tests'
        env:
          GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
          IS_DOCKER: "${{ matrix.sandbox == 'sandbox:docker' }}"
          KEEP_OUTPUT: 'true'
          RUNS: '${{ github.event.inputs.runs }}'
          TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
          VERBOSE: 'true'
        shell: 'bash'
        run: |
          if [[ "${IS_DOCKER}" == "true" ]]; then
            npm run deflake:test:integration:sandbox:docker -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
          else
            npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
          fi

  deflake_e2e_mac:
    name: 'E2E Test (macOS)'
    runs-on: 'macos-latest'
    if: "github.repository == 'google-gemini/gemini-cli'"
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
        with:
          ref: '${{ github.event.pull_request.head.sha }}'
          repository: '${{ github.repository }}'

      - name: 'Set up Node.js 20.x'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
        with:
          node-version: '20.x'

      - name: 'Install dependencies'
        run: 'npm ci'

      - name: 'Build project'
        run: 'npm run build'

      - name: 'Fix rollup optional dependencies on macOS'
        if: "runner.os == 'macOS'"
        run: |
          npm cache clean --force
      - name: 'Run E2E tests (non-Windows)'
        if: "runner.os != 'Windows'"
        env:
          GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
          KEEP_OUTPUT: 'true'
          RUNS: '${{ github.event.inputs.runs }}'
          SANDBOX: 'sandbox:none'
          TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
          VERBOSE: 'true'
        run: |
          npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"

  deflake_e2e_windows:
    name: 'Slow E2E - Win'
    runs-on: 'gemini-cli-windows-16-core'
    if: "github.repository == 'google-gemini/gemini-cli'"
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
        with:
          ref: '${{ github.event.pull_request.head.sha }}'
          repository: '${{ github.repository }}'

      - name: 'Set up Node.js 20.x'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
        with:
          node-version: '20.x'
          cache: 'npm'

      - name: 'Configure Windows Defender exclusions'
        run: |
          Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
          Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
          Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
          Add-MpPreference -ExclusionPath "$env:TEMP" -Force
        shell: 'pwsh'

      - name: 'Configure npm for Windows performance'
        run: |
          npm config set progress false
          npm config set audit false
          npm config set fund false
          npm config set loglevel error
          npm config set maxsockets 32
          npm config set registry https://registry.npmjs.org/
        shell: 'pwsh'

      - name: 'Install dependencies'
        run: 'npm ci'
        shell: 'pwsh'

      - name: 'Build project'
        run: 'npm run build'
        shell: 'pwsh'

      - name: 'Run E2E tests'
        env:
          GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
          KEEP_OUTPUT: 'true'
          SANDBOX: 'sandbox:none'
          VERBOSE: 'true'
          NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
          UV_THREADPOOL_SIZE: '32'
          NODE_ENV: 'test'
          RUNS: '${{ github.event.inputs.runs }}'
          TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
        shell: 'pwsh'
        run: |
          npm run deflake:test:integration:sandbox:none -- --runs="$env:RUNS" -- --testNamePattern "'$env:TEST_NAME_PATTERN'"
docs-page-action perms .github/workflows/docs-page-action.yml
Triggers
push, workflow_dispatch
Runs on
ubuntu-latest, ubuntu-latest
Jobs
build, deploy
Actions
actions/configure-pages, actions/jekyll-build-pages, actions/upload-pages-artifact, actions/deploy-pages
View raw YAML
name: 'Deploy GitHub Pages'

on:
  push:
    tags: 'v*'
  workflow_dispatch:

permissions:
  contents: 'read'
  pages: 'write'
  id-token: 'write'

# Allow only one concurrent deployment, skipping runs queued between the run
# in-progress and latest queued. However, do NOT cancel in-progress runs as we
# want to allow these production deployments to complete.
concurrency:
  group: '${{ github.workflow }}'
  cancel-in-progress: false

jobs:
  build:
    if: "github.repository == 'google-gemini/gemini-cli' && !contains(github.ref_name, 'nightly')"
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Setup Pages'
        uses: 'actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b' # ratchet:actions/configure-pages@v5

      - name: 'Build with Jekyll'
        uses: 'actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697' # ratchet:actions/jekyll-build-pages@v1
        with:
          source: './'
          destination: './_site'

      - name: 'Upload artifact'
        uses: 'actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa' # ratchet:actions/upload-pages-artifact@v3

  deploy:
    if: "github.repository == 'google-gemini/gemini-cli'"
    environment:
      name: 'github-pages'
      url: '${{ steps.deployment.outputs.page_url }}'
    runs-on: 'ubuntu-latest'
    needs: 'build'
    steps:
      - name: 'Deploy to GitHub Pages'
        id: 'deployment'
        uses: 'actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e' # ratchet:actions/deploy-pages@v4
docs-rebuild .github/workflows/docs-rebuild.yml
Triggers
push
Runs on
ubuntu-latest
Jobs
trigger-rebuild
Commands
  • curl -X POST \ -H "Content-Type: application/json" \ -d '{}' \ "${{ secrets.DOCS_REBUILD_URL }}"
View raw YAML
name: 'Trigger Docs Rebuild'
on:
  push:
    branches:
      - 'main'
    paths:
      - 'docs/**'
jobs:
  trigger-rebuild:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Trigger rebuild'
        run: |
          curl -X POST \
            -H "Content-Type: application/json" \
            -d '{}' \
            "${{ secrets.DOCS_REBUILD_URL }}"
eval perms .github/workflows/eval.yml
Triggers
workflow_dispatch
Runs on
ubuntu-latest
Jobs
eval
Actions
google-github-actions/auth
Commands
  • poetry run exp_run --experiment-mode=on-demand --branch-or-commit="${GITHUB_REF_NAME}" --model-name=gemini-2.5-pro --dataset=swebench_verified --concurrency=15 poetry run python agent_prototypes/scripts/parse_gcli_logs_experiment.py --experiment_dir=experiments/adhoc/gcli_temp_exp --gcs-bucket="${EVAL_GCS_BUCKET}" --gcs-path=gh_action_artifacts
View raw YAML
name: 'Eval'

on:
  workflow_dispatch:

defaults:
  run:
    shell: 'bash'

permissions:
  contents: 'read'
  id-token: 'write'
  packages: 'read'

jobs:
  eval:
    name: 'Eval'
    if: >-
      github.repository == 'google-gemini/gemini-cli'
    runs-on: 'ubuntu-latest'
    container:
      image: 'ghcr.io/google-gemini/gemini-cli-swe-agent-eval@sha256:cd5edc4afd2245c1f575e791c0859b3c084a86bb3bd9a6762296da5162b35a8f'
      credentials:
        username: '${{ github.actor }}'
        password: '${{ secrets.GITHUB_TOKEN }}'
      env:
        GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
        DEFAULT_VERTEXAI_PROJECT: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
        GOOGLE_CLOUD_PROJECT: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
        GEMINI_API_KEY: '${{ secrets.EVAL_GEMINI_API_KEY }}'
        GCLI_LOCAL_FILE_TELEMETRY: 'True'
        EVAL_GCS_BUCKET: '${{ vars.EVAL_GCS_ARTIFACTS_BUCKET }}'
    steps:
      - name: 'Authenticate to Google Cloud'
        id: 'auth'
        uses: 'google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed' # ratchet:exclude pin@v2.1.7
        with:
          project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}'
          workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}'
          service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
          token_format: 'access_token'
          access_token_scopes: 'https://www.googleapis.com/auth/cloud-platform'

      - name: 'Run evaluation'
        working-directory: '/app'
        run: |
          poetry run exp_run --experiment-mode=on-demand --branch-or-commit="${GITHUB_REF_NAME}" --model-name=gemini-2.5-pro --dataset=swebench_verified --concurrency=15
          poetry run python agent_prototypes/scripts/parse_gcli_logs_experiment.py --experiment_dir=experiments/adhoc/gcli_temp_exp --gcs-bucket="${EVAL_GCS_BUCKET}" --gcs-path=gh_action_artifacts
eval-guidance perms .github/workflows/eval-guidance.yml
Triggers
pull_request
Runs on
ubuntu-latest
Jobs
provide-guidance
Actions
thollander/actions-comment-pull-request
Commands
  • STEERING_DETECTED=$(node scripts/changed_prompt.js --steering-only) echo "STEERING_DETECTED=$STEERING_DETECTED" >> "$GITHUB_OUTPUT"
  • # Check for behavioral eval changes EVAL_CHANGES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep "^evals/" || true) if [ -z "$EVAL_CHANGES" ]; then echo "MISSING_EVALS=true" >> "$GITHUB_OUTPUT" fi # Check if user is a maintainer (has write/admin access) USER_PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission --jq '.permission') if [[ "$USER_PERMISSION" == "admin" || "$USER_PERMISSION" == "write" ]]; then echo "IS_MAINTAINER=true" >> "$GITHUB_OUTPUT" fi
View raw YAML
name: 'Evals: PR Guidance'

on:
  pull_request:
    paths:
      - 'packages/core/src/**/*.ts'
      - '!**/*.test.ts'
      - '!**/*.test.tsx'

permissions:
  pull-requests: 'write'
  contents: 'read'

jobs:
  provide-guidance:
    name: 'Model Steering Guidance'
    runs-on: 'ubuntu-latest'
    if: "github.repository == 'google-gemini/gemini-cli'"
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
        with:
          fetch-depth: 0

      - name: 'Set up Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'Detect Steering Changes'
        id: 'detect'
        run: |
          STEERING_DETECTED=$(node scripts/changed_prompt.js --steering-only)
          echo "STEERING_DETECTED=$STEERING_DETECTED" >> "$GITHUB_OUTPUT"

      - name: 'Analyze PR Content'
        if: "steps.detect.outputs.STEERING_DETECTED == 'true'"
        id: 'analysis'
        env:
          GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
        run: |
          # Check for behavioral eval changes
          EVAL_CHANGES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep "^evals/" || true)
          if [ -z "$EVAL_CHANGES" ]; then
            echo "MISSING_EVALS=true" >> "$GITHUB_OUTPUT"
          fi

          # Check if user is a maintainer (has write/admin access)
          USER_PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission --jq '.permission')
          if [[ "$USER_PERMISSION" == "admin" || "$USER_PERMISSION" == "write" ]]; then
            echo "IS_MAINTAINER=true" >> "$GITHUB_OUTPUT"
          fi

      - name: 'Post Guidance Comment'
        if: "steps.detect.outputs.STEERING_DETECTED == 'true'"
        uses: 'thollander/actions-comment-pull-request@65f9e5c9a1f2cd378bd74b2e057c9736982a8e74' # ratchet:thollander/actions-comment-pull-request@v3
        with:
          comment-tag: 'eval-guidance-bot'
          message: |
            ### 🧠 Model Steering Guidance

            This PR modifies files that affect the model's behavior (prompts, tools, or instructions).

            ${{ steps.analysis.outputs.MISSING_EVALS == 'true' && '- ⚠️ **Consider adding Evals:** No behavioral evaluations (`evals/*.eval.ts`) were added or updated in this PR. Consider adding a test case to verify the new behavior and prevent regressions.' || '' }}
            ${{ steps.analysis.outputs.IS_MAINTAINER == 'true' && '- 🚀 **Maintainer Reminder:** Please ensure that these changes do not regress results on benchmark evals before merging.' || '' }}

            ---
            *This is an automated guidance message triggered by steering logic signatures.*
evals-nightly matrix perms .github/workflows/evals-nightly.yml
Triggers
schedule, workflow_dispatch
Runs on
gemini-cli-ubuntu-16-core, gemini-cli-ubuntu-16-core
Jobs
evals, aggregate-results
Matrix
model, run_attempt→ 1, 2, 3, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.5-pro, gemini-3-flash-preview, gemini-3-pro-preview, gemini-3.1-pro-preview-customtools
Commands
  • npm ci
  • npm run build
  • mkdir -p evals/logs
  • CMD="npm run test:all_evals" PATTERN="${TEST_NAME_PATTERN}" if [[ -n "$PATTERN" ]]; then if [[ "$PATTERN" == *.ts || "$PATTERN" == *.js || "$PATTERN" == */* ]]; then $CMD -- "$PATTERN" else $CMD -- -t "$PATTERN" fi else $CMD fi
  • node scripts/aggregate_evals.js artifacts >> "$GITHUB_STEP_SUMMARY"
View raw YAML
name: 'Evals: Nightly'

on:
  schedule:
    - cron: '0 1 * * *' # Runs at 1 AM every day
  workflow_dispatch:
    inputs:
      run_all:
        description: 'Run all evaluations (including usually passing)'
        type: 'boolean'
        default: true
      test_name_pattern:
        description: 'Test name pattern or file name'
        required: false
        type: 'string'

permissions:
  contents: 'read'
  checks: 'write'
  actions: 'read'

jobs:
  evals:
    name: 'Evals (USUALLY_PASSING) nightly run'
    runs-on: 'gemini-cli-ubuntu-16-core'
    if: "github.repository == 'google-gemini/gemini-cli'"
    strategy:
      fail-fast: false
      matrix:
        model:
          - 'gemini-3.1-pro-preview-customtools'
          - 'gemini-3-pro-preview'
          - 'gemini-3-flash-preview'
          - 'gemini-2.5-pro'
          - 'gemini-2.5-flash'
          - 'gemini-2.5-flash-lite'
        run_attempt: [1, 2, 3]
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Set up Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'Install dependencies'
        run: 'npm ci'

      - name: 'Build project'
        run: 'npm run build'

      - name: 'Create logs directory'
        run: 'mkdir -p evals/logs'

      - name: 'Run Evals'
        continue-on-error: true
        env:
          GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
          GEMINI_MODEL: '${{ matrix.model }}'
          RUN_EVALS: "${{ github.event.inputs.run_all != 'false' }}"
          TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
          # Disable Vitest internal retries to avoid double-retrying;
          # custom retry logic is handled in evals/test-helper.ts
          VITEST_RETRY: 0
        run: |
          CMD="npm run test:all_evals"
          PATTERN="${TEST_NAME_PATTERN}"

          if [[ -n "$PATTERN" ]]; then
            if [[ "$PATTERN" == *.ts || "$PATTERN" == *.js || "$PATTERN" == */* ]]; then
              $CMD -- "$PATTERN"
            else
              $CMD -- -t "$PATTERN"
            fi
          else
            $CMD
          fi

      - name: 'Upload Logs'
        if: 'always()'
        uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
        with:
          name: 'eval-logs-${{ matrix.model }}-${{ matrix.run_attempt }}'
          path: 'evals/logs'
          retention-days: 7

  aggregate-results:
    name: 'Aggregate Results'
    needs: ['evals']
    if: "github.repository == 'google-gemini/gemini-cli' && always()"
    runs-on: 'gemini-cli-ubuntu-16-core'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Download Logs'
        uses: 'actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806' # ratchet:actions/download-artifact@v4
        with:
          path: 'artifacts'

      - name: 'Generate Summary'
        env:
          GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
        run: 'node scripts/aggregate_evals.js artifacts >> "$GITHUB_STEP_SUMMARY"'
gemini-automated-issue-dedup .github/workflows/gemini-automated-issue-dedup.yml
Triggers
issues, issue_comment, workflow_dispatch
Runs on
ubuntu-latest, ubuntu-latest
Jobs
find-duplicates, add-comment-and-label
Actions
docker/login-action, google-github-actions/run-gemini-cli, actions/create-github-app-token
View raw YAML
name: '🏷️ Gemini Automated Issue Deduplication'

on:
  issues:
    types:
      - 'opened'
      - 'reopened'
  issue_comment:
    types:
      - 'created'
  workflow_dispatch:
    inputs:
      issue_number:
        description: 'issue number to dedup'
        required: true
        type: 'number'

concurrency:
  group: '${{ github.workflow }}-${{ github.event.issue.number }}'
  cancel-in-progress: true

defaults:
  run:
    shell: 'bash'

jobs:
  find-duplicates:
    if: |-
      github.repository == 'google-gemini/gemini-cli' &&
      vars.TRIAGE_DEDUPLICATE_ISSUES != '' &&
      (github.event_name == 'issues' ||
       github.event_name == 'workflow_dispatch' ||
       (github.event_name == 'issue_comment' &&
       contains(github.event.comment.body, '@gemini-cli /deduplicate') &&
       (github.event.comment.author_association == 'OWNER' ||
        github.event.comment.author_association == 'MEMBER' ||
        github.event.comment.author_association == 'COLLABORATOR')))
    permissions:
      contents: 'read'
      id-token: 'write' # Required for WIF, see https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-google-cloud-platform#adding-permissions-settings
      issues: 'read'
      statuses: 'read'
      packages: 'read'
    timeout-minutes: 20
    runs-on: 'ubuntu-latest'
    outputs:
      duplicate_issues_csv: '${{ env.DUPLICATE_ISSUES_CSV }}'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Log in to GitHub Container Registry'
        uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
        with:
          registry: 'ghcr.io'
          username: '${{ github.actor }}'
          password: '${{ secrets.GITHUB_TOKEN }}'

      - name: 'Find Duplicate Issues'
        uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
        id: 'gemini_issue_deduplication'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          ISSUE_TITLE: '${{ github.event.issue.title }}'
          ISSUE_BODY: '${{ github.event.issue.body }}'
          ISSUE_NUMBER: '${{ github.event.issue.number }}'
          REPOSITORY: '${{ github.repository }}'
          FIRESTORE_PROJECT: '${{ vars.FIRESTORE_PROJECT }}'
        with:
          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 }}'
          use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
          settings: |-
            {
              "mcpServers": {
                "issue_deduplication": {
                  "command": "docker",
                  "args": [
                    "run",
                    "-i",
                    "--rm",
                    "--network", "host",
                    "-e", "GITHUB_TOKEN",
                    "-e", "GEMINI_API_KEY",
                    "-e", "DATABASE_TYPE",
                    "-e", "FIRESTORE_DATABASE_ID",
                    "-e", "GCP_PROJECT",
                    "-e", "GOOGLE_APPLICATION_CREDENTIALS=/app/gcp-credentials.json",
                    "-v", "${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json",
                    "ghcr.io/google-gemini/gemini-cli-issue-triage@sha256:e3de1523f6c83aabb3c54b76d08940a2bf42febcb789dd2da6f95169641f94d3"
                  ],
                  "env": {
                    "GITHUB_TOKEN": "${GITHUB_TOKEN}",
                    "GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}",
                    "DATABASE_TYPE":"firestore",
                    "GCP_PROJECT": "${FIRESTORE_PROJECT}",
                    "FIRESTORE_DATABASE_ID": "(default)",
                    "GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}"
                  },
                  "timeout": 600000
                }
              },
              "maxSessionTurns": 25,
              "coreTools": [
                "run_shell_command(echo)",
                "run_shell_command(gh issue view)"
              ],
              "telemetry": {
                "enabled": true,
                "target": "gcp"
              }
            }
          prompt: |-
            ## Role
            You are an issue de-duplication assistant. Your goal is to find
            duplicate issues for a given issue.
            ## Steps
            1.  **Find Potential Duplicates:**
                - The repository is ${{ github.repository }} and the issue number is ${{ github.event.issue.number }}.
                - Use the `duplicates` tool with the `repo` and `issue_number` to find potential duplicates for the current issue. Do not use the `threshold` parameter.
                - If no duplicates are found, you are done.
                - Print the JSON output from the `duplicates` tool to the logs.
            2.  **Refine Duplicates List (if necessary):**
                - If the `duplicates` tool returns between 1 and 14 results, you must refine the list.
                - For each potential duplicate issue, run `gh issue view <issue-number> --json title,body,comments` to fetch its content.
                - Also fetch the content of the original issue: `gh issue view "${ISSUE_NUMBER}" --json title,body,comments`.
                - Carefully analyze the content (title, body, comments) of the original issue and all potential duplicates.
                - It is very important if the comments on either issue mention that they are not duplicates of each other, to treat them as not duplicates.
                - Based on your analysis, create a final list containing only the issues you are highly confident are actual duplicates.
                - If your final list is empty, you are done.
                - Print to the logs if you omitted any potential duplicates based on your analysis.
                - If the `duplicates` tool returned 15+ results, use the top 15 matches (based on descending similarity score value) to perform this step.
            3.  **Output final duplicates list as CSV:**
                - Convert the list of appropriate duplicate issue numbers into a comma-separated list (CSV). If there are no appropriate duplicates, use the empty string.
                - Use the "echo" shell command to append the CSV of issue numbers into the filepath referenced by the environment variable "${GITHUB_ENV}":
                  echo "DUPLICATE_ISSUES_CSV=[DUPLICATE_ISSUES_AS_CSV]" >> "${GITHUB_ENV}"
            ## Guidelines
            - Only use the `duplicates` and `run_shell_command` tools.
            - The `run_shell_command` tool can be used with `gh issue view`.
            - Do not download or read media files like images, videos, or links. The `--json` flag for `gh issue view` will prevent this.
            - Do not modify the issue content or status.
            - Do not add comments or labels.
            - Reference all shell variables as "${VAR}" (with quotes and braces).

  add-comment-and-label:
    needs: 'find-duplicates'
    if: |-
      github.repository == 'google-gemini/gemini-cli' &&
      vars.TRIAGE_DEDUPLICATE_ISSUES != '' &&
      needs.find-duplicates.outputs.duplicate_issues_csv != '' &&
      (
       github.event_name == 'issues' ||
       github.event_name == 'workflow_dispatch' ||
       (
        github.event_name == 'issue_comment' &&
        contains(github.event.comment.body, '@gemini-cli /deduplicate') &&
        (
         github.event.comment.author_association == 'OWNER' ||
         github.event.comment.author_association == 'MEMBER' ||
         github.event.comment.author_association == 'COLLABORATOR'
        )
       )
      )
    permissions:
      issues: 'write'
    timeout-minutes: 5
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'
          permission-issues: 'write'

      - name: 'Comment and Label Duplicate Issue'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        env:
          DUPLICATES_OUTPUT: '${{ needs.find-duplicates.outputs.duplicate_issues_csv }}'
        with:
          github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
          script: |-
            const rawCsv = process.env.DUPLICATES_OUTPUT;
            core.info(`Raw duplicates CSV: ${rawCsv}`);
            const duplicateIssues = rawCsv.split(',').map(s => s.trim()).filter(s => s);

            if (duplicateIssues.length === 0) {
              core.info('No duplicate issues found. Nothing to do.');
              return;
            }

            const issueNumber = ${{ github.event.issue.number }};

            function formatCommentBody(issues, updated = false) {
              const header = updated
                ? 'Found possible duplicate issues (updated):'
                : 'Found possible duplicate issues:';
              const issuesList = issues.map(num => `- #${num}`).join('\n');
              const footer = 'If you believe this is not a duplicate, please remove the `status/possible-duplicate` label.';
              const magicComment = '<!-- gemini-cli-deduplication -->';
              return `${header}\n\n${issuesList}\n\n${footer}\n${magicComment}`;
            }

            const newCommentBody = formatCommentBody(duplicateIssues);
            const newUpdatedCommentBody = formatCommentBody(duplicateIssues, true);

            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
            });

            const magicComment = '<!-- gemini-cli-deduplication -->';
            const existingComment = comments.find(comment =>
              comment.user.type === 'Bot' && comment.body.includes(magicComment)
            );

            let commentMade = false;

            if (existingComment) {
              // To check if lists are same, just compare the formatted bodies without headers.
              const existingBodyForCompare = existingComment.body.substring(existingComment.body.indexOf('- #'));
              const newBodyForCompare = newCommentBody.substring(newCommentBody.indexOf('- #'));

              if (existingBodyForCompare.trim() !== newBodyForCompare.trim()) {
                core.info(`Updating existing comment ${existingComment.id}`);
                await github.rest.issues.updateComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  comment_id: existingComment.id,
                  body: newUpdatedCommentBody,
                });
                commentMade = true;
              } else {
                core.info('Existing comment is up-to-date. Nothing to do.');
              }
            } else {
              core.info('Creating new comment.');
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: issueNumber,
                body: newCommentBody,
              });
              commentMade = true;
            }

            if (commentMade) {
              core.info('Adding "status/possible-duplicate" label.');
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: issueNumber,
                labels: ['status/possible-duplicate'],
              });
            }
gemini-automated-issue-triage perms .github/workflows/gemini-automated-issue-triage.yml
Triggers
issues, issue_comment, workflow_dispatch, workflow_call
Runs on
ubuntu-latest
Jobs
triage-issue
Actions
actions/create-github-app-token, google-github-actions/run-gemini-cli
Commands
  • if echo "${LABELS}" | grep -q 'area/'; then echo "Issue #${ISSUE_NUMBER_INPUT} already has 'area/' label. Stopping workflow." exit 1 fi echo "Manual triage checks passed."
View raw YAML
name: '🏷️ Gemini Automated Issue Triage'

on:
  issues:
    types:
      - 'opened'
      - 'reopened'
  issue_comment:
    types:
      - 'created'
  workflow_dispatch:
    inputs:
      issue_number:
        description: 'issue number to triage'
        required: true
        type: 'number'
  workflow_call:
    inputs:
      issue_number:
        description: 'issue number to triage'
        required: false
        type: 'string'

concurrency:
  group: '${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number || inputs.issue_number }}'
  cancel-in-progress: true

defaults:
  run:
    shell: 'bash'

permissions:
  contents: 'read'
  id-token: 'write'
  issues: 'write'
  statuses: 'write'
  packages: 'read'
  actions: 'write' # Required for cancelling a workflow run

jobs:
  triage-issue:
    if: |-
      (github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli') &&
      (
        github.event_name == 'workflow_dispatch' ||
        (
          (github.event_name == 'issues' || github.event_name == 'issue_comment') &&
          (github.event_name != 'issue_comment' || (
            contains(github.event.comment.body, '@gemini-cli /triage') &&
            (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')
          ))
        )
      ) &&
      !contains(github.event.issue.labels.*.name, 'area/')
    timeout-minutes: 5
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Get issue data for manual trigger'
        id: 'get_issue_data'
        if: |-
          github.event_name == 'workflow_dispatch'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          script: |
            const issueNumber = ${{ github.event.inputs.issue_number || inputs.issue_number }};
            const { data: issue } = await github.rest.issues.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
            });
            core.setOutput('title', issue.title);
            core.setOutput('body', issue.body);
            core.setOutput('labels', issue.labels.map(label => label.name).join(','));
            return issue;

      - name: 'Manual Trigger Pre-flight Checks'
        if: |-
          github.event_name == 'workflow_dispatch'
        env:
          ISSUE_NUMBER_INPUT: '${{ github.event.inputs.issue_number || inputs.issue_number }}'
          LABELS: '${{ steps.get_issue_data.outputs.labels }}'
        run: |
          if echo "${LABELS}" | grep -q 'area/'; then
            echo "Issue #${ISSUE_NUMBER_INPUT} already has 'area/' label. Stopping workflow."
            exit 1
          fi

          echo "Manual triage checks passed."

      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        env:
          APP_ID: '${{ secrets.APP_ID }}'
        if: |-
          ${{ env.APP_ID != '' }}
        uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'
          permission-issues: 'write'

      - name: 'Get Repository Labels'
        id: 'get_labels'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
          script: |-
            const { data: labels } = await github.rest.issues.listLabelsForRepo({
              owner: context.repo.owner,
              repo: context.repo.repo,
            });
            const allowedLabels = [
              'area/agent',
              'area/enterprise',
              'area/non-interactive',
              'area/core',
              'area/security',
              'area/platform',
              'area/extensions',
              'area/documentation',
              'area/unknown'
            ];
            const labelNames = labels.map(label => label.name).filter(name => allowedLabels.includes(name));
            core.setOutput('available_labels', labelNames.join(','));
            core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
            return labelNames;

      - name: 'Run Gemini Issue Analysis'
        uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
        id: 'gemini_issue_analysis'
        env:
          GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
          ISSUE_TITLE: >-
            ${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.title || github.event.issue.title }}
          ISSUE_BODY: >-
            ${{ github.event_name == 'workflow_dispatch' && steps.get_issue_data.outputs.body || github.event.issue.body }}
          ISSUE_NUMBER: >-
            ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.issue_number || inputs.issue_number) || github.event.issue.number }}
          REPOSITORY: '${{ github.repository }}'
          AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
        with:
          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 }}'
          use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
          settings: |-
            {
              "maxSessionTurns": 25,
              "telemetry": {
                "enabled": true,
                "target": "gcp"
              },
              "coreTools": [
                "run_shell_command(echo)"
              ]
            }
          prompt: |-
            ## Role

            You are an issue triage assistant. Your role is to analyze a GitHub issue and determine the single most appropriate area/ label based on the definitions provided.

            ## Steps
            1. Review the issue title and body: ${{ env.ISSUE_TITLE }} and ${{ env.ISSUE_BODY }}.
            2. Review the available labels: ${{ env.AVAILABLE_LABELS }}.
            3. Select exactly one area/ label that best matches the issue based on Reference 1: Area Definitions.
            4. Fallback Logic:
                - If you cannot confidently determine the correct area/ label from the definitions, you must use area/unknown.
            5. Output your selected label in JSON format and nothing else. Example:
                {"labels_to_set": ["area/core"]}

            ## Guidelines
            - Your output must contain exactly one area/ label.
            - Triage only the current issue based on its title and body.
            - Output only valid JSON format.
            - Do not include any explanation or additional text, just the JSON.

            Reference 1: Area Definitions
            area/agent
            - Description: Issues related to the "brain" of the CLI. This includes the core agent logic, model quality, tool/function calling, and memory.
            - Example Issues:
              "I am not getting a reasonable or expected response."
              "The model is not calling the tool I expected."
              "The web search tool is not working as expected."
              "Feature request for a new built-in tool (e.g., read file, write file)."
              "The generated code is poor quality or incorrect."
              "The model seems stuck in a loop."
              "The response from the model is malformed (e.g., broken JSON, bad formatting)."
              "Concerns about unnecessary token consumption."
              "Issues with how memory or chat history is managed."
              "Issues with sub-agents."
              "Model is switching from one to another unexpectedly."

            area/enterprise
            - Description: Issues specific to enterprise-level features, including telemetry, policy, and licenses.
            - Example Issues:
              "Usage data is not appearing in our telemetry dashboard."
              "A user is able to perform an action that should be blocked by an admin policy."
              "Questions about billing, licensing tiers, or enterprise quotas."

            area/non-interactive
            - Description: Issues related to using the CLI in automated or non-interactive environments (headless mode).
            - Example Issues:
              "Problems using the CLI as an SDK in another surface."
              "The CLI is behaving differently when run from a shell script vs. an interactive terminal."
              "GitHub action is failing."
              "I am having trouble running the CLI in headless mode"

            area/core
            - Description: Issues with the fundamental CLI app itself. This includes the user interface (UI/UX), installation, OS compatibility, and performance.
            - Example Issues:
              "I am seeing my screen flicker when using the CLI."
              "The output in my terminal is malformed or unreadable."
              "Theme changes are not taking effect."
              "Keyboard inputs (e.g., arrow keys, Ctrl+C) are not being recognized."
              "The CLI failed to install or update."
              "An issue specific to running on Windows, macOS, or Linux."
              "Problems with command parsing, flags, or argument handling."
              "High CPU or memory usage by the CLI process."
              "Issues related to multi-modality (e.g., handling image inputs)."
              "Problems with the IDE integration connection or installation"

            area/security
            - Description: Issues related to user authentication, authorization, data security, and privacy.
            - Example Issues:
              "I am unable to sign in."
              "The login flow is selecting the wrong authentication path"
              "Problems with API key handling or credential storage."
              "A report of a security vulnerability"
              "Concerns about data sanitization or potential data leaks."
              "Issues or requests related to privacy controls."
              "Preventing unauthorized data access."

            area/platform
            - Description: Issues related to CI/CD, release management, testing, eval infrastructure, capacity, quota management, and sandbox environments.
            - Example Issues:
              "I am getting a 429 'Resource Exhausted' or 500-level server error."
              "General slowness or high latency from the service."
              "The build script is broken on the main branch."
              "Tests are failing in the CI/CD pipeline."
              "Issues with the release management or publishing process."
              "User is running out of capacity."
              "Problems specific to the sandbox or staging environments."
              "Questions about quota limits or requests for increases."

            area/extensions
            - Description: Issues related to the extension ecosystem, including the marketplace and website.
            - Example Issues:
              "Bugs related to the extension marketplace website."
              "Issues with a specific extension."
              "Feature request for the extension ecosystem."

            area/documentation
            - Description: Issues related to user-facing documentation and other content on the documentation website.
            - Example Issues:
              "A typo in a README file."
              "DOCS: A command is not working as described in the documentation."
              "A request for a new documentation page."
              "Instructions missing for skills feature"

            area/unknown
            - Description: Issues that do not clearly fit into any other defined area/ category, or where information is too limited to make a determination. Use this when no other area is appropriate.

      - name: 'Apply Labels to Issue'
        if: |-
          ${{ steps.gemini_issue_analysis.outputs.summary != '' }}
        env:
          REPOSITORY: '${{ github.repository }}'
          ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
          LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
          script: |
            const rawOutput = process.env.LABELS_OUTPUT;
            core.info(`Raw output from model: ${rawOutput}`);
            let parsedLabels;
            try {
              // First, try to parse the raw output as JSON.
              parsedLabels = JSON.parse(rawOutput);
            } catch (jsonError) {
              // If that fails, check for a markdown code block.
              core.warning(`Direct JSON parsing failed: ${jsonError.message}. Trying to extract from a markdown block.`);
              const jsonMatch = rawOutput.match(/```json\s*([\s\S]*?)\s*```/);
              if (jsonMatch && jsonMatch[1]) {
                try {
                  parsedLabels = JSON.parse(jsonMatch[1].trim());
                } catch (markdownError) {
                  core.setFailed(`Failed to parse JSON even after extracting from markdown block: ${markdownError.message}\nRaw output: ${rawOutput}`);
                  return;
                }
              } else {
                // If no markdown block, try to find a raw JSON object in the output.
                // The CLI may include debug/log lines (e.g. telemetry init, YOLO mode)
                // before the actual JSON response.
                const jsonObjectMatch = rawOutput.match(/(\{[\s\S]*"labels_to_set"[\s\S]*\})/);
                if (jsonObjectMatch) {
                  try {
                    parsedLabels = JSON.parse(jsonObjectMatch[0]);
                  } catch (extractError) {
                    core.setFailed(`Found JSON-like content but failed to parse: ${extractError.message}\nRaw output: ${rawOutput}`);
                    return;
                  }
                } else {
                  core.setFailed(`Output is not valid JSON and does not contain extractable JSON.\nRaw output: ${rawOutput}`);
                  return;
                }
              }
            }

            const issueNumber = parseInt(process.env.ISSUE_NUMBER);
            const labelsToAdd = parsedLabels.labels_to_set || [];

            if (labelsToAdd.length !== 1) {
              core.setFailed(`Expected exactly 1 label (area/), but got ${labelsToAdd.length}. Labels: ${labelsToAdd.join(', ')}`);
              return;
            }

            // Set labels based on triage result
            await github.rest.issues.addLabels({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
              labels: labelsToAdd
            });
            core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}`);

      - name: 'Post Issue Analysis Failure Comment'
        if: |-
          ${{ failure() && steps.gemini_issue_analysis.outcome == 'failure' }}
        env:
          ISSUE_NUMBER: '${{ github.event.issue.number || github.event.inputs.issue_number }}'
          RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
          script: |-
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: parseInt(process.env.ISSUE_NUMBER),
              body: 'There is a problem with the Gemini CLI issue triaging. Please check the [action logs](${process.env.RUN_URL}) for details.'
            })
gemini-scheduled-issue-dedup .github/workflows/gemini-scheduled-issue-dedup.yml
Triggers
schedule, workflow_dispatch
Runs on
ubuntu-latest
Jobs
refresh-embeddings
Actions
docker/login-action, google-github-actions/run-gemini-cli
View raw YAML
name: '📋 Gemini Scheduled Issue Deduplication'

on:
  schedule:
    - cron: '0 * * * *' # Runs every hour
  workflow_dispatch:

concurrency:
  group: '${{ github.workflow }}'
  cancel-in-progress: true

defaults:
  run:
    shell: 'bash'

jobs:
  refresh-embeddings:
    if: |-
      ${{ vars.TRIAGE_DEDUPLICATE_ISSUES != '' && github.repository == 'google-gemini/gemini-cli' }}
    permissions:
      contents: 'read'
      id-token: 'write' # Required for WIF, see https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-google-cloud-platform#adding-permissions-settings
      issues: 'read'
      statuses: 'read'
      packages: 'read'
    timeout-minutes: 20
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Log in to GitHub Container Registry'
        uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3
        with:
          registry: 'ghcr.io'
          username: '${{ github.actor }}'
          password: '${{ secrets.GITHUB_TOKEN }}'

      - name: 'Run Gemini Issue Deduplication Refresh'
        uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
        id: 'gemini_refresh_embeddings'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          ISSUE_TITLE: '${{ github.event.issue.title }}'
          ISSUE_BODY: '${{ github.event.issue.body }}'
          ISSUE_NUMBER: '${{ github.event.issue.number }}'
          REPOSITORY: '${{ github.repository }}'
          FIRESTORE_PROJECT: '${{ vars.FIRESTORE_PROJECT }}'
        with:
          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 }}'
          use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
          settings: |-
            {
              "mcpServers": {
                "issue_deduplication": {
                  "command": "docker",
                  "args": [
                    "run",
                    "-i",
                    "--rm",
                    "--network", "host",
                    "-e", "GITHUB_TOKEN",
                    "-e", "GEMINI_API_KEY",
                    "-e", "DATABASE_TYPE",
                    "-e", "FIRESTORE_DATABASE_ID",
                    "-e", "GCP_PROJECT",
                    "-e", "GOOGLE_APPLICATION_CREDENTIALS=/app/gcp-credentials.json",
                    "-v", "${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json",
                    "ghcr.io/google-gemini/gemini-cli-issue-triage@sha256:e3de1523f6c83aabb3c54b76d08940a2bf42febcb789dd2da6f95169641f94d3"
                  ],
                  "env": {
                    "GITHUB_TOKEN": "${GITHUB_TOKEN}",
                    "GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}",
                    "DATABASE_TYPE":"firestore",
                    "GCP_PROJECT": "${FIRESTORE_PROJECT}",
                    "FIRESTORE_DATABASE_ID": "(default)",
                    "GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}"
                  },
                  "timeout": 600000
                }
              },
              "maxSessionTurns": 25,
              "coreTools": [
                "run_shell_command(echo)"
              ],
              "telemetry": {
                "enabled": true,
                "target": "gcp"
              }
            }
          prompt: |-
            ## Role

            You are a database maintenance assistant for a GitHub issue deduplication system.

            ## Goal

            Your sole responsibility is to refresh the embeddings for all open issues in the repository to ensure the deduplication database is up-to-date.

            ## Steps

            1.  **Extract Repository Information:** The repository is ${{ github.repository }}.
            2.  **Refresh Embeddings:** Call the `refresh` tool with the correct `repo`. Do not use the `force` parameter.
            3.  **Log Output:** Print the JSON output from the `refresh` tool to the logs.

            ## Guidelines

            - Only use the `refresh` tool.
            - Do not attempt to find duplicates or modify any issues.
            - Your only task is to call the `refresh` tool and log its output.
gemini-scheduled-issue-triage perms .github/workflows/gemini-scheduled-issue-triage.yml
Triggers
issues, schedule, workflow_dispatch
Runs on
ubuntu-latest
Jobs
triage-issues
Actions
actions/create-github-app-token, google-github-actions/run-gemini-cli
Commands
  • set -euo pipefail ISSUE_JSON=$(echo "$ISSUE_EVENT" | jq -c '[{number: .number, title: .title, body: .body}]') echo "issues_to_triage=${ISSUE_JSON}" >> "${GITHUB_OUTPUT}" echo "✅ Found issue #${{ github.event.issue.number }} from event to triage! 🎯"
  • set -euo pipefail echo '🔍 Finding issues missing area labels...' NO_AREA_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \ --search 'is:open is:issue -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/documentation -label:area/unknown' --limit 100 --json number,title,body)" echo '🔍 Finding issues missing kind labels...' NO_KIND_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \ --search 'is:open is:issue -label:kind/bug -label:kind/enhancement -label:kind/customer-issue -label:kind/question' --limit 100 --json number,title,body)" echo '🏷️ Finding issues missing priority labels...' NO_PRIORITY_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \ --search 'is:open is:issue -label:priority/p0 -label:priority/p1 -label:priority/p2 -label:priority/p3 -label:priority/unknown' --limit 100 --json number,title,body)" echo '🔄 Merging and deduplicating issues...' ISSUES="$(echo "${NO_AREA_ISSUES}" "${NO_KIND_ISSUES}" "${NO_PRIORITY_ISSUES}" | jq -c -s 'add | unique_by(.number)')" echo '📝 Setting output for GitHub Actions...' echo "issues_to_triage=${ISSUES}" >> "${GITHUB_OUTPUT}" ISSUE_COUNT="$(echo "${ISSUES}" | jq 'length')" echo "✅ Found ${ISSUE_COUNT} unique issues to triage! 🎯"
View raw YAML
name: '📋 Gemini Scheduled Issue Triage'

on:
  issues:
    types:
      - 'opened'
      - 'reopened'
  schedule:
    - cron: '0 * * * *' # Runs every hour
  workflow_dispatch:

concurrency:
  group: '${{ github.workflow }}-${{ github.event.number || github.run_id }}'
  cancel-in-progress: true

defaults:
  run:
    shell: 'bash'

permissions:
  id-token: 'write'
  issues: 'write'

jobs:
  triage-issues:
    timeout-minutes: 10
    if: |-
      ${{ github.repository == 'google-gemini/gemini-cli' }}
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'
          permission-issues: 'write'

      - name: 'Get issue from event'
        if: |-
          ${{ github.event_name == 'issues' }}
        id: 'get_issue_from_event'
        env:
          ISSUE_EVENT: '${{ toJSON(github.event.issue) }}'
        run: |
          set -euo pipefail
          ISSUE_JSON=$(echo "$ISSUE_EVENT" | jq -c '[{number: .number, title: .title, body: .body}]')
          echo "issues_to_triage=${ISSUE_JSON}" >> "${GITHUB_OUTPUT}"
          echo "✅ Found issue #${{ github.event.issue.number }} from event to triage! 🎯"

      - name: 'Find untriaged issues'
        if: |-
          ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
        id: 'find_issues'
        env:
          GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
          GITHUB_REPOSITORY: '${{ github.repository }}'
        run: |-
          set -euo pipefail

          echo '🔍 Finding issues missing area labels...'
          NO_AREA_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
            --search 'is:open is:issue -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/documentation -label:area/unknown' --limit 100 --json number,title,body)"

          echo '🔍 Finding issues missing kind labels...'
          NO_KIND_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
            --search 'is:open is:issue -label:kind/bug -label:kind/enhancement -label:kind/customer-issue -label:kind/question' --limit 100 --json number,title,body)"

          echo '🏷️ Finding issues missing priority labels...'
          NO_PRIORITY_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
            --search 'is:open is:issue -label:priority/p0 -label:priority/p1 -label:priority/p2 -label:priority/p3 -label:priority/unknown' --limit 100 --json number,title,body)"

          echo '🔄 Merging and deduplicating issues...'
          ISSUES="$(echo "${NO_AREA_ISSUES}" "${NO_KIND_ISSUES}" "${NO_PRIORITY_ISSUES}" | jq -c -s 'add | unique_by(.number)')"

          echo '📝 Setting output for GitHub Actions...'
          echo "issues_to_triage=${ISSUES}" >> "${GITHUB_OUTPUT}"

          ISSUE_COUNT="$(echo "${ISSUES}" | jq 'length')"
          echo "✅ Found ${ISSUE_COUNT} unique issues to triage! 🎯"

      - name: 'Get Repository Labels'
        id: 'get_labels'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token }}'
          script: |-
            const { data: labels } = await github.rest.issues.listLabelsForRepo({
              owner: context.repo.owner,
              repo: context.repo.repo,
            });
            const labelNames = labels.map(label => label.name);
            core.setOutput('available_labels', labelNames.join(','));
            core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`);
            return labelNames;

      - name: 'Run Gemini Issue Analysis'
        if: |-
          (steps.get_issue_from_event.outputs.issues_to_triage != '' && steps.get_issue_from_event.outputs.issues_to_triage != '[]') ||
          (steps.find_issues.outputs.issues_to_triage != '' && steps.find_issues.outputs.issues_to_triage != '[]')
        uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
        id: 'gemini_issue_analysis'
        env:
          GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs
          ISSUES_TO_TRIAGE: '${{ steps.get_issue_from_event.outputs.issues_to_triage || steps.find_issues.outputs.issues_to_triage }}'
          REPOSITORY: '${{ github.repository }}'
          AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}'
        with:
          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 }}'
          use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}'
          settings: |-
            {
              "maxSessionTurns": 25,
              "coreTools": [
                "run_shell_command(echo)"
              ],
              "telemetry": {
                "enabled": true,
                "target": "gcp"
              }
            }
          prompt: |-
            ## Role

            You are an issue triage assistant. Analyze issues and identify
            appropriate labels. Use the available tools to gather information;
            do not ask for information to be provided.

            ## Steps

            1. You are only able to use the echo command. Review the available labels in the environment variable: "${AVAILABLE_LABELS}".
            2. Check environment variable for issues to triage: $ISSUES_TO_TRIAGE (JSON array of issues)
            3. Review the issue title, body and any comments provided in the environment variables.
            4. Identify the most relevant labels from the existing labels, specifically focusing on area/*, kind/* and priority/*.
            5. Label Policy:
               - If the issue already has a kind/ label, do not change it.
               - If the issue already has a priority/ label, do not change it.
               - If the issue already has an area/ label, do not change it.
               - If any of these are missing, select exactly ONE appropriate label for the missing category.
            6. Identify other applicable labels based on the issue content, such as status/*, help wanted, good first issue, etc.
            7. Give me a single short explanation about why you are selecting each label in the process.
            8. Output a JSON array of objects, each containing the issue number
               and the labels to add and remove, along with an explanation. For example:
               ```
               [
                 {
                   "issue_number": 123,
                   "labels_to_add": ["area/core", "kind/bug", "priority/p2"],
                   "labels_to_remove": ["status/need-triage"],
                   "explanation": "This issue is a UI bug that needs to be addressed with medium priority."
                 }
               ]
               ```
              If an issue cannot be classified, do not include it in the output array.
            9. For each issue please check if CLI version is present, this is usually in the output of the /about command and will look like 0.1.5
              - Anything more than 6 versions older than the most recent should add the status/need-retesting label
            10. If you see that the issue doesn't look like it has sufficient information recommend the status/need-information label and leave a comment politely requesting the relevant information, eg.. if repro steps are missing request for repro steps. if version information is missing request for version information into the explanation section below.
            11. If you think an issue might be a Priority/P0 do not apply the priority/p0 label. Instead apply a status/manual-triage label and include a note in your explanation.
            12. If you are uncertain about a category, use the area/unknown, kind/question, or priority/unknown labels as appropriate. If you are extremely uncertain, apply the status/manual-triage label.

            ## Guidelines

            - Output only valid JSON format
            - Do not include any explanation or additional text, just the JSON
            - Only use labels that already exist in the repository.
              - Do not add comments or modify the issue content.
              - Do not remove the following labels maintainer, help wanted or good first issue.
              - Triage only the current issue.
              - Identify only one area/ label.
              - Identify only one kind/ label (Do not apply kind/duplicate or kind/parent-issue)
              - Identify only one priority/ label.
              - Once you categorize the issue if it needs information bump down the priority by 1 eg.. a p0 would become a p1 a p1 would become a p2. P2 and P3 can stay as is in this scenario.

            Categorization Guidelines (Priority):
            P0 - Urgent Blocking Issues:
              - DO NOT APPLY THIS LABEL AUTOMATICALLY. Use status/manual-triage instead.
              - Definition: Urgent, block a significant percentage of the user base, and prevent frequent use of the Gemini CLI.
              - This includes core stability blockers (e.g., authentication failures, broken upgrades), critical crashes, and P0 security vulnerabilities.
              - Impact: Blocks development or testing for the entire team; Major security vulnerability; Causes data loss or corruption with no workaround; Crashes the application or makes a core feature completely unusable for all or most users.
              - Qualifier: Is the main function of the software broken?
            P1 - High-Impact Issues:
              - Definition: Affect a large number of users, blocking them from using parts of the Gemini CLI, or make the CLI frequently unusable even with workarounds available.
              - Impact: A core feature is broken or behaving incorrectly for a large number of users or use cases; Severe performance degradation; No straightforward workaround exists.
              - Qualifier: Is a key feature unusable or giving very wrong results?
            P2 - Significant Issues:
              - Definition: Affect some users significantly, such as preventing the use of certain features or authentication types.
              - Can also be issues that many users complain about, causing annoyance or hindering daily use.
              - Impact: Affects a non-critical feature or a smaller, specific subset of users; An inconvenient but functional workaround is available; Noticeable UI/UX problems that look unprofessional.
              - Qualifier: Is it an annoying but non-blocking problem?
            P3 - Low-Impact Issues:
              - Definition: Typically usability issues that cause annoyance to a limited user base.
              - Includes feature requests that could be addressed in the near future and may be suitable for community contributions.
              - Impact: Minor cosmetic issues; An edge-case bug that is very difficult to reproduce and affects a tiny fraction of users.
              - Qualifier: Is it a "nice-to-fix" issue?

            Categorization Guidelines (Area):
            area/agent: Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality
            area/core: User Interface, OS Support, Core Functionality
            area/documentation: End-user and contributor-facing documentation, website-related
            area/enterprise: Telemetry, Policy, Quota / Licensing
            area/extensions: Gemini CLI extensions capability
            area/non-interactive: GitHub Actions, SDK, 3P Integrations, Shell Scripting, Command line automation
            area/platform: Build infra, Release mgmt, Testing, Eval infra, Capacity, Quota mgmt
            area/security: security related issues

            Additional Context:
            - If users are talking about issues where the model gets downgraded from pro to flash then i want you to categorize that as a performance issue.
            - This product is designed to use different models eg.. using pro, downgrading to flash etc.
            - When users report that they dont expect the model to change those would be categorized as feature requests.

      - name: 'Apply Labels to Issues'
        if: |-
          ${{ steps.gemini_issue_analysis.outcome == 'success' &&
              steps.gemini_issue_analysis.outputs.summary != '[]' }}
        env:
          REPOSITORY: '${{ github.repository }}'
          LABELS_OUTPUT: '${{ steps.gemini_issue_analysis.outputs.summary }}'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token }}'
          script: |-
            const rawLabels = process.env.LABELS_OUTPUT;
            core.info(`Raw labels JSON: ${rawLabels}`);
            let parsedLabels;
            try {
              const jsonMatch = rawLabels.match(/```json\s*([\s\S]*?)\s*```/);
              if (!jsonMatch || !jsonMatch[1]) {
                throw new Error("Could not find a ```json ... ``` block in the output.");
              }
              const jsonString = jsonMatch[1].trim();
              parsedLabels = JSON.parse(jsonString);
              core.info(`Parsed labels JSON: ${JSON.stringify(parsedLabels)}`);
            } catch (err) {
              core.setFailed(`Failed to parse labels JSON from Gemini output: ${err.message}\nRaw output: ${rawLabels}`);
              return;
            }

            for (const entry of parsedLabels) {
              const issueNumber = entry.issue_number;
              if (!issueNumber) {
                core.info(`Skipping entry with no issue number: ${JSON.stringify(entry)}`);
                continue;
              }

              const labelsToAdd = entry.labels_to_add || [];
              labelsToAdd.push('status/bot-triaged');

              if (labelsToAdd.length > 0) {
                await github.rest.issues.addLabels({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: issueNumber,
                  labels: labelsToAdd
                });
                const explanation = entry.explanation ? ` - ${entry.explanation}` : '';
                core.info(`Successfully added labels for #${issueNumber}: ${labelsToAdd.join(', ')}${explanation}`);
              }

              if (entry.explanation) {
                await github.rest.issues.createComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: issueNumber,
                  body: entry.explanation,
                });
              }

              if ((!entry.labels_to_add || entry.labels_to_add.length === 0) && (!entry.labels_to_remove || entry.labels_to_remove.length === 0)) {
                core.info(`No labels to add or remove for #${issueNumber}, leaving as is`);
              }
            }
gemini-scheduled-pr-triage .github/workflows/gemini-scheduled-pr-triage.yml
Triggers
schedule, workflow_dispatch
Runs on
ubuntu-latest
Jobs
audit-prs
Actions
actions/create-github-app-token
Commands
  • ./.github/scripts/pr-triage.sh # If prs_needing_comment is empty, set it to [] explicitly for downstream steps if [[ -z "$(grep 'prs_needing_comment' "${GITHUB_OUTPUT}" | cut -d'=' -f2-)" ]]; then echo "prs_needing_comment=[]" >> "${GITHUB_OUTPUT}" fi
View raw YAML
name: 'Gemini Scheduled PR Triage 🚀'

on:
  schedule:
    - cron: '*/15 * * * *' # Runs every 15 minutes
  workflow_dispatch:

jobs:
  audit-prs:
    timeout-minutes: 15
    if: |-
      ${{ github.repository == 'google-gemini/gemini-cli' }}
    permissions:
      contents: 'read'
      id-token: 'write'
      issues: 'write'
      pull-requests: 'write'
    runs-on: 'ubuntu-latest'
    outputs:
      prs_needing_comment: '${{ steps.run_triage.outputs.prs_needing_comment }}'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'
          permission-issues: 'write'
          permission-pull-requests: 'write'

      - name: 'Run PR Triage Script'
        id: 'run_triage'
        shell: 'bash'
        env:
          GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}'
          GITHUB_REPOSITORY: '${{ github.repository }}'
        run: |-
          ./.github/scripts/pr-triage.sh
          # If prs_needing_comment is empty, set it to [] explicitly for downstream steps
          if [[ -z "$(grep 'prs_needing_comment' "${GITHUB_OUTPUT}" | cut -d'=' -f2-)" ]]; then
            echo "prs_needing_comment=[]" >> "${GITHUB_OUTPUT}"
          fi
gemini-scheduled-stale-issue-closer .github/workflows/gemini-scheduled-stale-issue-closer.yml
Triggers
schedule, workflow_dispatch
Runs on
ubuntu-latest
Jobs
close-stale-issues
Actions
actions/create-github-app-token
View raw YAML
name: '🔒 Gemini Scheduled Stale Issue Closer'

on:
  schedule:
    - cron: '0 0 * * 0' # Every Sunday at midnight UTC
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Run in dry-run mode (no changes applied)'
        required: false
        default: false
        type: 'boolean'

concurrency:
  group: '${{ github.workflow }}'
  cancel-in-progress: true

defaults:
  run:
    shell: 'bash'

jobs:
  close-stale-issues:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'ubuntu-latest'
    permissions:
      issues: 'write'
    steps:
      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'
          permission-issues: 'write'

      - name: 'Process Stale Issues'
        uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
        env:
          DRY_RUN: '${{ inputs.dry_run }}'
        with:
          github-token: '${{ steps.generate_token.outputs.token }}'
          script: |
            const dryRun = process.env.DRY_RUN === 'true';
            if (dryRun) {
              core.info('DRY RUN MODE ENABLED: No changes will be applied.');
            }
            const batchLabel = 'Stale';

            const threeMonthsAgo = new Date();
            threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);

            const tenDaysAgo = new Date();
            tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);

            core.info(`Cutoff date for creation: ${threeMonthsAgo.toISOString()}`);
            core.info(`Cutoff date for updates: ${tenDaysAgo.toISOString()}`);

            const query = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open created:<${threeMonthsAgo.toISOString()}`;
            core.info(`Searching with query: ${query}`);

            const itemsToCheck = await github.paginate(github.rest.search.issuesAndPullRequests, {
              q: query,
              sort: 'created',
              order: 'asc',
              per_page: 100
            });

            core.info(`Found ${itemsToCheck.length} open issues to check.`);

            let processedCount = 0;

            for (const issue of itemsToCheck) {
              const createdAt = new Date(issue.created_at);
              const updatedAt = new Date(issue.updated_at);
              const reactionCount = issue.reactions.total_count;

              // Basic thresholds
              if (reactionCount >= 5) {
                continue;
              }

              // Skip if it has a maintainer, help wanted, or Public Roadmap label
              const rawLabels = issue.labels.map((l) => l.name);
              const lowercaseLabels = rawLabels.map((l) => l.toLowerCase());
              if (
                lowercaseLabels.some((l) => l.includes('maintainer')) ||
                lowercaseLabels.includes('help wanted') ||
                rawLabels.includes('🗓️ Public Roadmap')
              ) {
                continue;
              }

              let isStale = updatedAt < tenDaysAgo;

              // If apparently active, check if it's only bot activity
              if (!isStale) {
                try {
                  const comments = await github.rest.issues.listComments({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: issue.number,
                    per_page: 100,
                    sort: 'created',
                    direction: 'desc'
                  });

                  const lastHumanComment = comments.data.find(comment => comment.user.type !== 'Bot');
                  if (lastHumanComment) {
                    isStale = new Date(lastHumanComment.created_at) < tenDaysAgo;
                  } else {
                    // No human comments. Check if creator is human.
                    if (issue.user.type !== 'Bot') {
                       isStale = createdAt < tenDaysAgo;
                    } else {
                       isStale = true; // Bot created, only bot comments
                    }
                  }
                } catch (error) {
                  core.warning(`Failed to fetch comments for issue #${issue.number}: ${error.message}`);
                  continue;
                }
              }

              if (isStale) {
                processedCount++;
                const message = `Closing stale issue #${issue.number}: "${issue.title}" (${issue.html_url})`;
                core.info(message);

                if (!dryRun) {
                  // Add label
                  await github.rest.issues.addLabels({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: issue.number,
                    labels: [batchLabel]
                  });

                  // Add comment
                  await github.rest.issues.createComment({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: issue.number,
                    body: 'Hello! As part of our effort to keep our backlog manageable and focus on the most active issues, we are tidying up older reports.\n\nIt looks like this issue hasn\'t been active for a while, so we are closing it for now. However, if you are still experiencing this bug on the latest stable build, please feel free to comment on this issue or create a new one with updated details.\n\nThank you for your contribution!'
                  });

                  // Close issue
                  await github.rest.issues.update({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: issue.number,
                    state: 'closed',
                    state_reason: 'not_planned'
                  });
                }
              }
            }

            core.info(`\nTotal issues processed: ${processedCount}`);
gemini-scheduled-stale-pr-closer .github/workflows/gemini-scheduled-stale-pr-closer.yml
Triggers
schedule, pull_request, workflow_dispatch
Runs on
ubuntu-latest
Jobs
close-stale-prs
Actions
actions/create-github-app-token
View raw YAML
name: 'Gemini Scheduled Stale PR Closer'

on:
  schedule:
    - cron: '0 2 * * *' # Every day at 2 AM UTC
  pull_request:
    types: ['opened', 'edited']
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Run in dry-run mode'
        required: false
        default: false
        type: 'boolean'

jobs:
  close-stale-prs:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'ubuntu-latest'
    permissions:
      pull-requests: 'write'
      issues: 'write'
    steps:
      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        env:
          APP_ID: '${{ secrets.APP_ID }}'
        if: |-
          ${{ env.APP_ID != '' }}
        uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'

      - name: 'Process Stale PRs'
        uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
        env:
          DRY_RUN: '${{ inputs.dry_run }}'
        with:
          github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
          script: |
            const dryRun = process.env.DRY_RUN === 'true';
            const fourteenDaysAgo = new Date();
            fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - 14);
            const thirtyDaysAgo = new Date();
            thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

            // 1. Fetch maintainers for verification
            let maintainerLogins = new Set();
            const teams = ['gemini-cli-maintainers', 'gemini-cli-askmode-approvers', 'gemini-cli-docs'];

            for (const team_slug of teams) {
              try {
                const members = await github.paginate(github.rest.teams.listMembersInOrg, {
                  org: context.repo.owner,
                  team_slug: team_slug
                });
                for (const m of members) maintainerLogins.add(m.login.toLowerCase());
                core.info(`Successfully fetched ${members.length} team members from ${team_slug}`);
              } catch (e) {
                // Silently skip if permissions are insufficient; we will rely on author_association
                core.debug(`Skipped team fetch for ${team_slug}: ${e.message}`);
              }
            }

            const isMaintainer = async (login, assoc) => {
              // Reliably identify maintainers using authorAssociation (provided by GitHub)
              // and organization membership (if available).
              const isTeamMember = maintainerLogins.has(login.toLowerCase());
              const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);

              if (isTeamMember || isRepoMaintainer) return true;

              // Fallback: Check if user belongs to the 'google' or 'googlers' orgs (requires permission)
              try {
                const orgs = ['googlers', 'google'];
                for (const org of orgs) {
                  try {
                    await github.rest.orgs.checkMembershipForUser({ org: org, username: login });
                    return true;
                  } catch (e) {
                    if (e.status !== 404) throw e;
                  }
                }
              } catch (e) {
                // Gracefully ignore failures here
              }

              return false;
            };

            // 2. Fetch all open PRs
            let prs = [];
            if (context.eventName === 'pull_request') {
              const { data: pr } = await github.rest.pulls.get({
                owner: context.repo.owner,
                repo: context.repo.repo,
                pull_number: context.payload.pull_request.number
              });
              prs = [pr];
            } else {
              prs = await github.paginate(github.rest.pulls.list, {
                owner: context.repo.owner,
                repo: context.repo.repo,
                state: 'open',
                per_page: 100
              });
            }

            for (const pr of prs) {
              const maintainerPr = await isMaintainer(pr.user.login, pr.author_association);
              const isBot = pr.user.type === 'Bot' || pr.user.login.endsWith('[bot]');
              if (maintainerPr || isBot) continue;

              // Helper: Fetch labels and linked issues via GraphQL
              const prDetailsQuery = `query($owner:String!, $repo:String!, $number:Int!) {
                repository(owner:$owner, name:$repo) {
                  pullRequest(number:$number) {
                    closingIssuesReferences(first: 10) {
                      nodes {
                        number
                        labels(first: 20) {
                          nodes { name }
                        }
                      }
                    }
                  }
                }
              }`;

              let linkedIssues = [];
              try {
                const res = await github.graphql(prDetailsQuery, {
                  owner: context.repo.owner, repo: context.repo.repo, number: pr.number
                });
                linkedIssues = res.repository.pullRequest.closingIssuesReferences.nodes;
              } catch (e) {
                core.warning(`GraphQL fetch failed for PR #${pr.number}: ${e.message}`);
              }

              // Check for mentions in body as fallback (regex)
              const body = pr.body || '';
              const mentionRegex = /(?:#|https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/)(\d+)/i;
              const matches = body.match(mentionRegex);
              if (matches && linkedIssues.length === 0) {
                const issueNumber = parseInt(matches[1]);
                try {
                  const { data: issue } = await github.rest.issues.get({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: issueNumber
                  });
                  linkedIssues = [{ number: issueNumber, labels: { nodes: issue.labels.map(l => ({ name: l.name })) } }];
                } catch (e) {}
              }

              // 3. Enforcement Logic
              const prLabels = pr.labels.map(l => l.name.toLowerCase());
              const hasHelpWanted = prLabels.includes('help wanted') ||
                                    linkedIssues.some(issue => issue.labels.nodes.some(l => l.name.toLowerCase() === 'help wanted'));

              const hasMaintainerOnly = prLabels.includes('🔒 maintainer only') ||
                                        linkedIssues.some(issue => issue.labels.nodes.some(l => l.name.toLowerCase() === '🔒 maintainer only'));

              const hasLinkedIssue = linkedIssues.length > 0;

              // Closure Policy: No help-wanted label = Close after 14 days
              if (pr.state === 'open' && !hasHelpWanted && !hasMaintainerOnly) {
                const prCreatedAt = new Date(pr.created_at);

                // We give a 14-day grace period for non-help-wanted PRs to be manually reviewed/labeled by an EM
                if (prCreatedAt > fourteenDaysAgo) {
                  core.info(`PR #${pr.number} is new and lacks 'help wanted'. Giving 14-day grace period for EM review.`);
                  continue;
                }

                core.info(`PR #${pr.number} is older than 14 days and lacks 'help wanted' association. Closing.`);
                if (!dryRun) {
                  await github.rest.issues.createComment({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: pr.number,
                    body: "Hi there! Thank you for your interest in contributing to Gemini CLI. \n\nTo ensure we maintain high code quality and focus on our prioritized roadmap, we have updated our contribution policy (see [Discussion #17383](https://github.com/google-gemini/gemini-cli/discussions/17383)). \n\n**We only *guarantee* review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.** All other community pull requests are subject to closure after 14 days if they do not align with our current focus areas. For this reason, we strongly recommend that contributors only submit pull requests against issues explicitly labeled as **'help-wanted'**. \n\nThis pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding and for being part of our community!"
                  });
                  await github.rest.pulls.update({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    pull_number: pr.number,
                    state: 'closed'
                  });
                }
                continue;
              }

              // Also check for linked issue even if it has help wanted (redundant but safe)
              if (pr.state === 'open' && !hasLinkedIssue) {
                 // Already covered by hasHelpWanted check above, but good for future-proofing
                 continue;
              }

              // 4. Staleness Check (Scheduled only)
              if (pr.state === 'open' && context.eventName !== 'pull_request') {
                // Skip PRs that were created less than 30 days ago - they cannot be stale yet
                const prCreatedAt = new Date(pr.created_at);
                if (prCreatedAt > thirtyDaysAgo) continue;

                let lastActivity = new Date(pr.created_at);
                try {
                  const reviews = await github.paginate(github.rest.pulls.listReviews, {
                    owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number
                  });
                  for (const r of reviews) {
                    if (await isMaintainer(r.user.login, r.author_association)) {
                      const d = new Date(r.submitted_at || r.updated_at);
                      if (d > lastActivity) lastActivity = d;
                    }
                  }
                  const comments = await github.paginate(github.rest.issues.listComments, {
                    owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number
                  });
                  for (const c of comments) {
                    if (await isMaintainer(c.user.login, c.author_association)) {
                      const d = new Date(c.updated_at);
                      if (d > lastActivity) lastActivity = d;
                    }
                  }
                } catch (e) {}

                if (lastActivity < thirtyDaysAgo) {
                  const labels = pr.labels.map(l => l.name.toLowerCase());
                  const isProtected = labels.includes('help wanted') || labels.includes('🔒 maintainer only');
                  if (isProtected) {
                    core.info(`PR #${pr.number} is stale but has a protected label. Skipping closure.`);
                    continue;
                  }

                  core.info(`PR #${pr.number} is stale (no maintainer activity for 30+ days). Closing.`);
                  if (!dryRun) {
                    await github.rest.issues.createComment({
                      owner: context.repo.owner,
                      repo: context.repo.repo,
                      issue_number: pr.number,
                      body: "Hi there! Thank you for your contribution. To keep our backlog manageable, we are closing pull requests that haven't seen maintainer activity for 30 days. If you're still working on this, please let us know!"
                    });
                    await github.rest.pulls.update({
                      owner: context.repo.owner,
                      repo: context.repo.repo,
                      pull_number: pr.number,
                      state: 'closed'
                    });
                  }
                }
              }
            }
gemini-self-assign-issue perms .github/workflows/gemini-self-assign-issue.yml
Triggers
issue_comment
Runs on
ubuntu-latest
Jobs
self-assign-issue
Actions
actions/create-github-app-token
View raw YAML
name: 'Assign Issue on Comment'

on:
  issue_comment:
    types:
      - 'created'

concurrency:
  group: '${{ github.workflow }}-${{ github.event.issue.number }}'
  cancel-in-progress: true

defaults:
  run:
    shell: 'bash'

permissions:
  contents: 'read'
  id-token: 'write'
  issues: 'write'
  statuses: 'write'
  packages: 'read'

jobs:
  self-assign-issue:
    if: |-
      github.repository == 'google-gemini/gemini-cli' &&
      github.event_name == 'issue_comment' &&
      (contains(github.event.comment.body, '/assign') || contains(github.event.comment.body, '/unassign'))
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b'
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'
          # Add 'assignments' write permission
          permission-issues: 'write'

      - name: 'Assign issue to user'
        if: "contains(github.event.comment.body, '/assign')"
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token }}'
          script: |
            const issueNumber = context.issue.number;
            const commenter = context.actor;
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const MAX_ISSUES_ASSIGNED = 3;

            const issue = await github.rest.issues.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
            });

            const hasHelpWantedLabel = issue.data.labels.some(label => label.name === 'help wanted');

            if (!hasHelpWantedLabel) {
              await github.rest.issues.createComment({
                owner: owner,
                repo: repo,
                issue_number: issueNumber,
                body: `👋 @${commenter}, thanks for your interest in this issue! We're reserving self-assignment for issues that have been marked with the \`help wanted\` label. Feel free to check out our list of [issues that need attention](https://github.com/google-gemini/gemini-cli/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).`
              });
              return;
            }

            // Search for open issues already assigned to the commenter in this repo
            const { data: assignedIssues } = await github.rest.search.issuesAndPullRequests({
              q: `is:issue repo:${owner}/${repo} assignee:${commenter} is:open`,
              advanced_search: true
            });

            if (assignedIssues.total_count >= MAX_ISSUES_ASSIGNED) {
              await github.rest.issues.createComment({
                owner: owner,
                repo: repo,
                issue_number: issueNumber,
                body: `👋 @${commenter}! You currently have ${assignedIssues.total_count} issues assigned to you. We have a ${MAX_ISSUES_ASSIGNED} max issues assigned at once policy. Once you close out an existing issue it will open up space to take another. You can also unassign yourself from an existing issue but please work on a hand-off if someone is expecting work on that issue.`
              });
              return; // exit
            }

            if (issue.data.assignees.length > 0) {
              // Comment that it's already assigned
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: issueNumber,
                body: `@${commenter} Thanks for taking interest but this issue is already assigned. We'd still love to have you contribute. Check out our [Help Wanted](https://github.com/google-gemini/gemini-cli/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22) list for issues where we need some extra attention.`
              });
              return;
            }

            // If not taken, assign the user who commented
            await github.rest.issues.addAssignees({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
              assignees: [commenter]
            });

            // Post a comment to confirm assignment
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: issueNumber,
              body: `👋 @${commenter}, you've been assigned to this issue! Thank you for taking the time to contribute. Make sure to check out our [contributing guidelines](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md).`
            });

      - name: 'Unassign issue from user'
        if: "contains(github.event.comment.body, '/unassign')"
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token }}'
          script: |
            const issueNumber = context.issue.number;
            const commenter = context.actor;
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const commentBody = context.payload.comment.body.trim();

            if (commentBody !== '/unassign') {
              return;
            }

            const issue = await github.rest.issues.get({
              owner: owner,
              repo: repo,
              issue_number: issueNumber,
            });

            const isAssigned = issue.data.assignees.some(assignee => assignee.login === commenter);

            if (isAssigned) {
              await github.rest.issues.removeAssignees({
                owner: owner,
                repo: repo,
                issue_number: issueNumber,
                assignees: [commenter]
              });
              await github.rest.issues.createComment({
                owner: owner,
                repo: repo,
                issue_number: issueNumber,
                body: `👋 @${commenter}, you have been unassigned from this issue.`
              });
            }
issue-opened-labeler .github/workflows/issue-opened-labeler.yml
Triggers
issues
Runs on
ubuntu-latest
Jobs
label-issue
Actions
actions/create-github-app-token
View raw YAML
name: '🏷️ Issue Opened Labeler'

on:
  issues:
    types:
      - 'opened'

jobs:
  label-issue:
    runs-on: 'ubuntu-latest'
    if: |-
      ${{ github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli' }}
    steps:
      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        env:
          APP_ID: '${{ secrets.APP_ID }}'
        if: |-
          ${{ env.APP_ID != '' }}
        uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'

      - name: 'Add need-triage label'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
          script: |-
            const { data: issue } = await github.rest.issues.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });

            const hasLabel = issue.labels.some(l => l.name === 'status/need-triage');
            if (!hasLabel) {
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                labels: ['status/need-triage']
              });
            } else {
              core.info('Issue already has status/need-triage label. Skipping.');
            }
label-backlog-child-issues perms .github/workflows/label-backlog-child-issues.yml
Triggers
issues, schedule, workflow_dispatch
Runs on
ubuntu-latest, ubuntu-latest
Jobs
labeler, sync-maintainer-labels
Commands
  • npm ci
  • node .github/scripts/sync-maintainer-labels.cjs
  • npm ci
  • node .github/scripts/sync-maintainer-labels.cjs
View raw YAML
name: 'Label Child Issues for Project Rollup'

on:
  issues:
    types: ['opened', 'edited', 'reopened']
  schedule:
    - cron: '0 * * * *' # Run every hour
  workflow_dispatch:

permissions:
  issues: 'write'
  contents: 'read'

jobs:
  # Event-based: Quick reaction to new/edited issues in THIS repo
  labeler:
    if: "github.repository == 'google-gemini/gemini-cli' && github.event_name == 'issues'"
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: 'Install Dependencies'
        run: 'npm ci'

      - name: 'Run Multi-Repo Sync Script'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
        run: 'node .github/scripts/sync-maintainer-labels.cjs'

  # Scheduled/Manual: Recursive sync across multiple repos
  sync-maintainer-labels:
    if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')"
    runs-on: 'ubuntu-latest'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: 'Install Dependencies'
        run: 'npm ci'

      - name: 'Run Multi-Repo Sync Script'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
        run: 'node .github/scripts/sync-maintainer-labels.cjs'
label-workstream-rollup .github/workflows/label-workstream-rollup.yml
Triggers
issues, schedule, workflow_dispatch
Runs on
ubuntu-latest
Jobs
labeler
View raw YAML
name: 'Label Workstream Rollup'

on:
  issues:
    types: ['opened', 'edited', 'reopened']
  schedule:
    - cron: '0 * * * *'
  workflow_dispatch:

jobs:
  labeler:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'ubuntu-latest'
    permissions:
      issues: 'write'
    steps:
      - name: 'Check for Parent Workstream and Apply Label'
        uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
        with:
          script: |
            const labelToAdd = 'workstream-rollup';

            // Allow-list of parent issue URLs
            const allowedParentUrls = [
              'https://github.com/google-gemini/gemini-cli/issues/15374',
              'https://github.com/google-gemini/gemini-cli/issues/15456',
              'https://github.com/google-gemini/gemini-cli/issues/15324',
              'https://github.com/google-gemini/gemini-cli/issues/17202',
              'https://github.com/google-gemini/gemini-cli/issues/17203'
            ];

            // Single issue processing (for event triggers)
            async function processSingleIssue(owner, repo, number) {
              const query = `
                query($owner:String!, $repo:String!, $number:Int!) {
                  repository(owner:$owner, name:$repo) {
                    issue(number:$number) {
                      number
                      parent {
                        url
                        parent {
                          url
                          parent {
                            url
                            parent {
                              url
                              parent {
                                url
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              `;
              try {
                const result = await github.graphql(query, { owner, repo, number });

                if (!result || !result.repository || !result.repository.issue) {
                  console.log(`Issue #${number} not found or data missing.`);
                  return;
                }

                const issue = result.repository.issue;
                await checkAndLabel(issue, owner, repo);
              } catch (error) {
                console.error(`Failed to process issue #${number}:`, error);
                throw error; // Re-throw to be caught by main execution
              }
            }

            // Bulk processing (for schedule/dispatch)
            async function processAllOpenIssues(owner, repo) {
              const query = `
                query($owner:String!, $repo:String!, $cursor:String) {
                  repository(owner:$owner, name:$repo) {
                    issues(first: 100, states: OPEN, after: $cursor) {
                      pageInfo {
                        hasNextPage
                        endCursor
                      }
                      nodes {
                        number
                        parent {
                          url
                          parent {
                            url
                            parent {
                              url
                              parent {
                                url
                                parent {
                                  url
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              `;

              let hasNextPage = true;
              let cursor = null;

              while (hasNextPage) {
                try {
                  const result = await github.graphql(query, { owner, repo, cursor });

                  if (!result || !result.repository || !result.repository.issues) {
                     console.error('Invalid response structure from GitHub API');
                     break;
                  }

                  const issues = result.repository.issues.nodes || [];

                  console.log(`Processing batch of ${issues.length} issues...`);
                  for (const issue of issues) {
                    await checkAndLabel(issue, owner, repo);
                  }

                  hasNextPage = result.repository.issues.pageInfo.hasNextPage;
                  cursor = result.repository.issues.pageInfo.endCursor;
                } catch (error) {
                  console.error('Failed to fetch issues batch:', error);
                  throw error; // Re-throw to be caught by main execution
                }
              }
            }

            async function checkAndLabel(issue, owner, repo) {
              if (!issue || !issue.parent) return;

              let currentParent = issue.parent;
              let tracedParents = [];
              let matched = false;

              while (currentParent) {
                tracedParents.push(currentParent.url);

                if (allowedParentUrls.includes(currentParent.url)) {
                  console.log(`SUCCESS: Issue #${issue.number} is a descendant of ${currentParent.url}. Trace: ${tracedParents.join(' -> ')}. Adding label.`);
                  await github.rest.issues.addLabels({
                    owner,
                    repo,
                    issue_number: issue.number,
                    labels: [labelToAdd]
                  });
                  matched = true;
                  break;
                }
                currentParent = currentParent.parent;
              }

              if (!matched && context.eventName === 'issues') {
                 console.log(`Issue #${issue.number} did not match any allowed workstreams. Trace: ${tracedParents.join(' -> ') || 'None'}.`);
              }
            }

            // Main execution
            try {
              if (context.eventName === 'issues') {
                console.log(`Processing single issue #${context.payload.issue.number}...`);
                await processSingleIssue(context.repo.owner, context.repo.repo, context.payload.issue.number);
              } else {
                console.log(`Running for event: ${context.eventName}. Processing all open issues...`);
                await processAllOpenIssues(context.repo.owner, context.repo.repo);
              }
            } catch (error) {
              core.setFailed(`Workflow failed: ${error.message}`);
            }
links .github/workflows/links.yml
Triggers
push, pull_request, repository_dispatch, workflow_dispatch, schedule
Runs on
ubuntu-latest
Jobs
linkChecker
Actions
lycheeverse/lychee-action
View raw YAML
name: 'Links'

on:
  push:
    branches: ['main']
  pull_request:
    branches: ['main']
  repository_dispatch:
  workflow_dispatch:
  schedule:
    - cron: '00 18 * * *'

jobs:
  linkChecker:
    if: |-
      ${{ github.repository == 'google-gemini/gemini-cli' }}
    runs-on: 'ubuntu-latest'
    steps:
      - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5

      - name: 'Link Checker'
        id: 'lychee'
        uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
        with:
          args: '--verbose --no-progress --accept 200,503 ./**/*.md'
no-response .github/workflows/no-response.yml
Triggers
schedule, workflow_dispatch
Runs on
ubuntu-latest
Jobs
no-response
Actions
actions/stale
View raw YAML
name: 'No Response'

# Run as a daily cron at 1:45 AM
on:
  schedule:
    - cron: '45 1 * * *'
  workflow_dispatch:

jobs:
  no-response:
    runs-on: 'ubuntu-latest'
    if: |-
      ${{ github.repository == 'google-gemini/gemini-cli' }}
    permissions:
      issues: 'write'
      pull-requests: 'write'
    concurrency:
      group: '${{ github.workflow }}-no-response'
      cancel-in-progress: true
    steps:
      - uses: 'actions/stale@5bef64f19d7facfb25b37b414482c7164d639639' # ratchet:actions/stale@v9
        with:
          repo-token: '${{ secrets.GITHUB_TOKEN }}'
          days-before-stale: -1
          days-before-close: 14
          stale-issue-label: 'status/need-information'
          close-issue-message: >-
            This issue was marked as needing more information and has not received a response in 14 days.
            Closing it for now. If you still face this problem, feel free to reopen with more details. Thank you!
          stale-pr-label: 'status/need-information'
          close-pr-message: >-
            This pull request was marked as needing more information and has had no updates in 14 days.
            Closing it for now. You are welcome to reopen with the required info. Thanks for contributing!
pr-contribution-guidelines-notifier .github/workflows/pr-contribution-guidelines-notifier.yml
Triggers
pull_request
Runs on
ubuntu-latest
Jobs
notify-process-change
Actions
actions/create-github-app-token
View raw YAML
name: '🏷️ PR Contribution Guidelines Notifier'

on:
  pull_request:
    types:
      - 'opened'

jobs:
  notify-process-change:
    runs-on: 'ubuntu-latest'
    if: |-
      github.repository == 'google-gemini/gemini-cli' || github.repository == 'google-gemini/maintainers-gemini-cli'
    permissions:
      pull-requests: 'write'
    steps:
      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        env:
          APP_ID: '${{ secrets.APP_ID }}'
        if: |-
          ${{ env.APP_ID != '' }}
        uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'

      - name: 'Check membership and post comment'
        uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
        with:
          github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
          script: |-
            const org = context.repo.owner;
            const repo = context.repo.repo;
            const username = context.payload.pull_request.user.login;
            const pr_number = context.payload.pull_request.number;

            // 1. Check if the PR author is a maintainer
            // Check team membership (most reliable for private org members)
            let isTeamMember = false;
            const teams = ['gemini-cli-maintainers', 'gemini-cli-askmode-approvers', 'gemini-cli-docs'];
            for (const team_slug of teams) {
              try {
                const members = await github.paginate(github.rest.teams.listMembersInOrg, {
                  org: org,
                  team_slug: team_slug
                });
                if (members.some(m => m.login.toLowerCase() === username.toLowerCase())) {
                  isTeamMember = true;
                  core.info(`${username} is a member of ${team_slug}. No notification needed.`);
                  break;
                }
              } catch (e) {
                core.warning(`Failed to fetch team members from ${team_slug}: ${e.message}`);
              }
            }

            if (isTeamMember) return;

            // Check author_association from webhook payload
            const authorAssociation = context.payload.pull_request.author_association;
            const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(authorAssociation);

            if (isRepoMaintainer) {
              core.info(`${username} is a maintainer (author_association: ${authorAssociation}). No notification needed.`);
              return;
            }

            // Check if author is a Googler
            const isGoogler = async (login) => {
              try {
                const orgs = ['googlers', 'google'];
                for (const org of orgs) {
                  try {
                    await github.rest.orgs.checkMembershipForUser({
                      org: org,
                      username: login
                    });
                    return true;
                  } catch (e) {
                    if (e.status !== 404) throw e;
                  }
                }
              } catch (e) {
                core.warning(`Failed to check org membership for ${login}: ${e.message}`);
              }
              return false;
            };

            if (await isGoogler(username)) {
              core.info(`${username} is a Googler. No notification needed.`);
              return;
            }

            // 2. Check if the PR is already associated with an issue
            const query = `
              query($owner:String!, $repo:String!, $number:Int!) {
                repository(owner:$owner, name:$repo) {
                  pullRequest(number:$number) {
                    closingIssuesReferences(first: 1) {
                      totalCount
                    }
                  }
                }
              }
            `;
            const variables = { owner: org, repo: repo, number: pr_number };
            const result = await github.graphql(query, variables);
            const issueCount = result.repository.pullRequest.closingIssuesReferences.totalCount;

            if (issueCount > 0) {
              core.info(`PR #${pr_number} is already associated with an issue. No notification needed.`);
              return;
            }

            // 3. Post the notification comment
            core.info(`${username} is not a maintainer and PR #${pr_number} has no linked issue. Posting notification.`);

            const comment = `
            Hi @${username}, thank you so much for your contribution to Gemini CLI! We really appreciate the time and effort you've put into this.

            We're making some updates to our contribution process to improve how we track and review changes. Please take a moment to review our recent discussion post: [Improving Our Contribution Process & Introducing New Guidelines](https://github.com/google-gemini/gemini-cli/discussions/16706).

            Key Update: Starting **January 26, 2026**, the Gemini CLI project will require all pull requests to be associated with an existing issue. Any pull requests not linked to an issue by that date will be automatically closed.

            Thank you for your understanding and for being a part of our community!
            `.trim().replace(/^[ ]+/gm, '');

            await github.rest.issues.createComment({
              owner: org,
              repo: repo,
              issue_number: pr_number,
              body: comment
            });
pr-rate-limiter perms .github/workflows/pr-rate-limiter.yaml
Triggers
pull_request_target
Runs on
gemini-cli-ubuntu-16-core
Jobs
limit
Actions
Homebrew/actions/limit-pull-requests
View raw YAML
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

name: 'PR rate limiter'

permissions: {}

on:
  pull_request_target:
    types:
      - 'opened'
      - 'reopened'

jobs:
  limit:
    runs-on: 'gemini-cli-ubuntu-16-core'
    permissions:
      contents: 'read'
      pull-requests: 'write'
    steps:
      - name: 'Limit open pull requests per user'
        uses: 'Homebrew/actions/limit-pull-requests@9ceb7934560eb61d131dde205a6c2d77b2e1529d' # master
        with:
          except-author-associations: 'MEMBER,OWNER,COLLABORATOR'
          comment-limit: 8
          comment: >
            You already have 7 pull requests open. Please work on getting
            existing PRs merged before opening more.
          close-limit: 8
          close: true
release-change-tags .github/workflows/release-change-tags.yml
Triggers
workflow_dispatch
Runs on
ubuntu-latest
Jobs
change-tags
View raw YAML
name: 'Release: Change Tags'

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'The package version to tag (e.g., 0.5.0-preview-2). This version must already exist on the npm registry.'
        required: true
        type: 'string'
      channel:
        description: 'The npm dist-tag to apply (e.g., latest, preview, nightly).'
        required: true
        type: 'choice'
        options:
          - 'dev'
          - 'latest'
          - 'preview'
          - 'nightly'
      dry-run:
        description: 'Whether to run in dry-run mode.'
        required: false
        type: 'boolean'
        default: true
      environment:
        description: 'Environment'
        required: false
        type: 'choice'
        options:
          - 'prod'
          - 'dev'
        default: 'prod'

jobs:
  change-tags:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'ubuntu-latest'
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    permissions:
      packages: 'write'
      issues: 'write'
    steps:
      - name: 'Checkout repository'
        uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
        with:
          ref: '${{ github.ref }}'
          fetch-depth: 0

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
        with:
          node-version-file: '.nvmrc'

      - name: 'Change tag'
        uses: './.github/actions/tag-npm-release'
        with:
          channel: '${{ github.event.inputs.channel }}'
          version: '${{ github.event.inputs.version }}'
          dry-run: '${{ github.event.inputs.dry-run }}'
          wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
          wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
          wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
          core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
          a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
          working-directory: '.'
release-manual .github/workflows/release-manual.yml
Triggers
workflow_dispatch
Runs on
ubuntu-latest
Jobs
release
Commands
  • echo "$JSON_INPUTS"
  • npm ci
  • RELEASE_VERSION="${INPUT_VERSION}" echo "RELEASE_VERSION=${RELEASE_VERSION#v}" >> "${GITHUB_OUTPUT}" echo "PREVIOUS_TAG=$(git describe --tags --abbrev=0)" >> "${GITHUB_OUTPUT}"
  • gh issue create \ --title 'Manual Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \ --body 'The manual release workflow failed. See the full run for details: ${DETAILS_URL}' \ --label 'release-failure,priority/p0'
View raw YAML
name: 'Release: Manual'

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'The version to release (e.g., v0.1.11). Must be a valid semver string with a "v" prefix.'
        required: true
        type: 'string'
      ref:
        description: 'The branch, tag, or SHA to release from.'
        required: true
        type: 'string'
      npm_channel:
        description: 'The npm channel to publish to'
        required: true
        type: 'choice'
        options:
          - 'dev'
          - 'preview'
          - 'nightly'
          - 'latest'
        default: 'latest'
      dry_run:
        description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
        required: true
        type: 'boolean'
        default: true
      force_skip_tests:
        description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
        required: false
        type: 'boolean'
        default: false
      skip_github_release:
        description: 'Select to skip creating a GitHub release (only used when environment is PROD)'
        required: false
        type: 'boolean'
        default: false
      environment:
        description: 'Environment'
        required: false
        type: 'choice'
        options:
          - 'prod'
          - 'dev'
        default: 'prod'

jobs:
  release:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'ubuntu-latest'
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    permissions:
      contents: 'write'
      packages: 'write'
      issues: 'write'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          fetch-depth: 0

      - name: 'Checkout Release Code'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ github.event.inputs.ref }}'
          path: 'release'
          fetch-depth: 0

      - name: 'Debug Inputs'
        shell: 'bash'
        env:
          JSON_INPUTS: '${{ toJSON(inputs) }}'
        run: 'echo "$JSON_INPUTS"'

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
        with:
          node-version-file: './release/.nvmrc'
          cache: 'npm'

      - name: 'Install Dependencies'
        working-directory: './release'
        run: 'npm ci'

      - name: 'Prepare Release Info'
        id: 'release_info'
        working-directory: './release'
        env:
          INPUT_VERSION: '${{ github.event.inputs.version }}'
        run: |
          RELEASE_VERSION="${INPUT_VERSION}"
          echo "RELEASE_VERSION=${RELEASE_VERSION#v}" >> "${GITHUB_OUTPUT}"
          echo "PREVIOUS_TAG=$(git describe --tags --abbrev=0)" >> "${GITHUB_OUTPUT}"

      - name: 'Run Tests'
        if: "${{github.event.inputs.force_skip_tests != 'true'}}"
        uses: './.github/actions/run-tests'
        with:
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          working-directory: './release'

      - name: 'Publish Release'
        uses: './.github/actions/publish-release'
        with:
          force-skip-tests: '${{ github.event.inputs.force_skip_tests }}'
          release-version: '${{ steps.release_info.outputs.RELEASE_VERSION }}'
          release-tag: '${{ github.event.inputs.version }}'
          npm-tag: '${{ github.event.inputs.npm_channel }}'
          wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
          wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
          wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          dry-run: '${{ github.event.inputs.dry_run }}'
          previous-tag: '${{ steps.release_info.outputs.PREVIOUS_TAG }}'
          skip-github-release: '${{ github.event.inputs.skip_github_release }}'
          working-directory: './release'
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          npm-registry-publish-url: '${{ vars.NPM_REGISTRY_PUBLISH_URL }}'
          npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
          npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
          cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
          core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
          a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'

      - name: 'Create Issue on Failure'
        if: '${{ failure() && github.event.inputs.dry_run == false }}'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          RELEASE_TAG: '${{ github.event.inputs.version }}'
          DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
        run: |
          gh issue create \
            --title 'Manual Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
            --body 'The manual release workflow failed. See the full run for details: ${DETAILS_URL}' \
            --label 'release-failure,priority/p0'
release-nightly .github/workflows/release-nightly.yml
Triggers
schedule, workflow_dispatch
Runs on
ubuntu-latest
Jobs
release
Commands
  • npm ci
  • echo "$JSON_INPUTS"
  • echo "$JSON_VARS"
  • # Calculate the version using the centralized script VERSION_JSON=$(node scripts/get-release-version.js --type=nightly) # Extract values for logging and outputs RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag) RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion) NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag) PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag) # Print calculated values for logging echo "Calculated Release Tag: ${RELEASE_TAG}" echo "Calculated Release Version: ${RELEASE_VERSION}" echo "Calculated Previous Tag: ${PREVIOUS_TAG}" # Set outputs for subsequent steps echo "RELEASE_TAG=${RELEASE_TAG}" >> "${GITHUB_OUTPUT}" echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_OUTPUT}" echo "NPM_TAG=${NPM_TAG}" >> "${GITHUB_OUTPUT}" echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "${GITHUB_OUTPUT}"
  • gh issue create \ --title "Nightly Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \ --body "The nightly-release workflow failed. See the full run for details: ${DETAILS_URL}" \ --label 'release-failure,priority/p0'
View raw YAML
name: 'Release: Nightly'

on:
  schedule:
    - cron: '0 0 * * *'
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
        required: true
        type: 'boolean'
        default: true
      force_skip_tests:
        description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
        required: false
        type: 'boolean'
        default: true
      ref:
        description: 'The branch, tag, or SHA to release from.'
        required: false
        type: 'string'
        default: 'main'
      environment:
        description: 'Environment'
        required: false
        type: 'choice'
        options:
          - 'prod'
          - 'dev'
        default: 'prod'

jobs:
  release:
    if: "github.repository == 'google-gemini/gemini-cli'"
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    runs-on: 'ubuntu-latest'
    permissions:
      contents: 'write'
      packages: 'write'
      issues: 'write'
      pull-requests: 'write'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          fetch-depth: 0

      - name: 'Checkout Release Code'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ github.event.inputs.ref }}'
          path: 'release'
          fetch-depth: 0

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version-file: './release/.nvmrc'
          cache: 'npm'

      - name: 'Install Dependencies'
        working-directory: './release'
        run: 'npm ci'

      - name: 'Print Inputs'
        shell: 'bash'
        env:
          JSON_INPUTS: '${{ toJSON(github.event.inputs) }}'
        run: 'echo "$JSON_INPUTS"'

      - name: 'Calculate Release Variables'
        id: 'vars'
        uses: './.github/actions/calculate-vars'
        with:
          dry_run: '${{ github.event.inputs.dry_run }}'

      - name: 'Print Calculated vars'
        shell: 'bash'
        env:
          JSON_VARS: '${{ toJSON(steps.vars.outputs) }}'
        run: 'echo "$JSON_VARS"'

      - name: 'Run Tests'
        if: "${{ github.event_name == 'schedule' || github.event.inputs.force_skip_tests == 'false' }}"
        uses: './.github/actions/run-tests'
        with:
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          working-directory: './release'

      - name: 'Get Nightly Version'
        id: 'nightly_version'
        working-directory: './release'
        env:
          GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
        run: |
          # Calculate the version using the centralized script
          VERSION_JSON=$(node scripts/get-release-version.js --type=nightly)

          # Extract values for logging and outputs
          RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag)
          RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion)
          NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag)
          PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag)

          # Print calculated values for logging
          echo "Calculated Release Tag: ${RELEASE_TAG}"
          echo "Calculated Release Version: ${RELEASE_VERSION}"
          echo "Calculated Previous Tag: ${PREVIOUS_TAG}"

          # Set outputs for subsequent steps
          echo "RELEASE_TAG=${RELEASE_TAG}" >> "${GITHUB_OUTPUT}"
          echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_OUTPUT}"
          echo "NPM_TAG=${NPM_TAG}" >> "${GITHUB_OUTPUT}"
          echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "${GITHUB_OUTPUT}"

      - name: 'Publish Release'
        if: true
        uses: './.github/actions/publish-release'
        with:
          release-version: '${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
          release-tag: '${{ steps.nightly_version.outputs.RELEASE_TAG }}'
          npm-tag: '${{ steps.nightly_version.outputs.NPM_TAG }}'
          wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
          wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
          wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          dry-run: '${{ steps.vars.outputs.is_dry_run }}'
          previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}'
          working-directory: './release'
          skip-branch-cleanup: true
          force-skip-tests: "${{ github.event_name != 'schedule' && github.event.inputs.force_skip_tests == 'true' }}"
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          npm-registry-publish-url: '${{ vars.NPM_REGISTRY_PUBLISH_URL }}'
          npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
          npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
          cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
          core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
          a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'

      - name: 'Create and Merge Pull Request'
        if: "github.event.inputs.environment != 'dev'"
        uses: './.github/actions/create-pull-request'
        with:
          branch-name: 'release/${{ steps.nightly_version.outputs.RELEASE_TAG }}'
          pr-title: 'chore/release: bump version to ${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
          pr-body: 'Automated version bump for nightly release.'
          github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          dry-run: '${{ steps.vars.outputs.is_dry_run }}'
          working-directory: './release'

      - name: 'Create Issue on Failure'
        if: "${{ failure() && github.event.inputs.environment != 'dev' && (github.event_name == 'schedule' || github.event.inputs.dry_run != 'true') }}"
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          RELEASE_TAG: '${{ steps.nightly_version.outputs.RELEASE_TAG }}'
          DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
        run: |
          gh issue create \
            --title "Nightly Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \
            --body "The nightly-release workflow failed. See the full run for details: ${DETAILS_URL}" \
            --label 'release-failure,priority/p0'
release-notes .github/workflows/release-notes.yml
Triggers
release, workflow_dispatch
Runs on
ubuntu-latest
Jobs
generate-release-notes
Actions
google-github-actions/run-gemini-cli, peter-evans/create-pull-request
Commands
  • VERSION="${{ github.event.inputs.version || github.event.release.tag_name }}" TIME="${{ github.event.inputs.time || github.event.release.created_at }}" echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT" echo "TIME=${TIME}" >> "$GITHUB_OUTPUT" # Use a heredoc to preserve multiline release body echo 'RAW_CHANGELOG<<EOF' >> "$GITHUB_OUTPUT" printf "%s\n" "$BODY" >> "$GITHUB_OUTPUT" echo 'EOF' >> "$GITHUB_OUTPUT"
  • if echo "${{ steps.release_info.outputs.VERSION }}" | grep -q "nightly"; then echo "Nightly release detected. Stopping workflow." echo "CONTINUE=false" >> "$GITHUB_OUTPUT" else echo "CONTINUE=true" >> "$GITHUB_OUTPUT" fi
View raw YAML
# This workflow is triggered on every new release.
# It uses Gemini to generate release notes and creates a PR with the changes.
name: 'Generate Release Notes'

on:
  release:
    types: ['published']
  workflow_dispatch:
    inputs:
      version:
        description: 'New version (e.g., v1.2.3)'
        required: true
        type: 'string'
      body:
        description: 'Release notes body'
        required: true
        type: 'string'
      time:
        description: 'Release time'
        required: true
        type: 'string'

jobs:
  generate-release-notes:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'ubuntu-latest'
    permissions:
      contents: 'write'
      pull-requests: 'write'
    steps:
      - name: 'Checkout repository'
        uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
        with:
          # The user-level skills need to be available to the workflow
          fetch-depth: 0
          ref: 'main'

      - name: 'Set up Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version: '20'

      - name: 'Get release information'
        id: 'release_info'
        run: |
          VERSION="${{ github.event.inputs.version || github.event.release.tag_name }}"
          TIME="${{ github.event.inputs.time || github.event.release.created_at }}"

          echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
          echo "TIME=${TIME}" >> "$GITHUB_OUTPUT"

          # Use a heredoc to preserve multiline release body
          echo 'RAW_CHANGELOG<<EOF' >> "$GITHUB_OUTPUT"
          printf "%s\n" "$BODY" >> "$GITHUB_OUTPUT"
          echo 'EOF' >> "$GITHUB_OUTPUT"
        env:
          GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          BODY: '${{ github.event.inputs.body || github.event.release.body }}'

      - name: 'Validate version'
        id: 'validate_version'
        run: |
          if echo "${{ steps.release_info.outputs.VERSION }}" | grep -q "nightly"; then
            echo "Nightly release detected. Stopping workflow."
            echo "CONTINUE=false" >> "$GITHUB_OUTPUT"
          else
            echo "CONTINUE=true" >> "$GITHUB_OUTPUT"
          fi

      - name: 'Generate Changelog with Gemini'
        if: "steps.validate_version.outputs.CONTINUE == 'true'"
        uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
        with:
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          prompt: |
            Activate the 'docs-changelog' skill.

            **Release Information:**
            - New Version: ${{ steps.release_info.outputs.VERSION }}
            - Release Date: ${{ steps.release_info.outputs.TIME }}
            - Raw Changelog Data: ${{ steps.release_info.outputs.RAW_CHANGELOG }}

            Execute the release notes generation process using the information provided.

            When you are done, please output your thought process and the steps you took for future debugging purposes.

      - name: 'Create Pull Request'
        if: "steps.validate_version.outputs.CONTINUE == 'true'"
        uses: 'peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c' # ratchet:peter-evans/create-pull-request@v6
        with:
          token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          commit-message: 'docs(changelog): update for ${{ steps.release_info.outputs.VERSION }}'
          title: 'Changelog for ${{ steps.release_info.outputs.VERSION }}'
          body: |
            This PR contains the auto-generated changelog for the ${{ steps.release_info.outputs.VERSION }} release.

            Please review and merge.

            Related to #18505
          branch: 'changelog-${{ steps.release_info.outputs.VERSION }}'
          base: 'main'
          team-reviewers: 'gemini-cli-docs, gemini-cli-maintainers'
          delete-branch: true
release-patch-0-from-comment .github/workflows/release-patch-0-from-comment.yml
Triggers
issue_comment
Runs on
ubuntu-latest
Jobs
slash-command
Actions
peter-evans/slash-command-dispatch, peter-evans/create-or-update-comment, peter-evans/create-or-update-comment, peter-evans/create-or-update-comment, peter-evans/create-or-update-comment
Commands
  • gh pr view "${{ github.event.issue.number }}" --json mergeCommit,state > pr_status.json echo "MERGE_COMMIT_SHA=$(jq -r .mergeCommit.oid pr_status.json)" >> "$GITHUB_OUTPUT" echo "STATE=$(jq -r .state pr_status.json)" >> "$GITHUB_OUTPUT"
View raw YAML
name: 'Release: Patch (0) from Comment'

on:
  issue_comment:
    types: ['created']

jobs:
  slash-command:
    runs-on: 'ubuntu-latest'
    # Only run if the comment is from a human user (not automated)
    if: "github.event.comment.user.type == 'User' && github.event.comment.user.login != 'github-actions[bot]'"
    permissions:
      contents: 'write'
      pull-requests: 'write'
      actions: 'write'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          fetch-depth: 1

      - name: 'Slash Command Dispatch'
        id: 'slash_command'
        uses: 'peter-evans/slash-command-dispatch@40877f718dce0101edfc7aea2b3800cc192f9ed5'
        with:
          token: '${{ secrets.GITHUB_TOKEN }}'
          commands: 'patch'
          permission: 'write'
          issue-type: 'pull-request'

      - name: 'Get PR Status'
        id: 'pr_status'
        if: "startsWith(github.event.comment.body, '/patch')"
        env:
          GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
        run: |
          gh pr view "${{ github.event.issue.number }}" --json mergeCommit,state > pr_status.json
          echo "MERGE_COMMIT_SHA=$(jq -r .mergeCommit.oid pr_status.json)" >> "$GITHUB_OUTPUT"
          echo "STATE=$(jq -r .state pr_status.json)" >> "$GITHUB_OUTPUT"

      - name: 'Dispatch if Merged'
        if: "steps.pr_status.outputs.STATE == 'MERGED'"
        id: 'dispatch_patch'
        uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325'
        env:
          COMMENT_BODY: '${{ github.event.comment.body }}'
        with:
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          script: |
            // Parse the comment body directly to extract channel(s)
            const commentBody = process.env.COMMENT_BODY;
            console.log('Comment body:', commentBody);

            let channels = ['stable', 'preview'];  // default to both

            // Parse different formats:
            // /patch (defaults to both)
            // /patch both
            // /patch stable
            // /patch preview
            if (commentBody.trim() === '/patch' || commentBody.trim() === '/patch both') {
              channels = ['stable', 'preview'];
            } else if (commentBody.trim() === '/patch stable') {
              channels = ['stable'];
            } else if (commentBody.trim() === '/patch preview') {
              channels = ['preview'];
            } else {
              // Fallback parsing for legacy formats
              if (commentBody.includes('channel=preview')) {
                channels = ['preview'];
              } else if (commentBody.includes('--channel preview')) {
                channels = ['preview'];
              }
            }

            console.log('Detected channels:', channels);

            const dispatchedRuns = [];

            // Dispatch workflow for each channel
            for (const channel of channels) {
              console.log(`Dispatching workflow for channel: ${channel}`);

              const response = await github.rest.actions.createWorkflowDispatch({
                owner: context.repo.owner,
                repo: context.repo.repo,
                workflow_id: 'release-patch-1-create-pr.yml',
                ref: 'main',
                inputs: {
                  commit: '${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}',
                  channel: channel,
                  original_pr: '${{ github.event.issue.number }}',
                  environment: 'prod'
                }
              });

              dispatchedRuns.push({ channel, response });
            }

            // Wait a moment for the workflows to be created
            await new Promise(resolve => setTimeout(resolve, 3000));

            const runs = await github.rest.actions.listWorkflowRuns({
              owner: context.repo.owner,
              repo: context.repo.repo,
              workflow_id: 'release-patch-1-create-pr.yml',
              per_page: 20  // Increased to handle multiple runs
            });

            // Find the recent runs that match our trigger
            const recentRuns = runs.data.workflow_runs.filter(run =>
              run.event === 'workflow_dispatch' &&
              new Date(run.created_at) > new Date(Date.now() - 15000) // Within last 15 seconds
            ).slice(0, channels.length); // Limit to the number of channels we dispatched

            // Set outputs
            core.setOutput('dispatched_channels', channels.join(','));
            core.setOutput('dispatched_run_count', channels.length.toString());

            if (recentRuns.length > 0) {
              core.setOutput('dispatched_run_urls', recentRuns.map(r => r.html_url).join(','));
              core.setOutput('dispatched_run_ids', recentRuns.map(r => r.id).join(','));

              const markdownLinks = recentRuns.map(r => `- [View dispatched workflow run](${r.html_url})`).join('\n');
              core.setOutput('dispatched_run_links', markdownLinks);
            }

      - name: 'Comment on Failure'
        if: "startsWith(github.event.comment.body, '/patch') && steps.pr_status.outputs.STATE != 'MERGED'"
        uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
        with:
          token: '${{ secrets.GITHUB_TOKEN }}'
          issue-number: '${{ github.event.issue.number }}'
          body: |
            :x: The `/patch` command failed. This pull request must be merged before a patch can be created.

      - name: 'Final Status Comment - Success'
        if: "always() && startsWith(github.event.comment.body, '/patch') && steps.dispatch_patch.outcome == 'success' && steps.dispatch_patch.outputs.dispatched_run_urls"
        uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
        with:
          token: '${{ secrets.GITHUB_TOKEN }}'
          issue-number: '${{ github.event.issue.number }}'
          body: |
            🚀 **[Step 1/4] Patch workflow(s) waiting for approval!**

            **📋 Details:**
            - **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
            - **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
            - **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}

            **⏳ Status:** The patch creation workflow has been triggered and is waiting for deployment approval. Please visit the specific workflow links below and approve the runs.

            **🔗 Track Progress:**
            ${{ steps.dispatch_patch.outputs.dispatched_run_links }}
            - [View patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
            - [This trigger workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})

      - name: 'Final Status Comment - Dispatch Success (No URL)'
        if: "always() && startsWith(github.event.comment.body, '/patch') && steps.dispatch_patch.outcome == 'success' && !steps.dispatch_patch.outputs.dispatched_run_urls"
        uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
        with:
          token: '${{ secrets.GITHUB_TOKEN }}'
          issue-number: '${{ github.event.issue.number }}'
          body: |
            🚀 **[Step 1/4] Patch workflow(s) waiting for approval!**

            **📋 Details:**
            - **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
            - **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
            - **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}

            **⏳ Status:** The patch creation workflow has been triggered and is waiting for deployment approval. Please visit the workflow history link below and approve the runs.

            **🔗 Track Progress:**
            - [View patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
            - [This trigger workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})

      - name: 'Final Status Comment - Failure'
        if: "always() && startsWith(github.event.comment.body, '/patch') && (steps.dispatch_patch.outcome == 'failure' || steps.dispatch_patch.outcome == 'cancelled')"
        uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d'
        with:
          token: '${{ secrets.GITHUB_TOKEN }}'
          issue-number: '${{ github.event.issue.number }}'
          body: |
            ❌ **[Step 1/4] Patch workflow dispatch failed!**

            There was an error dispatching the patch creation workflow.

            **🔍 Troubleshooting:**
            - Check that the PR is properly merged
            - Verify workflow permissions
            - Review error logs in the workflow run

            **🔗 Debug Links:**
            - [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
            - [Patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
release-patch-1-create-pr .github/workflows/release-patch-1-create-pr.yml
Triggers
workflow_dispatch
Runs on
ubuntu-latest
Jobs
create-patch
Commands
  • npm ci
  • git config user.name "gemini-cli-robot" git config user.email "gemini-cli-robot@google.com" # Configure git to use GITHUB_TOKEN for remote operations (has actions:write for workflow files) git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${REPOSITORY}.git"
  • # Capture output and display it in logs using tee { node scripts/releasing/create-patch-pr.js \ --cli-package-name="${CLI_PACKAGE_NAME}" \ --commit="${PATCH_COMMIT}" \ --channel="${PATCH_CHANNEL}" \ --pullRequestNumber="${ORIGINAL_PR}" \ --dry-run="${DRY_RUN}" } 2>&1 | tee >( echo "LOG_CONTENT<<EOF" >> "$GITHUB_ENV" cat >> "$GITHUB_ENV" echo "EOF" >> "$GITHUB_ENV" ) echo "EXIT_CODE=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
  • git checkout "${TARGET_REF}" node scripts/releasing/patch-create-comment.js
  • echo "Patch creation failed with exit code: ${EXIT_CODE}" echo "Check the logs above and the comment posted to the original PR for details." exit 1
View raw YAML
name: 'Release: Patch (1) Create PR'

run-name: >-
  Release Patch (1) Create PR | S:${{ inputs.channel }} | C:${{ inputs.commit }} ${{ inputs.original_pr && format('| PR:#{0}', inputs.original_pr) || '' }}

on:
  workflow_dispatch:
    inputs:
      commit:
        description: 'The commit SHA to cherry-pick for the patch.'
        required: true
        type: 'string'
      channel:
        description: 'The release channel to patch.'
        required: true
        type: 'choice'
        options:
          - 'stable'
          - 'preview'
      dry_run:
        description: 'Whether to run in dry-run mode.'
        required: false
        type: 'boolean'
        default: false
      ref:
        description: 'The branch, tag, or SHA to test from.'
        required: false
        type: 'string'
        default: 'main'
      original_pr:
        description: 'The original PR number to comment back on.'
        required: false
        type: 'string'
      environment:
        description: 'Environment'
        required: false
        type: 'choice'
        options:
          - 'prod'
          - 'dev'
        default: 'prod'

jobs:
  create-patch:
    runs-on: 'ubuntu-latest'
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    permissions:
      contents: 'write'
      pull-requests: 'write'
      actions: 'write'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
        with:
          ref: '${{ github.event.inputs.ref }}'
          fetch-depth: 0

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'configure .npmrc'
        uses: './.github/actions/setup-npmrc'
        with:
          github-token: '${{ secrets.GITHUB_TOKEN }}'

      - name: 'Install Script Dependencies'
        run: 'npm ci'

      - name: 'Configure Git User'
        env:
          GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          REPOSITORY: '${{ github.repository }}'
        run: |-
          git config user.name "gemini-cli-robot"
          git config user.email "gemini-cli-robot@google.com"
          # Configure git to use GITHUB_TOKEN for remote operations (has actions:write for workflow files)
          git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${REPOSITORY}.git"

      - name: 'Create Patch'
        id: 'create_patch'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
          PATCH_COMMIT: '${{ github.event.inputs.commit }}'
          PATCH_CHANNEL: '${{ github.event.inputs.channel }}'
          ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
          DRY_RUN: '${{ github.event.inputs.dry_run }}'
        continue-on-error: true
        run: |
          # Capture output and display it in logs using tee
          {
            node scripts/releasing/create-patch-pr.js \
              --cli-package-name="${CLI_PACKAGE_NAME}" \
              --commit="${PATCH_COMMIT}" \
              --channel="${PATCH_CHANNEL}" \
              --pullRequestNumber="${ORIGINAL_PR}" \
              --dry-run="${DRY_RUN}"
          } 2>&1 | tee >(
            echo "LOG_CONTENT<<EOF" >> "$GITHUB_ENV"
            cat >> "$GITHUB_ENV"
            echo "EOF" >> "$GITHUB_ENV"
          )
          echo "EXIT_CODE=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"

      - name: 'Comment on Original PR'
        if: 'always() && inputs.original_pr'
        env:
          GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
          EXIT_CODE: '${{ steps.create_patch.outputs.EXIT_CODE }}'
          COMMIT: '${{ github.event.inputs.commit }}'
          CHANNEL: '${{ github.event.inputs.channel }}'
          REPOSITORY: '${{ github.repository }}'
          GITHUB_RUN_ID: '${{ github.run_id }}'
          LOG_CONTENT: '${{ env.LOG_CONTENT }}'
          TARGET_REF: '${{ github.event.inputs.ref }}'
          ENVIRONMENT: '${{ github.event.inputs.environment }}'
        continue-on-error: true
        run: |
          git checkout "${TARGET_REF}"
          node scripts/releasing/patch-create-comment.js

      - name: 'Fail Workflow if Main Task Failed'
        if: 'always() && steps.create_patch.outputs.EXIT_CODE != 0'
        env:
          EXIT_CODE: '${{ steps.create_patch.outputs.EXIT_CODE }}'
        run: |
          echo "Patch creation failed with exit code: ${EXIT_CODE}"
          echo "Check the logs above and the comment posted to the original PR for details."
          exit 1
release-patch-2-trigger .github/workflows/release-patch-2-trigger.yml
Triggers
pull_request, workflow_dispatch
Runs on
ubuntu-latest
Jobs
trigger-patch-release
Commands
  • npm ci
  • node scripts/releasing/patch-trigger.js --dry-run="${DRY_RUN}"
View raw YAML
name: 'Release: Patch (2) Trigger'

run-name: >-
  Release Patch (2) Trigger |
  ${{ github.event.pull_request.number && format('PR #{0}', github.event.pull_request.number) || 'Manual' }} |
  ${{ github.event.pull_request.head.ref || github.event.inputs.ref }}

on:
  pull_request:
    types:
      - 'closed'
    branches:
      - 'release/**'
  workflow_dispatch:
    inputs:
      ref:
        description: 'The head ref of the merged hotfix PR to trigger the release for (e.g. hotfix/v1.2.3/cherry-pick-abc).'
        required: true
        type: 'string'
      workflow_ref:
        description: 'The ref to checkout the workflow code from.'
        required: false
        type: 'string'
        default: 'main'
      workflow_id:
        description: 'The workflow to trigger. Defaults to release-patch-3-release.yml'
        required: false
        type: 'string'
        default: 'release-patch-3-release.yml'
      dry_run:
        description: 'Whether this is a dry run.'
        required: false
        type: 'boolean'
        default: false
      force_skip_tests:
        description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
        required: false
        type: 'boolean'
        default: false
      test_mode:
        description: 'Whether or not to run in test mode'
        required: false
        type: 'boolean'
        default: false
      environment:
        description: 'Environment'
        required: false
        type: 'choice'
        options:
          - 'prod'
          - 'dev'
        default: 'prod'

jobs:
  trigger-patch-release:
    if: "(github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'hotfix/')) || github.event_name == 'workflow_dispatch'"
    runs-on: 'ubuntu-latest'
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    permissions:
      actions: 'write'
      contents: 'write'
      pull-requests: 'write'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: "${{ github.event.inputs.workflow_ref || 'main' }}"
          fetch-depth: 1

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'Install Dependencies'
        run: 'npm ci'

      - name: 'Trigger Patch Release'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          HEAD_REF: "${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.event.inputs.ref }}"
          PR_BODY: "${{ github.event_name == 'pull_request' && github.event.pull_request.body || '' }}"
          WORKFLOW_ID: '${{ github.event.inputs.workflow_id }}'
          GITHUB_REPOSITORY_OWNER: '${{ github.repository_owner }}'
          GITHUB_REPOSITORY_NAME: '${{ github.event.repository.name }}'
          GITHUB_EVENT_NAME: '${{ github.event_name }}'
          GITHUB_EVENT_PAYLOAD: '${{ toJSON(github.event) }}'
          FORCE_SKIP_TESTS: '${{ github.event.inputs.force_skip_tests }}'
          TEST_MODE: '${{ github.event.inputs.test_mode }}'
          ENVIRONMENT: "${{ github.event.inputs.environment || 'prod' }}"
          DRY_RUN: '${{ github.event.inputs.dry_run }}'
        run: |
          node scripts/releasing/patch-trigger.js --dry-run="${DRY_RUN}"
release-patch-3-release .github/workflows/release-patch-3-release.yml
Triggers
workflow_dispatch
Runs on
ubuntu-latest
Jobs
release
Commands
  • npm ci
  • npm ci
  • echo "$JSON_INPUTS"
  • # Use the existing get-release-version.js script to calculate patch version # Run from main checkout which has full git history and access to npm PATCH_JSON=$(node scripts/get-release-version.js --type=patch --cli-package-name="${CLI_PACKAGE_NAME}" --patch-from="${PATCH_FROM}") echo "Patch version calculation result: ${PATCH_JSON}" RELEASE_VERSION=$(echo "${PATCH_JSON}" | jq -r .releaseVersion) RELEASE_TAG=$(echo "${PATCH_JSON}" | jq -r .releaseTag) NPM_TAG=$(echo "${PATCH_JSON}" | jq -r .npmTag) PREVIOUS_TAG=$(echo "${PATCH_JSON}" | jq -r .previousReleaseTag) echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_OUTPUT}" echo "RELEASE_TAG=${RELEASE_TAG}" >> "${GITHUB_OUTPUT}" echo "NPM_TAG=${NPM_TAG}" >> "${GITHUB_OUTPUT}" echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "${GITHUB_OUTPUT}"
  • echo "🔍 Verifying no concurrent patch releases have occurred..." # Store original calculation for comparison echo "Original calculation:" echo " Release version: ${ORIGINAL_RELEASE_VERSION}" echo " Release tag: ${ORIGINAL_RELEASE_TAG}" echo " Previous tag: ${ORIGINAL_PREVIOUS_TAG}" # Re-run the same version calculation script echo "Re-calculating version to check for changes..." CURRENT_PATCH_JSON=$(node scripts/get-release-version.js --cli-package-name="${VARS_CLI_PACKAGE_NAME}" --type=patch --patch-from="${CHANNEL}") CURRENT_RELEASE_VERSION=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseVersion) CURRENT_RELEASE_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseTag) CURRENT_PREVIOUS_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .previousReleaseTag) echo "Current calculation:" echo " Release version: ${CURRENT_RELEASE_VERSION}" echo " Release tag: ${CURRENT_RELEASE_TAG}" echo " Previous tag: ${CURRENT_PREVIOUS_TAG}" # Compare calculations if [[ "${ORIGINAL_RELEASE_VERSION}" != "${CURRENT_RELEASE_VERSION}" ]] || \ [[ "${ORIGINAL_RELEASE_TAG}" != "${CURRENT_RELEASE_TAG}" ]] || \ [[ "${ORIGINAL_PREVIOUS_TAG}" != "${CURRENT_PREVIOUS_TAG}" ]]; then echo "❌ RACE CONDITION DETECTED: Version calculations have changed!" echo "This indicates another patch release completed while this one was in progress." echo "" echo "Originally planned: ${ORIGINAL_RELEASE_VERSION} (from ${ORIGINAL_PREVIOUS_TAG})" echo "Should now build: ${CURRENT_RELEASE_VERSION} (from ${CURRENT_PREVIOUS_TAG})" echo "" echo "# Setting outputs for failure comment" echo "CURRENT_RELEASE_VERSION=${CURRENT_RELEASE_VERSION}" >> "${GITHUB_ENV}" echo "CURRENT_RELEASE_TAG=${CURRENT_RELEASE_TAG}" >> "${GITHUB_ENV}" echo "CURRENT_PREVIOUS_TAG=${CURRENT_PREVIOUS_TAG}" >> "${GITHUB_ENV}" echo "The patch release must be restarted to use the correct version numbers." exit 1 fi echo "✅ Version calculations unchanged - proceeding with release"
  • echo "Patch Release Summary:" echo " Release Version: ${STEPS_PATCH_VERSION_OUTPUTS_RELEASE_VERSION}" echo " Release Tag: ${STEPS_PATCH_VERSION_OUTPUTS_RELEASE_TAG}" echo " NPM Tag: ${STEPS_PATCH_VERSION_OUTPUTS_NPM_TAG}" echo " Previous Tag: ${STEPS_PATCH_VERSION_OUTPUTS_PREVIOUS_TAG}"
  • gh issue create \ --title 'Patch Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \ --body 'The patch-release workflow failed. See the full run for details: ${DETAILS_URL}' \ --label 'release-failure,priority/p0'
  • node scripts/releasing/patch-comment.js
View raw YAML
name: 'Release: Patch (3) Release'

run-name: >-
  Release Patch (3) Release | T:${{ inputs.type }} | R:${{ inputs.release_ref }} ${{ inputs.original_pr && format('| PR:#{0}', inputs.original_pr) || '' }}

on:
  workflow_dispatch:
    inputs:
      type:
        description: 'The type of release to perform.'
        required: true
        type: 'choice'
        options:
          - 'stable'
          - 'preview'
      dry_run:
        description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
        required: true
        type: 'boolean'
        default: true
      force_skip_tests:
        description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
        required: false
        type: 'boolean'
        default: false
      release_ref:
        description: 'The branch, tag, or SHA to release from.'
        required: true
        type: 'string'
      original_pr:
        description: 'The original PR number to comment back on.'
        required: false
        type: 'string'
      environment:
        description: 'Environment'
        required: false
        type: 'choice'
        options:
          - 'prod'
          - 'dev'
        default: 'prod'

jobs:
  release:
    runs-on: 'ubuntu-latest'
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    permissions:
      contents: 'write'
      packages: 'write'
      pull-requests: 'write'
      issues: 'write'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          fetch-depth: 0
          fetch-tags: true

      - name: 'Checkout Release Code'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ github.event.inputs.release_ref }}'
          path: 'release'
          fetch-depth: 0

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'configure .npmrc'
        uses: './.github/actions/setup-npmrc'
        with:
          github-token: '${{ secrets.GITHUB_TOKEN }}'

      - name: 'Install Script Dependencies'
        run: |-
          npm ci

      - name: 'Install Dependencies'
        working-directory: './release'
        run: |-
          npm ci

      - name: 'Print Inputs'
        shell: 'bash'
        env:
          JSON_INPUTS: '${{ toJSON(inputs) }}'
        run: 'echo "$JSON_INPUTS"'

      - name: 'Get Patch Version'
        id: 'patch_version'
        env:
          GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          PATCH_FROM: '${{ github.event.inputs.type }}'
          CLI_PACKAGE_NAME: '${{vars.CLI_PACKAGE_NAME}}'
        run: |
          # Use the existing get-release-version.js script to calculate patch version
          # Run from main checkout which has full git history and access to npm
          PATCH_JSON=$(node scripts/get-release-version.js --type=patch --cli-package-name="${CLI_PACKAGE_NAME}" --patch-from="${PATCH_FROM}")
          echo "Patch version calculation result: ${PATCH_JSON}"

          RELEASE_VERSION=$(echo "${PATCH_JSON}" | jq -r .releaseVersion)
          RELEASE_TAG=$(echo "${PATCH_JSON}" | jq -r .releaseTag)
          NPM_TAG=$(echo "${PATCH_JSON}" | jq -r .npmTag)
          PREVIOUS_TAG=$(echo "${PATCH_JSON}" | jq -r .previousReleaseTag)

          echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_OUTPUT}"
          echo "RELEASE_TAG=${RELEASE_TAG}" >> "${GITHUB_OUTPUT}"
          echo "NPM_TAG=${NPM_TAG}" >> "${GITHUB_OUTPUT}"
          echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "${GITHUB_OUTPUT}"

      - name: 'Verify Version Consistency'
        env:
          GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          CHANNEL: '${{ github.event.inputs.type }}'
          ORIGINAL_RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
          ORIGINAL_RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
          ORIGINAL_PREVIOUS_TAG: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
          VARS_CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
        run: |
          echo "🔍 Verifying no concurrent patch releases have occurred..."

          # Store original calculation for comparison
          echo "Original calculation:"
          echo "  Release version: ${ORIGINAL_RELEASE_VERSION}"
          echo "  Release tag: ${ORIGINAL_RELEASE_TAG}"
          echo "  Previous tag: ${ORIGINAL_PREVIOUS_TAG}"

          # Re-run the same version calculation script
          echo "Re-calculating version to check for changes..."
          CURRENT_PATCH_JSON=$(node scripts/get-release-version.js --cli-package-name="${VARS_CLI_PACKAGE_NAME}" --type=patch --patch-from="${CHANNEL}")
          CURRENT_RELEASE_VERSION=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseVersion)
          CURRENT_RELEASE_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseTag)
          CURRENT_PREVIOUS_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .previousReleaseTag)

          echo "Current calculation:"
          echo "  Release version: ${CURRENT_RELEASE_VERSION}"
          echo "  Release tag: ${CURRENT_RELEASE_TAG}"
          echo "  Previous tag: ${CURRENT_PREVIOUS_TAG}"

          # Compare calculations
          if [[ "${ORIGINAL_RELEASE_VERSION}" != "${CURRENT_RELEASE_VERSION}" ]] || \
             [[ "${ORIGINAL_RELEASE_TAG}" != "${CURRENT_RELEASE_TAG}" ]] || \
             [[ "${ORIGINAL_PREVIOUS_TAG}" != "${CURRENT_PREVIOUS_TAG}" ]]; then
            echo "❌ RACE CONDITION DETECTED: Version calculations have changed!"
            echo "This indicates another patch release completed while this one was in progress."
            echo ""
            echo "Originally planned:  ${ORIGINAL_RELEASE_VERSION} (from ${ORIGINAL_PREVIOUS_TAG})"
            echo "Should now build:    ${CURRENT_RELEASE_VERSION} (from ${CURRENT_PREVIOUS_TAG})"
            echo ""
            echo "# Setting outputs for failure comment"
            echo "CURRENT_RELEASE_VERSION=${CURRENT_RELEASE_VERSION}" >> "${GITHUB_ENV}"
            echo "CURRENT_RELEASE_TAG=${CURRENT_RELEASE_TAG}" >> "${GITHUB_ENV}"
            echo "CURRENT_PREVIOUS_TAG=${CURRENT_PREVIOUS_TAG}" >> "${GITHUB_ENV}"
            echo "The patch release must be restarted to use the correct version numbers."
            exit 1
          fi

          echo "✅ Version calculations unchanged - proceeding with release"

      - name: 'Print Calculated Version'
        run: |-
          echo "Patch Release Summary:"
          echo "  Release Version: ${STEPS_PATCH_VERSION_OUTPUTS_RELEASE_VERSION}"
          echo "  Release Tag: ${STEPS_PATCH_VERSION_OUTPUTS_RELEASE_TAG}"
          echo "  NPM Tag: ${STEPS_PATCH_VERSION_OUTPUTS_NPM_TAG}"
          echo "  Previous Tag: ${STEPS_PATCH_VERSION_OUTPUTS_PREVIOUS_TAG}"
        env:
          STEPS_PATCH_VERSION_OUTPUTS_RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
          STEPS_PATCH_VERSION_OUTPUTS_RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
          STEPS_PATCH_VERSION_OUTPUTS_NPM_TAG: '${{ steps.patch_version.outputs.NPM_TAG }}'
          STEPS_PATCH_VERSION_OUTPUTS_PREVIOUS_TAG: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'

      - name: 'Run Tests'
        if: "${{github.event.inputs.force_skip_tests != 'true'}}"
        uses: './.github/actions/run-tests'
        with:
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          working-directory: './release'

      - name: 'Publish Release'
        uses: './.github/actions/publish-release'
        with:
          release-version: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
          release-tag: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
          npm-tag: '${{ steps.patch_version.outputs.NPM_TAG }}'
          wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
          wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
          wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          dry-run: '${{ github.event.inputs.dry_run }}'
          previous-tag: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          npm-registry-publish-url: '${{ vars.NPM_REGISTRY_PUBLISH_URL }}'
          npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
          npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
          cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
          core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
          a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'
          working-directory: './release'

      - name: 'Create Issue on Failure'
        if: '${{ failure() && github.event.inputs.dry_run == false }}'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
          DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
        run: |
          gh issue create \
            --title 'Patch Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
            --body 'The patch-release workflow failed. See the full run for details: ${DETAILS_URL}' \
            --label 'release-failure,priority/p0'

      - name: 'Comment Success on Original PR'
        if: '${{ success() && github.event.inputs.original_pr }}'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
          SUCCESS: 'true'
          RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
          RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
          NPM_TAG: '${{ steps.patch_version.outputs.NPM_TAG }}'
          CHANNEL: '${{ github.event.inputs.type }}'
          DRY_RUN: '${{ github.event.inputs.dry_run }}'
          GITHUB_RUN_ID: '${{ github.run_id }}'
          GITHUB_REPOSITORY_OWNER: '${{ github.repository_owner }}'
          GITHUB_REPOSITORY_NAME: '${{ github.event.repository.name }}'
        run: |
          node scripts/releasing/patch-comment.js

      - name: 'Comment Failure on Original PR'
        if: '${{ failure() && github.event.inputs.original_pr }}'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          ORIGINAL_PR: '${{ github.event.inputs.original_pr }}'
          SUCCESS: 'false'
          RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
          RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
          NPM_TAG: '${{ steps.patch_version.outputs.NPM_TAG }}'
          CHANNEL: '${{ github.event.inputs.type }}'
          DRY_RUN: '${{ github.event.inputs.dry_run }}'
          GITHUB_RUN_ID: '${{ github.run_id }}'
          GITHUB_REPOSITORY_OWNER: '${{ github.repository_owner }}'
          GITHUB_REPOSITORY_NAME: '${{ github.event.repository.name }}'
          # Pass current version info for race condition failures
          CURRENT_RELEASE_VERSION: '${{ env.CURRENT_RELEASE_VERSION }}'
          CURRENT_RELEASE_TAG: '${{ env.CURRENT_RELEASE_TAG }}'
          CURRENT_PREVIOUS_TAG: '${{ env.CURRENT_PREVIOUS_TAG }}'
        run: |
          # Check if this was a version consistency failure
          if [[ -n "${CURRENT_RELEASE_VERSION}" ]]; then
            echo "Detected version race condition failure - posting specific comment with current version info"
            export RACE_CONDITION_FAILURE=true
          fi
          node scripts/releasing/patch-comment.js
release-promote matrix .github/workflows/release-promote.yml
Triggers
workflow_dispatch
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
calculate-versions, test, publish-preview, publish-stable, nightly-pr
Matrix
include, include.channel, include.sha→ ${{ github.event.inputs.ref }}, ${{ needs.calculate-versions.outputs.PREVIEW_SHA }}, ${{ needs.calculate-versions.outputs.STABLE_SHA }}, nightly, preview, stable
Commands
  • npm ci
  • echo "$JSON_INPUTS"
  • set -e STABLE_COMMAND="node scripts/get-release-version.js --type=stable" if [[ -n "${STABLE_OVERRIDE}" ]]; then STABLE_COMMAND+=" --stable_version_override=${STABLE_OVERRIDE}" fi PREVIEW_COMMAND="node scripts/get-release-version.js --type=preview" if [[ -n "${PREVIEW_OVERRIDE}" ]]; then PREVIEW_COMMAND+=" --preview_version_override=${PREVIEW_OVERRIDE}" fi NIGHTLY_COMMAND="node scripts/get-release-version.js --type=promote-nightly" STABLE_JSON=$(${STABLE_COMMAND}) STABLE_VERSION=$(echo "${STABLE_JSON}" | jq -r .releaseVersion) PREVIEW_COMMAND+=" --stable-base-version=${STABLE_VERSION}" NIGHTLY_COMMAND+=" --stable-base-version=${STABLE_VERSION}" PREVIEW_JSON=$(${PREVIEW_COMMAND}) NIGHTLY_JSON=$(${NIGHTLY_COMMAND}) echo "STABLE_JSON_COMMAND=${STABLE_COMMAND}" echo "PREVIEW_JSON_COMMAND=${PREVIEW_COMMAND}" echo "NIGHTLY_JSON_COMMAND=${NIGHTLY_COMMAND}" echo "STABLE_JSON: ${STABLE_JSON}" echo "PREVIEW_JSON: ${PREVIEW_JSON}" echo "NIGHTLY_JSON: ${NIGHTLY_JSON}" echo "STABLE_VERSION=${STABLE_VERSION}" >> "${GITHUB_OUTPUT}" # shellcheck disable=SC1083 echo "STABLE_SHA=$(git rev-parse "$(echo "${PREVIEW_JSON}" | jq -r .previousReleaseTag)"^{commit})" >> "${GITHUB_OUTPUT}" echo "PREVIOUS_STABLE_TAG=$(echo "${STABLE_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}" echo "PREVIEW_VERSION=$(echo "${PREVIEW_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}" # shellcheck disable=SC1083 REF="${REF_INPUT}" SHA=$(git ls-remote origin "$REF" | awk -v ref="$REF" '$2 == "refs/heads/"ref || $2 == "refs/tags/"ref || $2 == ref {print $1}' | head -n 1) if [ -z "$SHA" ]; then if [[ "$REF" =~ ^[0-9a-f]{7,40}$ ]]; then SHA="$REF" else echo "::error::Could not resolve ref '$REF' to a commit SHA." exit 1 fi fi echo "PREVIEW_SHA=$SHA" >> "${GITHUB_OUTPUT}" echo "PREVIOUS_PREVIEW_TAG=$(echo "${PREVIEW_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}" echo "NEXT_NIGHTLY_VERSION=$(echo "${NIGHTLY_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}" echo "PREVIOUS_NIGHTLY_TAG=$(echo "${NIGHTLY_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}" CURRENT_NIGHTLY_TAG=$(git describe --tags --abbrev=0 --match="*nightly*") echo "CURRENT_NIGHTLY_TAG=${CURRENT_NIGHTLY_TAG}" >> "${GITHUB_OUTPUT}" echo "NEXT_SHA=$SHA" >> "${GITHUB_OUTPUT}"
  • echo "Release Plan:" echo "-----------" echo "Stable Release: ${STABLE_VERSION}" echo " - Commit: ${STABLE_SHA}" echo " - Previous Tag: ${PREVIOUS_STABLE_TAG}" echo "" echo "Preview Release: ${PREVIEW_VERSION}" echo " - Commit: ${PREVIEW_SHA} (${INPUT_REF})" echo " - Previous Tag: ${PREVIOUS_PREVIEW_TAG}" echo "" echo "Preparing Next Nightly Release: ${NEXT_NIGHTLY_VERSION}" echo " - Merging Version Update PR to Branch: ${INPUT_REF}" echo " - Previous Tag: ${PREVIOUS_NIGHTLY_TAG}"
  • npm ci
  • npm ci
  • gh issue create \ --title 'Promote Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \ --body 'The promote-release workflow failed during preview publish. See the full run for details: ${DETAILS_URL}' \ --label 'release-failure,priority/p0'
  • npm ci
View raw YAML
name: 'Release: Promote'

on:
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
        required: true
        type: 'boolean'
        default: true
      force_skip_tests:
        description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
        required: false
        type: 'boolean'
        default: false
      ref:
        description: 'The branch, tag, or SHA to release from.'
        required: false
        type: 'string'
        default: 'main'
      stable_version_override:
        description: 'Manually override the stable version number.'
        required: false
        type: 'string'
      preview_version_override:
        description: 'Manually override the preview version number.'
        required: false
        type: 'string'
      environment:
        description: 'Environment'
        required: false
        type: 'choice'
        options:
          - 'prod'
          - 'dev'
        default: 'prod'

jobs:
  calculate-versions:
    name: 'Calculate Versions and Plan'
    runs-on: 'ubuntu-latest'
    environment: "${{ github.event.inputs.environment || 'prod' }}"

    outputs:
      STABLE_VERSION: '${{ steps.versions.outputs.STABLE_VERSION }}'
      STABLE_SHA: '${{ steps.versions.outputs.STABLE_SHA }}'
      PREVIOUS_STABLE_TAG: '${{ steps.versions.outputs.PREVIOUS_STABLE_TAG }}'
      PREVIEW_VERSION: '${{ steps.versions.outputs.PREVIEW_VERSION }}'
      PREVIEW_SHA: '${{ steps.versions.outputs.PREVIEW_SHA }}'
      PREVIOUS_PREVIEW_TAG: '${{ steps.versions.outputs.PREVIOUS_PREVIEW_TAG }}'
      NEXT_NIGHTLY_VERSION: '${{ steps.versions.outputs.NEXT_NIGHTLY_VERSION }}'
      PREVIOUS_NIGHTLY_TAG: '${{ steps.versions.outputs.PREVIOUS_NIGHTLY_TAG }}'

    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          fetch-depth: 0
          fetch-tags: true

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'Install Dependencies'
        run: 'npm ci'

      - name: 'Print Inputs'
        shell: 'bash'
        env:
          JSON_INPUTS: '${{ toJSON(inputs) }}'
        run: 'echo "$JSON_INPUTS"'

      - name: 'Calculate Versions and SHAs'
        id: 'versions'
        env:
          GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          STABLE_OVERRIDE: '${{ github.event.inputs.stable_version_override }}'
          PREVIEW_OVERRIDE: '${{ github.event.inputs.preview_version_override }}'
          REF_INPUT: '${{ github.event.inputs.ref }}'
        run: |
          set -e
          STABLE_COMMAND="node scripts/get-release-version.js --type=stable"
          if [[ -n "${STABLE_OVERRIDE}" ]]; then
            STABLE_COMMAND+=" --stable_version_override=${STABLE_OVERRIDE}"
          fi
          PREVIEW_COMMAND="node scripts/get-release-version.js --type=preview"
          if [[ -n "${PREVIEW_OVERRIDE}" ]]; then
            PREVIEW_COMMAND+=" --preview_version_override=${PREVIEW_OVERRIDE}"
          fi
          NIGHTLY_COMMAND="node scripts/get-release-version.js --type=promote-nightly"
          STABLE_JSON=$(${STABLE_COMMAND})
          STABLE_VERSION=$(echo "${STABLE_JSON}" | jq -r .releaseVersion)
          PREVIEW_COMMAND+=" --stable-base-version=${STABLE_VERSION}"
          NIGHTLY_COMMAND+=" --stable-base-version=${STABLE_VERSION}"
          PREVIEW_JSON=$(${PREVIEW_COMMAND})
          NIGHTLY_JSON=$(${NIGHTLY_COMMAND})
          echo "STABLE_JSON_COMMAND=${STABLE_COMMAND}"
          echo "PREVIEW_JSON_COMMAND=${PREVIEW_COMMAND}"
          echo "NIGHTLY_JSON_COMMAND=${NIGHTLY_COMMAND}"
          echo "STABLE_JSON: ${STABLE_JSON}"
          echo "PREVIEW_JSON: ${PREVIEW_JSON}"
          echo "NIGHTLY_JSON: ${NIGHTLY_JSON}"
          echo "STABLE_VERSION=${STABLE_VERSION}" >> "${GITHUB_OUTPUT}"
          # shellcheck disable=SC1083
          echo "STABLE_SHA=$(git rev-parse "$(echo "${PREVIEW_JSON}" | jq -r .previousReleaseTag)"^{commit})" >> "${GITHUB_OUTPUT}"
          echo "PREVIOUS_STABLE_TAG=$(echo "${STABLE_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
          echo "PREVIEW_VERSION=$(echo "${PREVIEW_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
          # shellcheck disable=SC1083
          REF="${REF_INPUT}"
          SHA=$(git ls-remote origin "$REF" | awk -v ref="$REF" '$2 == "refs/heads/"ref || $2 == "refs/tags/"ref || $2 == ref {print $1}' | head -n 1)
          if [ -z "$SHA" ]; then
            if [[ "$REF" =~ ^[0-9a-f]{7,40}$ ]]; then
              SHA="$REF"
            else
              echo "::error::Could not resolve ref '$REF' to a commit SHA."
              exit 1
            fi
          fi
          echo "PREVIEW_SHA=$SHA" >> "${GITHUB_OUTPUT}"
          echo "PREVIOUS_PREVIEW_TAG=$(echo "${PREVIEW_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
          echo "NEXT_NIGHTLY_VERSION=$(echo "${NIGHTLY_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
          echo "PREVIOUS_NIGHTLY_TAG=$(echo "${NIGHTLY_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
          CURRENT_NIGHTLY_TAG=$(git describe --tags --abbrev=0 --match="*nightly*")
          echo "CURRENT_NIGHTLY_TAG=${CURRENT_NIGHTLY_TAG}" >> "${GITHUB_OUTPUT}"
          echo "NEXT_SHA=$SHA" >> "${GITHUB_OUTPUT}"

      - name: 'Display Pending Updates'
        env:
          STABLE_VERSION: '${{ steps.versions.outputs.STABLE_VERSION }}'
          STABLE_SHA: '${{ steps.versions.outputs.STABLE_SHA }}'
          PREVIOUS_STABLE_TAG: '${{ steps.versions.outputs.PREVIOUS_STABLE_TAG }}'
          PREVIEW_VERSION: '${{ steps.versions.outputs.PREVIEW_VERSION }}'
          PREVIEW_SHA: '${{ steps.versions.outputs.PREVIEW_SHA }}'
          PREVIOUS_PREVIEW_TAG: '${{ steps.versions.outputs.PREVIOUS_PREVIEW_TAG }}'
          NEXT_NIGHTLY_VERSION: '${{ steps.versions.outputs.NEXT_NIGHTLY_VERSION }}'
          PREVIOUS_NIGHTLY_TAG: '${{ steps.versions.outputs.PREVIOUS_NIGHTLY_TAG }}'
          INPUT_REF: '${{ github.event.inputs.ref }}'
        run: |
          echo "Release Plan:"
          echo "-----------"
          echo "Stable Release: ${STABLE_VERSION}"
          echo "  - Commit: ${STABLE_SHA}"
          echo "  - Previous Tag: ${PREVIOUS_STABLE_TAG}"
          echo ""
          echo "Preview Release: ${PREVIEW_VERSION}"
          echo "  - Commit: ${PREVIEW_SHA} (${INPUT_REF})"
          echo "  - Previous Tag: ${PREVIOUS_PREVIEW_TAG}"
          echo ""
          echo "Preparing Next Nightly Release: ${NEXT_NIGHTLY_VERSION}"
          echo "  - Merging Version Update PR to Branch: ${INPUT_REF}"
          echo "  - Previous Tag: ${PREVIOUS_NIGHTLY_TAG}"

  test:
    name: 'Test ${{ matrix.channel }}'
    needs: 'calculate-versions'
    runs-on: 'ubuntu-latest'
    strategy:
      fail-fast: false
      matrix:
        include:
          - channel: 'stable'
            sha: '${{ needs.calculate-versions.outputs.STABLE_SHA }}'
          - channel: 'preview'
            sha: '${{ needs.calculate-versions.outputs.PREVIEW_SHA }}'
          - channel: 'nightly'
            sha: '${{ github.event.inputs.ref }}'
    steps:
      - name: 'Checkout Ref'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ github.event.inputs.ref }}'

      - name: 'Checkout correct SHA'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ matrix.sha }}'
          path: 'release'
          fetch-depth: 0

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'Install Dependencies'
        working-directory: './release'
        run: 'npm ci'

      - name: 'Run Tests'
        if: "${{github.event.inputs.force_skip_tests != 'true'}}"
        uses: './.github/actions/run-tests'
        with:
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          working-directory: './release'

  publish-preview:
    name: 'Publish preview'
    needs: ['calculate-versions', 'test']
    runs-on: 'ubuntu-latest'
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    permissions:
      contents: 'write'
      packages: 'write'
      issues: 'write'
    steps:
      - name: 'Checkout Ref'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ github.event.inputs.ref }}'

      - name: 'Checkout correct SHA'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ needs.calculate-versions.outputs.PREVIEW_SHA }}'
          path: 'release'
          fetch-depth: 0

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'Install Dependencies'
        working-directory: './release'
        run: 'npm ci'

      - name: 'Publish Release'
        uses: './.github/actions/publish-release'
        with:
          release-version: '${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
          release-tag: 'v${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
          npm-tag: 'preview'
          wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
          wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
          wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          dry-run: '${{ github.event.inputs.dry_run }}'
          previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_PREVIEW_TAG }}'
          working-directory: './release'
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          force-skip-tests: '${{ github.event.inputs.force_skip_tests }}'
          npm-registry-publish-url: '${{ vars.NPM_REGISTRY_PUBLISH_URL }}'
          npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
          npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
          cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
          core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
          a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'

      - name: 'Create Issue on Failure'
        if: '${{ failure() && github.event.inputs.dry_run == false }}'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          RELEASE_TAG: 'v${{ needs.calculate-versions.outputs.PREVIEW_VERSION }}'
          DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
        run: |
          gh issue create \
            --title 'Promote Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
            --body 'The promote-release workflow failed during preview publish. See the full run for details: ${DETAILS_URL}' \
            --label 'release-failure,priority/p0'

  publish-stable:
    name: 'Publish stable'
    needs: ['calculate-versions', 'test', 'publish-preview']
    runs-on: 'ubuntu-latest'
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    permissions:
      contents: 'write'
      packages: 'write'
      issues: 'write'
    steps:
      - name: 'Checkout Ref'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ github.event.inputs.ref }}'

      - name: 'Checkout correct SHA'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ needs.calculate-versions.outputs.STABLE_SHA }}'
          path: 'release'
          fetch-depth: 0

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'Install Dependencies'
        working-directory: './release'
        run: 'npm ci'

      - name: 'Publish Release'
        uses: './.github/actions/publish-release'
        with:
          release-version: '${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
          release-tag: 'v${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
          npm-tag: 'latest'
          wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
          wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
          wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          dry-run: '${{ github.event.inputs.dry_run }}'
          previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_STABLE_TAG }}'
          working-directory: './release'
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          force-skip-tests: '${{ github.event.inputs.force_skip_tests }}'
          npm-registry-publish-url: '${{ vars.NPM_REGISTRY_PUBLISH_URL }}'
          npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
          npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'
          cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
          core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
          a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'

      - name: 'Create Issue on Failure'
        if: '${{ failure() && github.event.inputs.dry_run == false }}'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          RELEASE_TAG: 'v${{ needs.calculate-versions.outputs.STABLE_VERSION }}'
          DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
        run: |
          gh issue create \
            --title 'Promote Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
            --body 'The promote-release workflow failed during stable publish. See the full run for details: ${DETAILS_URL}' \
            --label 'release-failure,priority/p0'

  nightly-pr:
    name: 'Create Nightly PR'
    needs: ['publish-stable', 'calculate-versions']
    runs-on: 'ubuntu-latest'
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    permissions:
      contents: 'write'
      pull-requests: 'write'
      issues: 'write'
    steps:
      - name: 'Checkout Ref'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ github.event.inputs.ref }}'

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
        with:
          node-version-file: '.nvmrc'
          cache: 'npm'

      - name: 'Install Dependencies'
        run: 'npm ci'

      - name: 'Configure Git User'
        run: |-
          git config user.name "gemini-cli-robot"
          git config user.email "gemini-cli-robot@google.com"

      - name: 'Create and switch to a new branch'
        id: 'release_branch'
        run: |
          BRANCH_NAME="chore/nightly-version-bump-${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"
          git switch -c "${BRANCH_NAME}"
          echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
        env:
          NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'

      - name: 'Update package versions'
        run: 'npm run release:version "${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"'
        env:
          NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'

      - name: 'Commit and Push package versions'
        env:
          BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
          DRY_RUN: '${{ github.event.inputs.dry_run }}'
          NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
        run: |-
          git add package.json packages/*/package.json
          if [ -f package-lock.json ]; then
            git add package-lock.json
          fi
          git commit -m "chore(release): bump version to ${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"
          if [[ "${DRY_RUN}" == "false" ]]; then
            echo "Pushing release branch to remote..."
            git push --set-upstream origin "${BRANCH_NAME}"
          else
            echo "Dry run enabled. Skipping push."
          fi

      - name: 'Create and Merge Pull Request'
        uses: './.github/actions/create-pull-request'
        with:
          branch-name: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
          pr-title: 'chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
          pr-body: 'Automated version bump to prepare for the next nightly release.'
          github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
          dry-run: '${{ github.event.inputs.dry_run }}'

      - name: 'Create Issue on Failure'
        if: '${{ failure() && github.event.inputs.dry_run == false }}'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          RELEASE_TAG: 'v${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
          DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
        run: |
          gh issue create \
            --title 'Promote Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')' \
            --body 'The promote-release workflow failed during nightly PR creation. See the full run for details: ${DETAILS_URL}' \
            --label 'release-failure,priority/p0'
release-rollback .github/workflows/release-rollback.yml
Triggers
workflow_dispatch
Runs on
ubuntu-latest
Jobs
change-tags
Commands
  • TAG_VALUE="v${ROLLBACK_ORIGIN}" echo "ORIGIN_TAG=$TAG_VALUE" >> "$GITHUB_OUTPUT"
  • echo "ORIGIN_HASH=$(git rev-parse "${ORIGIN_TAG}")" >> "$GITHUB_OUTPUT"
  • npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."
  • npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."
  • npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."
  • gh release delete "${ORIGIN_TAG}" --yes
  • RELEASE_TAG=$(gh release view "$TARGET_TAG" --json tagName --jq .tagName) if [ "$RELEASE_TAG" = "$TARGET_TAG" ]; then echo "❌ Failed to delete release with tag ${TARGET_TAG}" echo '❌ This means the release was not deleted, and the workflow should fail.' exit 1 fi
  • echo "ROLLBACK_TAG=$ROLLBACK_TAG_NAME" >> "$GITHUB_OUTPUT" git tag "$ROLLBACK_TAG_NAME" "${ORIGIN_HASH}" git push origin --tags
View raw YAML
name: 'Release: Rollback change'

on:
  workflow_dispatch:
    inputs:
      rollback_origin:
        description: 'The package version to rollback FROM and delete (e.g., 0.5.0-preview-2)'
        required: true
        type: 'string'
      rollback_destination:
        description: 'The package version to rollback TO (e.g., 0.5.0-preview-2). This version must already exist on the npm registry.'
        required: false
        type: 'string'
      channel:
        description: 'The npm dist-tag to apply to rollback_destination (e.g., latest, preview, nightly). REQUIRED IF rollback_destination is set.'
        required: false
        type: 'choice'
        options:
          - 'latest'
          - 'preview'
          - 'nightly'
          - 'dev'
        default: 'dev'
      ref:
        description: 'The branch, tag, or SHA to run from.'
        required: false
        type: 'string'
        default: 'main'
      dry-run:
        description: 'Whether to run in dry-run mode.'
        required: false
        type: 'boolean'
        default: true
      environment:
        description: 'Environment'
        required: false
        type: 'choice'
        options:
          - 'prod'
          - 'dev'
        default: 'prod'

jobs:
  change-tags:
    if: "github.repository == 'google-gemini/gemini-cli'"
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    runs-on: 'ubuntu-latest'
    permissions:
      packages: 'write'
      issues: 'write'
    steps:
      - name: 'Checkout repository'
        uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4
        with:
          ref: '${{ github.event.inputs.ref }}'
          fetch-depth: 0

      - name: 'Setup Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020'
        with:
          node-version-file: '.nvmrc'

      - name: 'configure .npmrc'
        uses: './.github/actions/setup-npmrc'
        with:
          github-token: '${{ secrets.GITHUB_TOKEN }}'

      - name: 'Get Origin Version Tag'
        id: 'origin_tag'
        shell: 'bash'
        env:
          ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
        run: |
          TAG_VALUE="v${ROLLBACK_ORIGIN}"
          echo "ORIGIN_TAG=$TAG_VALUE" >> "$GITHUB_OUTPUT"

      - name: 'Get Origin Commit Hash'
        id: 'origin_hash'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
        shell: 'bash'
        run: |
          echo "ORIGIN_HASH=$(git rev-parse "${ORIGIN_TAG}")" >> "$GITHUB_OUTPUT"

      - name: 'Change tag'
        if: "${{ github.event.inputs.rollback_destination != '' }}"
        uses: './.github/actions/tag-npm-release'
        with:
          channel: '${{ github.event.inputs.channel }}'
          version: '${{ github.event.inputs.rollback_destination }}'
          dry-run: '${{ github.event.inputs.dry-run }}'
          wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
          wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
          wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          cli-package-name: '${{ vars.CLI_PACKAGE_NAME }}'
          core-package-name: '${{ vars.CORE_PACKAGE_NAME }}'
          a2a-package-name: '${{ vars.A2A_PACKAGE_NAME }}'

      - name: 'Get cli Token'
        uses: './.github/actions/npm-auth-token'
        id: 'cli-token'
        with:
          package-name: '${{ vars.CLI_PACKAGE_NAME }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
          wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
          wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'

      - name: 'Deprecate Cli Npm Package'
        if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod' }}"
        env:
          NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
          PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
          ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
        shell: 'bash'
        run: |
          npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."

      - name: 'Get core Token'
        uses: './.github/actions/npm-auth-token'
        id: 'core-token'
        with:
          package-name: '${{ vars.CLI_PACKAGE_NAME }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
          wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
          wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'

      - name: 'Deprecate Core Npm Package'
        if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod' }}"
        env:
          NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
          PACKAGE_NAME: '${{ vars.CORE_PACKAGE_NAME }}'
          ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
        shell: 'bash'
        run: |
          npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."

      - name: 'Get a2a Token'
        uses: './.github/actions/npm-auth-token'
        id: 'a2a-token'
        with:
          package-name: '${{ vars.A2A_PACKAGE_NAME }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
          wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
          wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'

      - name: 'Deprecate A2A Server Npm Package'
        if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod'  }}"
        env:
          NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
          PACKAGE_NAME: '${{ vars.A2A_PACKAGE_NAME }}'
          ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
        shell: 'bash'
        run: |
          npm deprecate "${PACKAGE_NAME}@${ROLLBACK_ORIGIN}" "This version has been rolled back."

      - name: 'Delete Github Release'
        if: "${{ github.event.inputs.dry-run == 'false' && github.event.inputs.environment == 'prod'}}"
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
        shell: 'bash'
        run: |
          gh release delete "${ORIGIN_TAG}" --yes

      - name: 'Verify Origin Release Deletion'
        if: "${{ github.event.inputs.dry-run == 'false' }}"
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          TARGET_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
        shell: 'bash'
        run: |
          RELEASE_TAG=$(gh release view "$TARGET_TAG" --json tagName --jq .tagName)
          if [ "$RELEASE_TAG" = "$TARGET_TAG" ]; then
            echo "❌ Failed to delete release with tag ${TARGET_TAG}"
            echo '❌ This means the release was not deleted, and the workflow should fail.'
            exit 1
          fi

      - name: 'Add Rollback Tag'
        id: 'rollback_tag'
        if: "${{ github.event.inputs.dry-run == 'false' }}"
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          ROLLBACK_TAG_NAME: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}-rollback'
          ORIGIN_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
        shell: 'bash'
        run: |
          echo "ROLLBACK_TAG=$ROLLBACK_TAG_NAME" >> "$GITHUB_OUTPUT"
          git tag "$ROLLBACK_TAG_NAME" "${ORIGIN_HASH}"
          git push origin --tags

      - name: 'Verify Rollback Tag Added'
        if: "${{ github.event.inputs.dry-run == 'false' }}"
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          TARGET_TAG: '${{ steps.rollback_tag.outputs.ROLLBACK_TAG }}'
          TARGET_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
        shell: 'bash'
        run: |
          ROLLBACK_COMMIT=$(git rev-parse -q --verify "$TARGET_TAG")
          if [ "$ROLLBACK_COMMIT" != "$TARGET_HASH" ]; then
            echo "❌ Failed to add tag ${TARGET_TAG} to commit ${TARGET_HASH}"
            echo '❌ This means the tag was not added, and the workflow should fail.'
            exit 1
          fi

      - name: 'Log Dry run'
        if: "${{ github.event.inputs.dry-run == 'true' }}"
        env:
          ROLLBACK_ORIGIN: '${{ github.event.inputs.rollback_origin }}'
          ROLLBACK_DESTINATION: '${{ github.event.inputs.rollback_destination }}'
          CHANNEL: '${{ github.event.inputs.channel }}'
          REF_INPUT: '${{ github.event.inputs.ref }}'
          ORIGIN_TAG: '${{ steps.origin_tag.outputs.ORIGIN_TAG }}'
          ORIGIN_HASH: '${{ steps.origin_hash.outputs.ORIGIN_HASH }}'
          ROLLBACK_TAG: '${{ steps.rollback_tag.outputs.ROLLBACK_TAG }}'
          CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
          CORE_PACKAGE_NAME: '${{ vars.CORE_PACKAGE_NAME }}'
          A2A_PACKAGE_NAME: '${{ vars.A2A_PACKAGE_NAME }}'
        shell: 'bash'
        run: |
          echo "
          Inputs:
          - rollback_origin: '${ROLLBACK_ORIGIN}'
          - rollback_destination: '${ROLLBACK_DESTINATION}'
          - channel: '${CHANNEL}'
          - ref: '${REF_INPUT}'

          Outputs:
          - ORIGIN_TAG: '${ORIGIN_TAG}'
          - ORIGIN_HASH: '${ORIGIN_HASH}'
          - ROLLBACK_TAG: '${ROLLBACK_TAG}'

          Would have npm deprecate ${CLI_PACKAGE_NAME}@${ROLLBACK_ORIGIN}, ${CORE_PACKAGE_NAME}@${ROLLBACK_ORIGIN}, and ${A2A_PACKAGE_NAME}@${ROLLBACK_ORIGIN}
          Would have deleted the github release with tag ${ORIGIN_TAG}
          Would have added tag ${ORIGIN_TAG}-rollback to ${ORIGIN_HASH}
          "
release-sandbox .github/workflows/release-sandbox.yml
Triggers
workflow_dispatch
Runs on
ubuntu-latest
Jobs
build
Commands
  • gh issue create \ --title 'Sandbox Release Failed on $(date +'%Y-%m-%d')' \ --body 'The sandbox-release workflow failed. See the full run for details: ${DETAILS_URL}' \ --label 'release-failure,priority/p0'
View raw YAML
name: 'Release Sandbox'

on:
  workflow_dispatch:
    inputs:
      ref:
        description: 'The branch, tag, or SHA to release from.'
        required: false
        type: 'string'
        default: 'main'
      dry-run:
        description: 'Whether this is a dry run.'
        required: false
        type: 'boolean'
        default: true

jobs:
  build:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'ubuntu-latest'
    permissions:
      contents: 'read'
      packages: 'write'
      issues: 'write'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ github.event.inputs.ref || github.sha }}'
          fetch-depth: 0
      - name: 'Push'
        uses: './.github/actions/push-sandbox'
        with:
          dockerhub-username: '${{ secrets.DOCKER_SERVICE_ACCOUNT_NAME }}'
          dockerhub-token: '${{ secrets.DOCKER_SERVICE_ACCOUNT_KEY }}'
          github-actor: '${{ github.actor }}'
          github-secret: '${{ secrets.GITHUB_TOKEN }}'
          github-sha: '${{ github.sha }}'
          github-ref-name: '${{github.event.inputs.ref}}'
          dry-run: '${{ github.event.inputs.dry-run }}'
      - name: 'Create Issue on Failure'
        if: '${{ failure() && github.event.inputs.dry-run == false }}'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
        run: |
          gh issue create \
            --title 'Sandbox Release Failed on $(date +'%Y-%m-%d')' \
            --body 'The sandbox-release workflow failed. See the full run for details: ${DETAILS_URL}' \
            --label 'release-failure,priority/p0'
smoke-test .github/workflows/smoke-test.yml
Triggers
push, workflow_dispatch
Runs on
ubuntu-latest
Jobs
smoke-test
Commands
  • npm ci
  • npm run bundle
  • node ./bundle/gemini.js --version
  • gh issue create \ --title 'Smoke test failed on ${REF} @ $(date +'%Y-%m-%d')' \ --body 'Smoke test build failed. See the full run for details: ${DETAILS_URL}' \ --label 'priority/p0'
View raw YAML
name: 'On Merge Smoke Test'

on:
  push:
    branches:
      - 'main'
      - 'release/**'
  workflow_dispatch:
    inputs:
      ref:
        description: 'The branch, tag, or SHA to test on.'
        required: false
        type: 'string'
        default: 'main'
      dry-run:
        description: 'Run a dry-run of the smoke test; No bug will be created'
        required: true
        type: 'boolean'
        default: true

jobs:
  smoke-test:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'ubuntu-latest'
    permissions:
      contents: 'write'
      packages: 'write'
      issues: 'write'
    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
        with:
          ref: '${{ github.event.inputs.ref || github.sha }}'
          fetch-depth: 0
      - name: 'Install Dependencies'
        run: 'npm ci'
      - name: 'Build bundle'
        run: 'npm run bundle'
      - name: 'Smoke test bundle'
        run: 'node ./bundle/gemini.js --version'
      - name: 'Create Issue on Failure'
        if: '${{ failure() && github.event.inputs.dry-run == false }}'
        env:
          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
          DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
          REF: '${{ github.event.inputs.ref }}'
        run: |
          gh issue create \
            --title 'Smoke test failed on ${REF} @ $(date +'%Y-%m-%d')' \
            --body 'Smoke test build failed. See the full run for details: ${DETAILS_URL}' \
            --label 'priority/p0'
stale matrix .github/workflows/stale.yml
Triggers
schedule, workflow_dispatch
Runs on
${{ matrix.runner }}
Jobs
stale
Matrix
runner→ ubuntu-latest
Actions
actions/stale
View raw YAML
name: 'Mark stale issues and pull requests'

# Run as a daily cron at 1:30 AM
on:
  schedule:
    - cron: '30 1 * * *'
  workflow_dispatch:

jobs:
  stale:
    strategy:
      fail-fast: false
      matrix:
        runner:
          - 'ubuntu-latest' # GitHub-hosted
    runs-on: '${{ matrix.runner }}'
    if: |-
      ${{ github.repository == 'google-gemini/gemini-cli' }}
    permissions:
      issues: 'write'
      pull-requests: 'write'
    concurrency:
      group: '${{ github.workflow }}-stale'
      cancel-in-progress: true
    steps:
      - uses: 'actions/stale@5bef64f19d7facfb25b37b414482c7164d639639' # ratchet:actions/stale@v9
        with:
          repo-token: '${{ secrets.GITHUB_TOKEN }}'
          stale-issue-message: >-
            This issue has been automatically marked as stale due to 60 days of inactivity.
            It will be closed in 14 days if no further activity occurs.
          stale-pr-message: >-
            This pull request has been automatically marked as stale due to 60 days of inactivity.
            It will be closed in 14 days if no further activity occurs.
          close-issue-message: >-
            This issue has been closed due to 14 additional days of inactivity after being marked as stale.
            If you believe this is still relevant, feel free to comment or reopen the issue. Thank you!
          close-pr-message: >-
            This pull request has been closed due to 14 additional days of inactivity after being marked as stale.
            If this is still relevant, you are welcome to reopen or leave a comment. Thanks for contributing!
          days-before-stale: 60
          days-before-close: 14
          exempt-issue-labels: 'pinned,security,🔒 maintainer only,help wanted,🗓️ Public Roadmap'
          exempt-pr-labels: 'pinned,security,🔒 maintainer only,help wanted,🗓️ Public Roadmap'
test-build-binary matrix perms .github/workflows/test-build-binary.yml
Triggers
workflow_dispatch
Runs on
${{ matrix.os }}
Jobs
build-node-binary
Matrix
include, include.arch, include.os, include.platform_name→ arm64, darwin-arm64, darwin-x64, linux-x64, macos-latest, ubuntu-latest, win32-x64, windows-latest, x64
Actions
microsoft/setup-msbuild
Commands
  • Set-MpPreference -DisableRealtimeMonitoring $true Stop-Service -Name "wsearch" -Force -ErrorAction SilentlyContinue Set-Service -Name "wsearch" -StartupType Disabled Stop-Service -Name "SysMain" -Force -ErrorAction SilentlyContinue Set-Service -Name "SysMain" -StartupType Disabled
  • npm ci
  • echo "has_win_cert=${{ secrets.WINDOWS_PFX_BASE64 != '' }}" >> "$GITHUB_OUTPUT" echo "has_mac_cert=${{ secrets.MACOS_CERT_P12_BASE64 != '' }}" >> "$GITHUB_OUTPUT"
  • $signtoolPath = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter "signtool.exe" | Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty DirectoryName echo "Found signtool at: $signtoolPath" echo "$signtoolPath" >> $env:GITHUB_PATH
  • # Create the P12 file echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > certificate.p12 # Create a temporary keychain security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain # Import the certificate security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign # Allow codesign to access it security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain # Set Identity for build script echo "APPLE_IDENTITY=${{ secrets.MACOS_CERT_IDENTITY }}" >> "$GITHUB_ENV"
  • $pfx_cert_byte = [System.Convert]::FromBase64String("$env:PFX_BASE64") $certPath = Join-Path (Get-Location) "cert.pfx" [IO.File]::WriteAllBytes($certPath, $pfx_cert_byte) echo "WINDOWS_PFX_FILE=$certPath" >> $env:GITHUB_ENV echo "WINDOWS_PFX_PASSWORD=$env:PFX_PASSWORD" >> $env:GITHUB_ENV
  • npm run build:binary
  • npm run build -w @google/gemini-cli-core
View raw YAML
name: 'Test Build Binary'

on:
  workflow_dispatch:

permissions:
  contents: 'read'

defaults:
  run:
    shell: 'bash'

jobs:
  build-node-binary:
    name: 'Build Binary (${{ matrix.os }})'
    runs-on: '${{ matrix.os }}'
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: 'ubuntu-latest'
            platform_name: 'linux-x64'
            arch: 'x64'
          - os: 'windows-latest'
            platform_name: 'win32-x64'
            arch: 'x64'
          - os: 'macos-latest' # Apple Silicon (ARM64)
            platform_name: 'darwin-arm64'
            arch: 'arm64'
          - os: 'macos-latest' # Intel (x64) running on ARM via Rosetta
            platform_name: 'darwin-x64'
            arch: 'x64'

    steps:
      - name: 'Checkout'
        uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4

      - name: 'Optimize Windows Performance'
        if: "matrix.os == 'windows-latest'"
        run: |
          Set-MpPreference -DisableRealtimeMonitoring $true
          Stop-Service -Name "wsearch" -Force -ErrorAction SilentlyContinue
          Set-Service -Name "wsearch" -StartupType Disabled
          Stop-Service -Name "SysMain" -Force -ErrorAction SilentlyContinue
          Set-Service -Name "SysMain" -StartupType Disabled
        shell: 'powershell'

      - name: 'Set up Node.js'
        uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          architecture: '${{ matrix.arch }}'
          cache: 'npm'

      - name: 'Install dependencies'
        run: 'npm ci'

      - name: 'Check Secrets'
        id: 'check_secrets'
        run: |
          echo "has_win_cert=${{ secrets.WINDOWS_PFX_BASE64 != '' }}" >> "$GITHUB_OUTPUT"
          echo "has_mac_cert=${{ secrets.MACOS_CERT_P12_BASE64 != '' }}" >> "$GITHUB_OUTPUT"

      - name: 'Setup Windows SDK (Windows)'
        if: "matrix.os == 'windows-latest'"
        uses: 'microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce' # ratchet:microsoft/setup-msbuild@v2

      - name: 'Add Signtool to Path (Windows)'
        if: "matrix.os == 'windows-latest'"
        run: |
          $signtoolPath = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter "signtool.exe" | Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty DirectoryName
          echo "Found signtool at: $signtoolPath"
          echo "$signtoolPath" >> $env:GITHUB_PATH
        shell: 'pwsh'

      - name: 'Setup macOS Keychain'
        if: "startsWith(matrix.os, 'macos') && steps.check_secrets.outputs.has_mac_cert == 'true' && github.event_name != 'pull_request'"
        env:
          BUILD_CERTIFICATE_BASE64: '${{ secrets.MACOS_CERT_P12_BASE64 }}'
          P12_PASSWORD: '${{ secrets.MACOS_CERT_PASSWORD }}'
          KEYCHAIN_PASSWORD: 'temp-password'
        run: |
          # Create the P12 file
          echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > certificate.p12

          # Create a temporary keychain
          security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain

          # Import the certificate
          security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign

          # Allow codesign to access it
          security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain

          # Set Identity for build script
          echo "APPLE_IDENTITY=${{ secrets.MACOS_CERT_IDENTITY }}" >> "$GITHUB_ENV"

      - name: 'Setup Windows Certificate'
        if: "matrix.os == 'windows-latest' && steps.check_secrets.outputs.has_win_cert == 'true' && github.event_name != 'pull_request'"
        env:
          PFX_BASE64: '${{ secrets.WINDOWS_PFX_BASE64 }}'
          PFX_PASSWORD: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
        run: |
          $pfx_cert_byte = [System.Convert]::FromBase64String("$env:PFX_BASE64")
          $certPath = Join-Path (Get-Location) "cert.pfx"
          [IO.File]::WriteAllBytes($certPath, $pfx_cert_byte)
          echo "WINDOWS_PFX_FILE=$certPath" >> $env:GITHUB_ENV
          echo "WINDOWS_PFX_PASSWORD=$env:PFX_PASSWORD" >> $env:GITHUB_ENV
        shell: 'pwsh'

      - name: 'Build Binary'
        run: 'npm run build:binary'

      - name: 'Build Core Package'
        run: 'npm run build -w @google/gemini-cli-core'

      - name: 'Verify Output Exists'
        run: |
          if [ -f "dist/${{ matrix.platform_name }}/gemini" ]; then
            echo "Binary found at dist/${{ matrix.platform_name }}/gemini"
          elif [ -f "dist/${{ matrix.platform_name }}/gemini.exe" ]; then
            echo "Binary found at dist/${{ matrix.platform_name }}/gemini.exe"
          else
            echo "Error: Binary not found in dist/${{ matrix.platform_name }}/"
            ls -R dist/
            exit 1
          fi

      - name: 'Smoke Test Binary'
        run: |
          echo "Running binary smoke test..."
          if [ -f "dist/${{ matrix.platform_name }}/gemini.exe" ]; then
            "./dist/${{ matrix.platform_name }}/gemini.exe" --version
          else
            "./dist/${{ matrix.platform_name }}/gemini" --version
          fi

      - name: 'Run Integration Tests'
        if: "github.event_name != 'pull_request'"
        env:
          GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
        run: |
          echo "Running integration tests with binary..."
          if [[ "${{ matrix.os }}" == 'windows-latest' ]]; then
            BINARY_PATH="$(cygpath -m "$(pwd)/dist/${{ matrix.platform_name }}/gemini.exe")"
          else
            BINARY_PATH="$(pwd)/dist/${{ matrix.platform_name }}/gemini"
          fi
          echo "Using binary at $BINARY_PATH"
          export INTEGRATION_TEST_GEMINI_BINARY_PATH="$BINARY_PATH"
          npm run test:integration:sandbox:none -- --testTimeout=600000

      - name: 'Upload Artifact'
        uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
        with:
          name: 'gemini-cli-${{ matrix.platform_name }}'
          path: 'dist/${{ matrix.platform_name }}/'
          retention-days: 5
trigger_e2e .github/workflows/trigger_e2e.yml
Triggers
workflow_dispatch, pull_request
Runs on
gemini-cli-ubuntu-16-core, gemini-cli-ubuntu-16-core
Jobs
save_repo_name, trigger_e2e
Commands
  • mkdir -p ./pr echo "${REPO_NAME}" > ./pr/repo_name echo "${HEAD_SHA}" > ./pr/head_sha
  • echo "Trigger e2e workflow"
View raw YAML
name: 'Trigger E2E'

on:
  workflow_dispatch:
    inputs:
      repo_name:
        description: 'Repository name (e.g., owner/repo)'
        required: false
        type: 'string'
      head_sha:
        description: 'SHA of the commit to test'
        required: false
        type: 'string'
  pull_request:

jobs:
  save_repo_name:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'gemini-cli-ubuntu-16-core'
    steps:
      - name: 'Save Repo name'
        env:
          REPO_NAME: '${{ github.event.inputs.repo_name || github.event.pull_request.head.repo.full_name }}'
          HEAD_SHA: '${{ github.event.inputs.head_sha || github.event.pull_request.head.sha }}'
        run: |
          mkdir -p ./pr
          echo "${REPO_NAME}" > ./pr/repo_name
          echo "${HEAD_SHA}" > ./pr/head_sha
      - uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
        with:
          name: 'repo_name'
          path: 'pr/'
  trigger_e2e:
    name: 'Trigger e2e'
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'gemini-cli-ubuntu-16-core'
    steps:
      - id: 'trigger-e2e'
        run: |
          echo "Trigger e2e workflow"
unassign-inactive-assignees .github/workflows/unassign-inactive-assignees.yml
Triggers
schedule, workflow_dispatch
Runs on
ubuntu-latest
Jobs
unassign-inactive-assignees
Actions
actions/create-github-app-token
View raw YAML
name: 'Unassign Inactive Issue Assignees'

# This workflow runs daily and scans every open "help wanted" issue that has
# one or more assignees.  For each assignee it checks whether they have a
# non-draft pull request (open and ready for review, or already merged) that
# is linked to the issue.  Draft PRs are intentionally excluded so that
# contributors cannot reset the check by opening a no-op PR.  If no
# qualifying PR is found within 7 days of assignment the assignee is
# automatically removed and a friendly comment is posted so that other
# contributors can pick up the work.
# Maintainers, org members, and collaborators (anyone with write access or
# above) are always exempted and will never be auto-unassigned.

on:
  schedule:
    - cron: '0 9 * * *' # Every day at 09:00 UTC
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Run in dry-run mode (no changes will be applied)'
        required: false
        default: false
        type: 'boolean'

concurrency:
  group: '${{ github.workflow }}'
  cancel-in-progress: true

defaults:
  run:
    shell: 'bash'

jobs:
  unassign-inactive-assignees:
    if: "github.repository == 'google-gemini/gemini-cli'"
    runs-on: 'ubuntu-latest'
    permissions:
      issues: 'write'

    steps:
      - name: 'Generate GitHub App Token'
        id: 'generate_token'
        uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
        with:
          app-id: '${{ secrets.APP_ID }}'
          private-key: '${{ secrets.PRIVATE_KEY }}'

      - name: 'Unassign inactive assignees'
        uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
        env:
          DRY_RUN: '${{ inputs.dry_run }}'
        with:
          github-token: '${{ steps.generate_token.outputs.token }}'
          script: |
            const dryRun = process.env.DRY_RUN === 'true';
            if (dryRun) {
              core.info('DRY RUN MODE ENABLED: No changes will be applied.');
            }

            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const GRACE_PERIOD_DAYS = 7;
            const now = new Date();

            let maintainerLogins = new Set();
            const teams = ['gemini-cli-maintainers', 'gemini-cli-askmode-approvers', 'gemini-cli-docs'];

            for (const team_slug of teams) {
              try {
                const members = await github.paginate(github.rest.teams.listMembersInOrg, {
                  org: owner,
                  team_slug,
                });
                for (const m of members) maintainerLogins.add(m.login.toLowerCase());
                core.info(`Fetched ${members.length} members from team ${team_slug}.`);
              } catch (e) {
                core.warning(`Could not fetch team ${team_slug}: ${e.message}`);
              }
            }

            const isGooglerCache = new Map();
            const isGoogler = async (login) => {
              if (isGooglerCache.has(login)) return isGooglerCache.get(login);
              try {
                for (const org of ['googlers', 'google']) {
                  try {
                    await github.rest.orgs.checkMembershipForUser({ org, username: login });
                    isGooglerCache.set(login, true);
                    return true;
                  } catch (e) {
                    if (e.status !== 404) throw e;
                  }
                }
              } catch (e) {
                core.warning(`Could not check org membership for ${login}: ${e.message}`);
              }
              isGooglerCache.set(login, false);
              return false;
            };

            const permissionCache = new Map();
            const isPrivilegedUser = async (login) => {
              if (maintainerLogins.has(login.toLowerCase())) return true;

              if (permissionCache.has(login)) return permissionCache.get(login);

              try {
                const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
                  owner,
                  repo,
                  username: login,
                });
                const privileged = ['admin', 'maintain', 'write', 'triage'].includes(data.permission);
                permissionCache.set(login, privileged);
                if (privileged) {
                  core.info(`  @${login} is a repo collaborator (${data.permission}) — exempt.`);
                  return true;
                }
              } catch (e) {
                if (e.status !== 404) {
                  core.warning(`Could not check permission for ${login}: ${e.message}`);
                }
              }

              const googler = await isGoogler(login);
              permissionCache.set(login, googler);
              return googler;
            };

            core.info('Fetching open "help wanted" issues with assignees...');

            const issues = await github.paginate(github.rest.issues.listForRepo, {
              owner,
              repo,
              state: 'open',
              labels: 'help wanted',
              per_page: 100,
            });

            const assignedIssues = issues.filter(
              (issue) => !issue.pull_request && issue.assignees && issue.assignees.length > 0
            );

            core.info(`Found ${assignedIssues.length} assigned "help wanted" issues.`);

            let totalUnassigned = 0;

              let timelineEvents = [];
              try {
                timelineEvents = await github.paginate(github.rest.issues.listEventsForTimeline, {
                  owner,
                  repo,
                  issue_number: issue.number,
                  per_page: 100,
                  mediaType: { previews: ['mockingbird'] },
                });
              } catch (err) {
                core.warning(`Could not fetch timeline for issue #${issue.number}: ${err.message}`);
                continue;
              }

              const assignedAtMap = new Map();

              for (const event of timelineEvents) {
                if (event.event === 'assigned' && event.assignee) {
                  const login = event.assignee.login.toLowerCase();
                  const at = new Date(event.created_at);
                  assignedAtMap.set(login, at);
                } else if (event.event === 'unassigned' && event.assignee) {
                  assignedAtMap.delete(event.assignee.login.toLowerCase());
                }
              }

              const linkedPRAuthorSet = new Set();
              const seenPRKeys = new Set();

              for (const event of timelineEvents) {
                if (
                  event.event !== 'cross-referenced' ||
                  !event.source ||
                  event.source.type !== 'pull_request' ||
                  !event.source.issue ||
                  !event.source.issue.user ||
                  !event.source.issue.number ||
                  !event.source.issue.repository
                ) continue;

                const prOwner  = event.source.issue.repository.owner.login;
                const prRepo   = event.source.issue.repository.name;
                const prNumber = event.source.issue.number;
                const prAuthor = event.source.issue.user.login.toLowerCase();
                const prKey    = `${prOwner}/${prRepo}#${prNumber}`;

                if (seenPRKeys.has(prKey)) continue;
                seenPRKeys.add(prKey);

                try {
                  const { data: pr } = await github.rest.pulls.get({
                    owner: prOwner,
                    repo: prRepo,
                    pull_number: prNumber,
                  });

                  const isReady = (pr.state === 'open' && !pr.draft) ||
                                  (pr.state === 'closed' && pr.merged_at !== null);

                  core.info(
                    `  PR ${prKey} by @${prAuthor}: ` +
                    `state=${pr.state}, draft=${pr.draft}, merged=${!!pr.merged_at} → ` +
                    (isReady ? 'qualifies' : 'does NOT qualify (draft or closed without merge)')
                  );

                  if (isReady) linkedPRAuthorSet.add(prAuthor);
                } catch (err) {
                  core.warning(`Could not fetch PR ${prKey}: ${err.message}`);
                }
              }

              const assigneesToRemove = [];

              for (const assignee of issue.assignees) {
                const login = assignee.login.toLowerCase();

                if (await isPrivilegedUser(assignee.login)) {
                  core.info(`  @${assignee.login}: privileged user — skipping.`);
                  continue;
                }

                const assignedAt = assignedAtMap.get(login);

                if (!assignedAt) {
                  core.warning(
                    `No 'assigned' event found for @${login} on issue #${issue.number}; ` +
                    `falling back to issue creation date (${issue.created_at}).`
                  );
                  assignedAtMap.set(login, new Date(issue.created_at));
                }
                const resolvedAssignedAt = assignedAtMap.get(login);

                const daysSinceAssignment = (now - resolvedAssignedAt) / (1000 * 60 * 60 * 24);

                core.info(
                  `  @${login}: assigned ${daysSinceAssignment.toFixed(1)} day(s) ago, ` +
                  `ready-for-review PR: ${linkedPRAuthorSet.has(login) ? 'yes' : 'no'}`
                );

                if (daysSinceAssignment < GRACE_PERIOD_DAYS) {
                  core.info(`    → within grace period, skipping.`);
                  continue;
                }

                if (linkedPRAuthorSet.has(login)) {
                  core.info(`    → ready-for-review PR found, keeping assignment.`);
                  continue;
                }

                core.info(`    → no ready-for-review PR after ${GRACE_PERIOD_DAYS} days, will unassign.`);
                assigneesToRemove.push(assignee.login);
              }

              if (assigneesToRemove.length === 0) {
                continue;
              }

              if (!dryRun) {
                try {
                  await github.rest.issues.removeAssignees({
                    owner,
                    repo,
                    issue_number: issue.number,
                    assignees: assigneesToRemove,
                  });
                } catch (err) {
                  core.warning(
                    `Failed to unassign ${assigneesToRemove.join(', ')} from issue #${issue.number}: ${err.message}`
                  );
                  continue;
                }

                const mentionList = assigneesToRemove.map((l) => `@${l}`).join(', ');
                const commentBody =
                  `👋 ${mentionList} — it has been more than ${GRACE_PERIOD_DAYS} days since ` +
                  `you were assigned to this issue and we could not find a pull request ` +
                  `ready for review.\n\n` +
                  `To keep the backlog moving and ensure issues stay accessible to all ` +
                  `contributors, we require a PR that is open and ready for review (not a ` +
                  `draft) within ${GRACE_PERIOD_DAYS} days of assignment.\n\n` +
                  `We are automatically unassigning you so that other contributors can pick ` +
                  `this up. If you are still actively working on this, please:\n` +
                  `1. Re-assign yourself by commenting \`/assign\`.\n` +
                  `2. Open a PR (not a draft) linked to this issue (e.g. \`Fixes #${issue.number}\`) ` +
                  `within ${GRACE_PERIOD_DAYS} days so the automation knows real progress is being made.\n\n` +
                  `Thank you for your contribution — we hope to see a PR from you soon! 🙏`;

                try {
                  await github.rest.issues.createComment({
                    owner,
                    repo,
                    issue_number: issue.number,
                    body: commentBody,
                  });
                } catch (err) {
                  core.warning(
                    `Failed to post comment on issue #${issue.number}: ${err.message}`
                  );
                }
              }

              totalUnassigned += assigneesToRemove.length;
              core.info(
                `  ${dryRun ? '[DRY RUN] Would have unassigned' : 'Unassigned'}: ${assigneesToRemove.join(', ')}`
              );
            }

            core.info(`\nDone. Total assignees ${dryRun ? 'that would be' : ''} unassigned: ${totalUnassigned}`);
verify-release matrix .github/workflows/verify-release.yml
Triggers
workflow_dispatch
Runs on
${{ matrix.os }}
Jobs
verify-release
Matrix
os→ macos-latest, ubuntu-latest, windows-latest
Commands
  • echo "${{ toJSON(vars) }}"
View raw YAML
name: 'Verify NPM release tag'

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'The expected Gemini binary version that should be released (e.g., 0.5.0-preview-2).'
        required: true
        type: 'string'
      npm-tag:
        description: 'NPM tag to verify'
        required: true
        type: 'choice'
        options:
          - 'dev'
          - 'latest'
          - 'preview'
          - 'nightly'
        default: 'latest'
      environment:
        description: 'Environment'
        required: false
        type: 'choice'
        options:
          - 'prod'
          - 'dev'
        default: 'prod'

jobs:
  verify-release:
    if: "github.repository == 'google-gemini/gemini-cli'"
    environment: "${{ github.event.inputs.environment || 'prod' }}"
    strategy:
      fail-fast: false
      matrix:
        os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
    runs-on: '${{ matrix.os }}'
    permissions:
      contents: 'read'
      packages: 'write'
      issues: 'write'
    steps:
      - name: '📝 Print vars'
        shell: 'bash'
        run: 'echo "${{ toJSON(vars) }}"'
      - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8'
      - name: 'Verify release'
        uses: './.github/actions/verify-release'
        with:
          npm-package: '${{vars.CLI_PACKAGE_NAME}}@${{github.event.inputs.npm-tag}}'
          expected-version: '${{github.event.inputs.version}}'
          working-directory: '.'
          gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
          npm-registry-url: '${{ vars.NPM_REGISTRY_URL }}'
          github-token: '${{ secrets.GITHUB_TOKEN }}'
          npm-registry-scope: '${{ vars.NPM_REGISTRY_SCOPE }}'