From 0ca8669a809d34fe76fb31b109df403b850f131e Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Mon, 22 Sep 2025 15:11:27 -0700 Subject: [PATCH] Update .github directory from main branch (#9155) --- .../actions/create-pull-request/action.yml | 55 +++ .github/actions/publish-release/action.yml | 48 +- .github/actions/push-docker/action.yml | 73 +++ .github/actions/push-sandbox/action.yaml | 85 ++++ .github/actions/tag-npm-release/action.yml | 53 +++ .github/actions/verify-release/action.yml | 55 +++ .github/workflows/ci.yml | 419 +++++++----------- .github/workflows/create-patch-pr.yml | 58 --- .github/workflows/e2e.yml | 195 ++++++-- .github/workflows/patch-from-comment.yml | 56 --- .github/workflows/patch-release.yml | 94 ---- .github/workflows/release-change-tags.yml | 56 +++ .github/workflows/release-manual.yml | 89 ++++ ...ightly-release.yml => release-nightly.yml} | 15 +- .../workflows/release-patch-1-create-pr.yml | 109 +++++ .github/workflows/release-patch-2-trigger.yml | 72 +++ .github/workflows/release-patch-3-release.yml | 219 +++++++++ .../workflows/release-patch-from-comment.yml | 187 ++++++++ ...romote-release.yml => release-promote.yml} | 25 +- .github/workflows/release-sandbox.yml | 33 ++ .github/workflows/release.yml | 202 --------- .github/workflows/trigger-patch-release.yml | 30 -- .github/workflows/verify-release.yml | 31 ++ 23 files changed, 1520 insertions(+), 739 deletions(-) create mode 100644 .github/actions/create-pull-request/action.yml create mode 100644 .github/actions/push-docker/action.yml create mode 100644 .github/actions/push-sandbox/action.yaml create mode 100644 .github/actions/tag-npm-release/action.yml create mode 100644 .github/actions/verify-release/action.yml delete mode 100644 .github/workflows/create-patch-pr.yml delete mode 100644 .github/workflows/patch-from-comment.yml delete mode 100644 .github/workflows/patch-release.yml create mode 100644 .github/workflows/release-change-tags.yml create mode 100644 .github/workflows/release-manual.yml rename .github/workflows/{nightly-release.yml => release-nightly.yml} (84%) create mode 100644 .github/workflows/release-patch-1-create-pr.yml create mode 100644 .github/workflows/release-patch-2-trigger.yml create mode 100644 .github/workflows/release-patch-3-release.yml create mode 100644 .github/workflows/release-patch-from-comment.yml rename .github/workflows/{promote-release.yml => release-promote.yml} (95%) create mode 100644 .github/workflows/release-sandbox.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/trigger-patch-release.yml create mode 100644 .github/workflows/verify-release.yml diff --git a/.github/actions/create-pull-request/action.yml b/.github/actions/create-pull-request/action.yml new file mode 100644 index 0000000000..4b562a6183 --- /dev/null +++ b/.github/actions/create-pull-request/action.yml @@ -0,0 +1,55 @@ +name: 'Create and Merge Pull Request' +description: 'Creates a pull request and merges it automatically.' + +inputs: + branch-name: + description: 'The name of the branch to create the PR from.' + required: true + pr-title: + description: 'The title of the pull request.' + required: true + pr-body: + description: 'The body of the pull request.' + required: true + base-branch: + description: 'The branch to merge into.' + required: true + default: 'main' + app-id: + description: 'The ID of the GitHub App.' + required: true + private-key: + description: 'The private key of the GitHub App.' + required: true + dry-run: + description: 'Whether to run in dry-run mode.' + required: false + default: 'false' + +runs: + using: 'composite' + steps: + - name: 'Generate GitHub App Token' + id: 'generate_token' + if: "inputs.dry-run == 'false'" + uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' + with: + app-id: '${{ inputs.app-id }}' + private-key: '${{ inputs.private-key }}' + permission-pull-requests: 'write' + permission-contents: 'write' + + - name: 'Create and Approve Pull Request' + if: "inputs.dry-run == 'false'" + env: + GH_TOKEN: '${{ steps.generate_token.outputs.token }}' + shell: 'bash' + run: | + set -e + PR_URL=$(gh pr create \ + --title "${{ inputs.pr-title }}" \ + --body "${{ inputs.pr-body }}" \ + --base "${{ inputs.base-branch }}" \ + --head "${{ inputs.branch-name }}" \ + --fill) + gh pr merge "$PR_URL" --auto --squash diff --git a/.github/actions/publish-release/action.yml b/.github/actions/publish-release/action.yml index 579903de9c..8ab2104016 100644 --- a/.github/actions/publish-release/action.yml +++ b/.github/actions/publish-release/action.yml @@ -27,10 +27,19 @@ inputs: previous-tag: description: 'The previous tag to use for generating release notes.' required: true + skip-github-release: + description: 'Whether to skip creating a GitHub release.' + type: 'boolean' + required: false + default: false working-directory: description: 'The working directory to run the steps in.' required: false default: '.' + force-skip-tests: + description: 'Skip tests and validation' + required: false + default: false runs: using: 'composite' @@ -102,7 +111,7 @@ runs: npm publish \ --dry-run="${{ inputs.dry-run }}" \ --workspace="@google/gemini-cli-core" \ - --tag="${{ inputs.npm-tag }}" + --no-tag - name: '๐Ÿ”— Install latest core package' working-directory: '${{ inputs.working-directory }}' @@ -122,7 +131,31 @@ runs: npm publish \ --dry-run="${{ inputs.dry-run }}" \ --workspace="@google/gemini-cli" \ - --tag="${{ inputs.npm-tag }}" + --no-tag + + - name: '๐Ÿ”ฌ Verify NPM release by version' + uses: './.github/actions/verify-release' + if: "${{ inputs.dry-run == 'false' && inputs.force-skip-tests == 'false' }}" + with: + npm-package: '@google/gemini-cli@${{ inputs.release-version }}' + expected-version: '${{ inputs.release-version }}' + ref: '${{ steps.release_branch.outputs.BRANCH_NAME }}' + + - name: '๐Ÿท๏ธ Tag release' + uses: './.github/actions/tag-npm-release' + if: "${{ inputs.dry-run == 'false' }}" + with: + channel: '${{ inputs.npm-tag }}' + version: '${{ inputs.release-version }}' + dry-run: '${{ inputs.dry-run }}' + wombat-token-core: '${{ inputs.wombat-token-core }}' + wombat-token-cli: '${{ inputs.wombat-token-cli }}' + + - name: 'Install deps' + working-directory: '${{ inputs.working-directory }}' + shell: 'bash' + run: | + npm install - name: '๐ŸŽ Bundle' working-directory: '${{ inputs.working-directory }}' @@ -132,7 +165,7 @@ runs: - name: '๐ŸŽ‰ Create GitHub Release' working-directory: '${{ inputs.working-directory }}' - if: "${{ inputs.dry-run == 'false' }}" + if: "${{ inputs.dry-run == 'false' && inputs.skip-github-release == 'false' && inputs.npm-tag != 'dev' }}" env: GITHUB_TOKEN: '${{ inputs.github-token }}' shell: 'bash' @@ -143,3 +176,12 @@ runs: --title "Release ${{ inputs.release-tag }}" \ --notes-start-tag "${{ inputs.previous-tag }}" \ --generate-notes + + - name: '๐Ÿงน Clean up release branch' + working-directory: '${{ inputs.working-directory }}' + if: "${{ inputs.dry-run == 'false' }}" + continue-on-error: true + shell: 'bash' + run: | + echo "Cleaning up release branch ${{ steps.release_branch.outputs.BRANCH_NAME }}..." + git push origin --delete "${{ steps.release_branch.outputs.BRANCH_NAME }}" diff --git a/.github/actions/push-docker/action.yml b/.github/actions/push-docker/action.yml new file mode 100644 index 0000000000..d59f4be763 --- /dev/null +++ b/.github/actions/push-docker/action.yml @@ -0,0 +1,73 @@ +name: 'Push to docker' +description: 'Builds packages and pushes a docker image to GHCR' + +inputs: + github-actor: + description: 'Github actor' + required: true + github-secret: + description: 'Github secret' + required: true + ref-name: + description: 'Github ref name' + required: true + github-sha: + description: 'Github Commit SHA Hash' + required: true + +runs: + using: 'composite' + steps: + - name: 'Checkout' + uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4 + with: + ref: '${{ inputs.github-sha }}' + fetch-depth: 0 + - name: 'Install Dependencies' + shell: 'bash' + run: 'npm install' + - name: 'Set up Docker Buildx' + uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3 + - name: 'build' + shell: 'bash' + run: 'npm run build' + - name: 'pack @google/gemini-cli' + shell: 'bash' + run: 'npm pack -w @google/gemini-cli --pack-destination ./packages/cli/dist' + - name: 'pack @google/gemini-cli-core' + shell: 'bash' + run: 'npm pack -w @google/gemini-cli-core --pack-destination ./packages/core/dist' + - name: 'Log in to GitHub Container Registry' + uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3 + with: + registry: 'ghcr.io' + username: '${{ inputs.github-actor }}' + password: '${{ inputs.github-secret }}' + - name: 'Get branch name' + id: 'branch_name' + shell: 'bash' + run: | + REF_NAME="${{ inputs.ref-name }}" + echo "name=${REF_NAME%/merge}" >> $GITHUB_OUTPUT + - name: 'Build and Push the Docker Image' + uses: 'docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83' # ratchet:docker/build-push-action@v6 + with: + context: '.' + file: './Dockerfile' + push: true + provenance: false # avoid pushing 3 images to Aritfact Registry + tags: | + ghcr.io/${{ github.repository }}/cli:${{ steps.branch_name.outputs.name }} + ghcr.io/${{ github.repository }}/cli:${{ inputs.github-sha }} + - name: 'Create issue on failure' + if: |- + ${{ failure() }} + shell: 'bash' + env: + GITHUB_TOKEN: '${{ inputs.github-secret }}' + DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + run: |- + gh issue create \ + --title "Docker build failed" \ + --body "The docker build failed. See the full run for details: ${DETAILS_URL}" \ + --label "kind/bug,release-failure" diff --git a/.github/actions/push-sandbox/action.yaml b/.github/actions/push-sandbox/action.yaml new file mode 100644 index 0000000000..85c705b9ce --- /dev/null +++ b/.github/actions/push-sandbox/action.yaml @@ -0,0 +1,85 @@ +name: 'Build and push sandbox docker' +description: 'Pushes sandbox docker image to container registry' + +inputs: + github-actor: + description: 'Github actor' + required: true + github-secret: + description: 'Github secret' + required: true + github-sha: + description: 'Github Commit SHA Hash' + required: true + github-ref-name: + description: 'Github ref name' + required: true + dry-run: + description: 'Whether this is a dry run.' + required: true + type: 'boolean' + +runs: + using: 'composite' + steps: + - name: 'Checkout' + uses: 'actions/checkout@v4' + with: + ref: '${{ inputs.github-sha }}' + fetch-depth: 0 + - name: 'Install Dependencies' + shell: 'bash' + run: 'npm install' + - name: 'npm build' + shell: 'bash' + run: 'npm run build' + - name: 'Set up Docker Buildx' + uses: 'docker/setup-buildx-action@v3' + - name: 'Log in to GitHub Container Registry' + uses: 'docker/login-action@v3' + with: + registry: 'ghcr.io' + username: '${{ inputs.github-actor }}' + password: '${{ inputs.github-secret }}' + - name: 'determine image tag' + id: 'image_tag' + shell: 'bash' + run: |- + SHELL_TAG_NAME="${{ inputs.github-ref-name }}" + FINAL_TAG="${{ inputs.github-sha }}" + if [[ "$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + echo "Release detected." + FINAL_TAG="${SHELL_TAG_NAME#v}" + else + echo "Development release detected. Using commit SHA as tag." + fi + echo "Determined image tag: $FINAL_TAG" + echo "FINAL_TAG=$FINAL_TAG" >> $GITHUB_OUTPUT + - name: 'build' + id: 'docker_build' + shell: 'bash' + env: + GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}' + GEMINI_SANDBOX: 'docker' + run: |- + npm run build:sandbox -- \ + --image ghcr.io/${{ github.repository}}/sandbox:${{ steps.image_tag.outputs.FINAL_TAG }} \ + --output-file final_image_uri.txt + echo "uri=$(cat final_image_uri.txt)" >> $GITHUB_OUTPUT + - name: 'publish' + shell: 'bash' + if: "${{ inputs.dry-run == 'false' }}" + run: |- + docker push "${{ steps.docker_build.outputs.uri }}" + - name: 'Create issue on failure' + if: |- + ${{ failure() }} + shell: 'bash' + env: + GITHUB_TOKEN: '${{ inputs.github-secret }}' + DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + run: |- + gh issue create \ + --title "Docker build failed" \ + --body "The docker build failed. See the full run for details: ${DETAILS_URL}" \ + --label "kind/bug,release-failure" diff --git a/.github/actions/tag-npm-release/action.yml b/.github/actions/tag-npm-release/action.yml new file mode 100644 index 0000000000..cbc49d3d8d --- /dev/null +++ b/.github/actions/tag-npm-release/action.yml @@ -0,0 +1,53 @@ +name: 'Tag an NPM release' +description: 'Tags a specific npm version to a specific channel.' + +inputs: + channel: + description: 'NPM Channel tag' + required: true + version: + description: 'version' + required: true + dry-run: + description: 'Whether to run in dry-run mode.' + required: true + wombat-token-core: + description: 'The npm token for the wombat @google/gemini-cli-core' + required: true + wombat-token-cli: + description: 'The npm token for wombat @google/gemini-cli' + +runs: + using: 'composite' + steps: + - name: 'Setup Node.js' + uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' + with: + node-version-file: '.nvmrc' + registry-url: 'https://wombat-dressing-room.appspot.com' + scope: '@google' + + - name: 'Change tag for @google/gemini-cli-core' + if: |- + ${{ inputs.dry-run == 'false' }} + env: + NODE_AUTH_TOKEN: '${{ inputs.wombat-token-core }}' + shell: 'bash' + run: | + npm dist-tag add @google/gemini-cli-core@${{ inputs.version }} ${{ inputs.channel }} + + - name: 'Change tag for @google/gemini-cli' + if: |- + ${{ inputs.dry-run == 'false' }} + env: + NODE_AUTH_TOKEN: '${{ inputs.wombat-token-cli }}' + shell: 'bash' + run: | + npm dist-tag add @google/gemini-cli@${{ inputs.version }} ${{ inputs.channel }} + + - name: 'Log dry run' + if: |- + ${{ inputs.dry-run == 'true' }} + shell: 'bash' + run: | + echo "Dry run: Would have added tag '${{ inputs.channel }}' to version '${{ inputs.version }}' for @google/gemini-cli and @google/gemini-cli-core." diff --git a/.github/actions/verify-release/action.yml b/.github/actions/verify-release/action.yml new file mode 100644 index 0000000000..36f0eff6ce --- /dev/null +++ b/.github/actions/verify-release/action.yml @@ -0,0 +1,55 @@ +name: 'Verify an NPM release' +description: 'Fetches a package from NPM and does some basic smoke tests' + +inputs: + npm-package: + description: 'NPM Package' + required: true + default: '@google/gemini-cli@latest' + expected-version: + description: 'Expected version' + required: true + ref: + description: 'The branch, tag, or SHA to release from.' + required: false + type: 'string' + default: 'main' + +runs: + using: 'composite' + steps: + - name: '๐Ÿ“ Print Inputs' + shell: 'bash' + run: | + echo "${{ toJSON(inputs) }}" + - name: 'Checkout' + uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v4 + with: + ref: '${{ github.event.inputs.ref }}' + fetch-depth: 0 + + - name: 'Install from NPM' + uses: 'nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08' # ratchet:nick-fields/retry@v3 + with: + timeout_seconds: 900 + retry_wait_seconds: 30 + max_attempts: 10 + command: |- + npm install --prefer-online --no-cache -g ${{ inputs.npm-package }} + + # This provides a very basic smoke test for Gemini CLI + - name: 'Run Gemini CLI' + id: 'gemini_cli' + shell: 'bash' + run: |- + echo "gemini_version=$(gemini --version)" >> $GITHUB_OUTPUT + + # Force a failure if it doesn't match + - name: 'Fail workflow if version does not match' + if: '${{ steps.gemini_cli.outputs.gemini_version != inputs.expected-version }}' + shell: 'bash' + run: |- + echo 'โŒ Got ${{ steps.gemini_cli.outputs.gemini_version }} from ${{ inputs.npm-package }}' + echo 'โŒ Expected Version ${{ inputs.expected-version }}' + + exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d2f21737a..1e95d76623 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: 'Gemini CLI CI' +name: 'Testing: CI' on: push: @@ -10,6 +10,7 @@ on: - 'main' - 'release/**' merge_group: + workflow_dispatch: concurrency: group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' @@ -25,63 +26,10 @@ defaults: run: shell: 'bash' -env: - ACTIONLINT_VERSION: '1.7.7' - SHELLCHECK_VERSION: '0.11.0' - YAMLLINT_VERSION: '1.35.1' - jobs: - # - # Lint: GitHub Actions - # - lint_github_actions: - name: 'Lint (GitHub Actions)' - runs-on: 'ubuntu-latest' - steps: - - name: 'Checkout' - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 - with: - fetch-depth: 1 - - - name: 'Install shellcheck' # Actionlint uses shellcheck - run: |- - mkdir -p "${RUNNER_TEMP}/shellcheck" - curl -sSLo "${RUNNER_TEMP}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" - tar -xf "${RUNNER_TEMP}/.shellcheck.txz" -C "${RUNNER_TEMP}/shellcheck" --strip-components=1 - echo "${RUNNER_TEMP}/shellcheck" >> "${GITHUB_PATH}" - - - name: 'Install actionlint' - run: |- - mkdir -p "${RUNNER_TEMP}/actionlint" - curl -sSLo "${RUNNER_TEMP}/.actionlint.tgz" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" - tar -xzf "${RUNNER_TEMP}/.actionlint.tgz" -C "${RUNNER_TEMP}/actionlint" - echo "${RUNNER_TEMP}/actionlint" >> "${GITHUB_PATH}" - - # For actionlint, we specifically ignore shellcheck rules that are - # annoying or unhelpful. See the shellcheck action for a description. - - name: 'Run actionlint' - run: |- - actionlint \ - -color \ - -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Filepath}}@{{$err.Line}} {{$err.Message}}%0A```%0A{{replace $err.Snippet "\\n" "%0A"}}%0A```\n{{end}}' \ - -ignore 'SC2002:' \ - -ignore 'SC2016:' \ - -ignore 'SC2129:' \ - -ignore 'label ".+" is unknown' - - - name: 'Run ratchet' - uses: 'sethvargo/ratchet@8b4ca256dbed184350608a3023620f267f0a5253' # ratchet:sethvargo/ratchet@v0.11.4 - with: - files: |- - .github/workflows/*.yml - .github/actions/**/*.yml - - # - # Lint: Javascript - # - lint_javascript: - name: 'Lint (Javascript)' - runs-on: 'ubuntu-latest' + lint: + name: 'Lint' + runs-on: 'gemini-cli-ubuntu-16-core' steps: - name: 'Checkout' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 @@ -94,161 +42,33 @@ jobs: node-version-file: '.nvmrc' cache: 'npm' - - name: 'Run lockfile check' - run: |- - npm run check:lockfile - - name: 'Install dependencies' - run: |- - npm ci + run: 'npm ci' - - name: 'Run formatter check' - run: |- - npm run format - git diff --exit-code + - name: 'Check lockfile' + run: 'npm run check:lockfile' - - name: 'Run linter' - run: |- - npm run lint:ci + - name: 'Install linters' + run: 'node scripts/lint.js --setup' - - name: 'Run linter on integration tests' - run: |- - npx eslint integration-tests --max-warnings 0 + - name: 'Run ESLint' + run: 'node scripts/lint.js --eslint' - - name: 'Run formatter on integration tests' - run: |- - npx prettier --check integration-tests - git diff --exit-code + - name: 'Run actionlint' + run: 'node scripts/lint.js --actionlint' - - name: 'Build project' - run: |- - npm run build - - - name: 'Run type check' - run: |- - npm run typecheck - - # - # Lint: Shell - # - lint_shell: - name: 'Lint (Shell)' - runs-on: 'ubuntu-latest' - steps: - - name: 'Checkout' - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 - with: - fetch-depth: 1 - - - name: 'Install shellcheck' - run: |- - mkdir -p "${RUNNER_TEMP}/shellcheck" - curl -sSLo "${RUNNER_TEMP}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" - tar -xf "${RUNNER_TEMP}/.shellcheck.txz" -C "${RUNNER_TEMP}/shellcheck" --strip-components=1 - echo "${RUNNER_TEMP}/shellcheck" >> "${GITHUB_PATH}" - - - name: 'Install shellcheck problem matcher' - run: |- - cat > "${RUNNER_TEMP}/shellcheck/problem-matcher-lint-shell.json" <<"EOF" - { - "problemMatcher": [ - { - "owner": "lint_shell", - "pattern": [ - { - "regexp": "^(.*):(\\\\d+):(\\\\d+):\\\\s+(?:fatal\\\\s+)?(warning|error):\\\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5 - } - ] - } - ] - } - EOF - echo "::add-matcher::${RUNNER_TEMP}/shellcheck/problem-matcher-lint-shell.json" - - # Note that only warning and error severity show up in the github files - # page. So we replace 'style' and 'note' with 'warning' to make it show - # up. - # - # We also try and find all bash scripts even if they don't have an - # explicit extension. - # - # We explicitly ignore the following rules: - # - # - SC2002: This rule suggests using "cmd < file" instead of "cat | cmd". - # While < is more efficient, pipes are much more readable and expected. - # - # - SC2129: This rule suggests grouping multiple writes to a file in - # braces like "{ cmd1; cmd2; } >> file". This is unexpected and less - # readable. - # - # - SC2310: This is an optional warning that only appears with "set -e" - # and when a command is used as a conditional. - name: 'Run shellcheck' - run: |- - git ls-files | grep -E '^([^.]+|.*\.(sh|zsh|bash))$' | xargs file --mime-type \ - | grep "text/x-shellscript" | awk '{ print substr($1, 1, length($1)-1) }' \ - | xargs shellcheck \ - --check-sourced \ - --enable=all \ - --exclude=SC2002,SC2129,SC2310 \ - --severity=style \ - --format=gcc \ - --color=never | sed -e 's/note:/warning:/g' -e 's/style:/warning:/g' - - # - # Lint: YAML - # - lint_yaml: - name: 'Lint (YAML)' - runs-on: 'ubuntu-latest' - steps: - - name: 'Checkout' - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 - with: - fetch-depth: 1 - - - name: 'Setup Python' - uses: 'actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065' # ratchet:actions/setup-python@v5 - with: - python-version: '3' - - - name: 'Install yamllint' - run: |- - pip install --user "yamllint==${YAMLLINT_VERSION}" + run: 'node scripts/lint.js --shellcheck' - name: 'Run yamllint' - run: |- - git ls-files | grep -E '\.(yaml|yml)' | xargs yamllint --format github + run: 'node scripts/lint.js --yamllint' - # - # Lint: All - # - # This is a virtual job that other jobs depend on to wait for all linters to - # finish. It's also used to ensure linting happens on CI via required - # workflows. - lint: - name: 'Lint' - needs: - - 'lint_github_actions' - - 'lint_javascript' - - 'lint_shell' - - 'lint_yaml' - runs-on: 'ubuntu-latest' - steps: - - run: |- - echo 'All linters finished!' + - name: 'Run Prettier' + run: 'node scripts/lint.js --prettier' - # - # Test: Node - # - test: - name: 'Test' - runs-on: '${{ matrix.os }}' + test_linux: + name: 'Test (Linux)' + runs-on: 'gemini-cli-ubuntu-16-core' needs: - 'lint' permissions: @@ -256,12 +76,7 @@ jobs: checks: 'write' pull-requests: 'write' strategy: - fail-fast: false # So we can see all test failures matrix: - os: - - 'macos-latest' - - 'ubuntu-latest' - - 'windows-latest' node-version: - '20.x' - '22.x' @@ -277,18 +92,19 @@ jobs: cache: 'npm' - name: 'Build project' - run: |- - npm run build + run: 'npm run build' - name: 'Install dependencies for testing' - run: |- - npm ci + run: 'npm ci' - name: 'Run tests and generate reports' env: NO_COLOR: true run: 'npm run test:ci' + - 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) }} @@ -304,7 +120,67 @@ jobs: ${{ 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-${{ matrix.node-version }}-${{ matrix.os }}' + name: 'test-results-fork-${{ matrix.node-version }}-${{ runner.os }}' + path: 'packages/*/junit.xml' + + test_slow_platforms: + name: 'Slow Test - Mac' + runs-on: '${{ matrix.os }}' + needs: + - 'lint' + permissions: + contents: 'read' + checks: 'write' + pull-requests: 'write' + continue-on-error: true + strategy: + matrix: + os: + - 'macos-latest' + node-version: + - '20.x' + - '22.x' + - '24.x' + 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: 'npm run test:ci -- --coverage.enabled=false' + + - 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 ${{ matrix.node-version }})' + 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-${{ matrix.node-version }}-${{ runner.os }}' path: 'packages/*/junit.xml' - name: 'Upload coverage reports' @@ -315,47 +191,9 @@ jobs: name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}' path: 'packages/*/coverage' - post_coverage_comment: - name: 'Post Coverage Comment' - runs-on: 'ubuntu-latest' - needs: 'test' - if: |- - ${{ always() && github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name == github.repository) }} - continue-on-error: true - permissions: - contents: 'read' # For checkout - pull-requests: 'write' # For commenting - strategy: - matrix: - # Reduce noise by only posting the comment once - os: - - 'ubuntu-latest' - node-version: - - '22.x' - steps: - - name: 'Checkout' - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 - - - name: 'Download coverage reports artifact' - uses: 'actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0' # ratchet:actions/download-artifact@v5 - with: - name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}' - path: 'coverage_artifact' # Download to a specific directory - - - name: 'Post Coverage Comment using Composite Action' - uses: './.github/actions/post-coverage-comment' # Path to the composite action directory - with: - cli_json_file: 'coverage_artifact/cli/coverage/coverage-summary.json' - core_json_file: 'coverage_artifact/core/coverage/coverage-summary.json' - cli_full_text_summary_file: 'coverage_artifact/cli/coverage/full-text-summary.txt' - core_full_text_summary_file: 'coverage_artifact/core/coverage/full-text-summary.txt' - node_version: '${{ matrix.node-version }}' - os: '${{ matrix.os }}' - github_token: '${{ secrets.GITHUB_TOKEN }}' - codeql: name: 'CodeQL' - runs-on: 'ubuntu-latest' + runs-on: 'gemini-cli-ubuntu-16-core' permissions: actions: 'read' contents: 'read' @@ -375,9 +213,8 @@ jobs: # Check for changes in bundle size. bundle_size: name: 'Check Bundle Size' - if: |- - ${{ github.event_name != 'merge_group' }} - runs-on: 'ubuntu-latest' + if: "github.event_name == 'pull_request'" + runs-on: 'gemini-cli-ubuntu-16-core' permissions: contents: 'read' # For checkout pull-requests: 'write' # For commenting @@ -395,3 +232,79 @@ jobs: minimum-change-threshold: '1000' compression: 'none' clean-script: 'clean' + + test_windows: + name: 'Slow Test - Win' + runs-on: 'gemini-cli-windows-16-core' + continue-on-error: true + + steps: + - name: 'Checkout' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 + + - 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: 'npm run test:ci -- --coverage.enabled=false' + shell: 'pwsh' + + ci: + name: 'CI' + if: 'always()' + needs: + - 'lint' + - 'test_linux' + - '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.test_linux.result }} != 'success' && ${{ needs.test_linux.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!" diff --git a/.github/workflows/create-patch-pr.yml b/.github/workflows/create-patch-pr.yml deleted file mode 100644 index 2ec6aed3eb..0000000000 --- a/.github/workflows/create-patch-pr.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: 'Create Patch 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 - -jobs: - create-patch: - runs-on: 'ubuntu-latest' - permissions: - contents: 'write' - pull-requests: 'write' - steps: - - name: 'Checkout' - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 - with: - 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: '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 Patch for Stable' - if: "github.event.inputs.channel == 'stable'" - env: - GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - run: 'node scripts/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=stable --dry-run=${{ github.event.inputs.dry_run }}' - - - name: 'Create Patch for Preview' - env: - GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - run: 'node scripts/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=${{ github.event.inputs.channel }} --dry-run=${{ github.event.inputs.dry_run }}' diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 19c54a8606..e95cdcdc96 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,4 +1,4 @@ -name: 'E2E Tests' +name: 'Testing: E2E' on: push: @@ -14,11 +14,15 @@ on: types: ['labeled'] merge_group: +concurrency: + group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' + cancel-in-progress: |- + ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }} + jobs: - e2e-test: - name: 'E2E Test (${{ matrix.os }}) - ${{ matrix.sandbox }}' - # This condition ensures the job runs for pushes to main, merge groups, - # PRs from the base repo, OR PRs from forks with the correct label. + build: + name: 'Build Project' + runs-on: 'gemini-cli-ubuntu-16-core' if: | github.event_name == 'push' || github.event_name == 'merge_group' || @@ -62,21 +66,11 @@ jobs: strategy: fail-fast: false matrix: - os: - - 'ubuntu-latest' - - 'macos-latest' - - 'gemini-cli-windows-16-core' sandbox: - 'sandbox:none' - 'sandbox:docker' node-version: - '20.x' - exclude: - # Docker tests are not supported on macOS or Windows - - os: 'macos-latest' - sandbox: 'sandbox:docker' - - os: 'gemini-cli-windows-16-core' - sandbox: 'sandbox:docker' steps: - name: 'Checkout (fork)' @@ -90,22 +84,22 @@ jobs: uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 if: "github.event_name != 'pull_request_target'" + - name: 'Download build artifacts' + uses: 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' + with: + name: 'build-artifacts-${{ github.run_id }}' + path: '.' + + - name: 'Extract build artifacts' + run: 'tar -xvf build-artifacts.tar' + - 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.os == 'ubuntu-latest' && matrix.sandbox == 'sandbox:docker' + if: "matrix.sandbox == 'sandbox:docker'" uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3 - name: 'Run E2E tests' @@ -114,6 +108,153 @@ jobs: KEEP_OUTPUT: 'true' SANDBOX: '${{ matrix.sandbox }}' VERBOSE: 'true' + GEMINI_SANDBOX: 'docker' shell: 'bash' - run: |- - npm run "test:integration:${SANDBOX}" + run: | + if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then + npm run build:sandbox + fi + npx vitest run --root ./integration-tests + + e2e_slow_platforms: + name: 'Slow E2E - Mac' + needs: + - 'build' + if: | + github.event_name == 'push' || + github.event_name == 'merge_group' || + (github.event.pull_request.head.repo.full_name == github.repository) || + (github.event.label.name == 'maintainer:e2e:ok') + runs-on: '${{ matrix.os }}' + continue-on-error: true + strategy: + fail-fast: false + matrix: + os: + - 'macos-latest' + node-version: + - '20.x' + + steps: + - name: 'Checkout (fork)' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 + if: "github.event_name == 'pull_request_target'" + with: + ref: '${{ github.event.pull_request.head.sha }}' + repository: '${{ github.event.pull_request.head.repo.full_name }}' + + - name: 'Checkout (internal)' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 + if: "github.event_name != 'pull_request_target'" + + - name: 'Download build artifacts' + uses: 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' + with: + name: 'build-artifacts-${{ github.run_id }}' + path: '.' + + - name: 'Extract build artifacts' + run: 'tar -xvf build-artifacts.tar' + + - name: 'Set up Node.js ${{ matrix.node-version }}' + uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4 + with: + node-version: '${{ matrix.node-version }}' + + - name: 'Fix rollup optional dependencies on macOS' + if: "runner.os == 'macOS'" + run: | + npm cache clean --force + npm install --no-save @rollup/rollup-darwin-arm64 || true + + - 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: 'npx vitest run --root ./integration-tests' + + e2e_windows: + name: 'Slow E2E - Win' + if: | + github.event_name == 'push' || + github.event_name == 'merge_group' || + (github.event.pull_request.head.repo.full_name == github.repository) || + (github.event.label.name == 'maintainer:e2e:ok') + runs-on: 'gemini-cli-windows-16-core' + continue-on-error: true + + steps: + - name: 'Checkout (fork)' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 + if: "github.event_name == 'pull_request_target'" + with: + ref: '${{ github.event.pull_request.head.sha }}' + repository: '${{ github.event.pull_request.head.repo.full_name }}' + + - name: 'Checkout (internal)' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 + if: "github.event_name != 'pull_request_target'" + + - 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: '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: 'npx vitest run --root ./integration-tests' + + e2e: + name: 'E2E' + if: | + always() && ( + github.event_name == 'push' || + github.event_name == 'merge_group' || + (github.event.pull_request.head.repo.full_name == github.repository) || + (github.event.label.name == 'maintainer:e2e:ok') + ) + needs: + - 'e2e_linux' + runs-on: 'gemini-cli-ubuntu-16-core' + steps: + - name: 'Check E2E test results' + run: | + if [[ ${{ needs.e2e_linux.result }} != 'success' ]]; then + echo "The required E2E test job failed." + exit 1 + fi + echo "All required E2E test jobs passed!" diff --git a/.github/workflows/patch-from-comment.yml b/.github/workflows/patch-from-comment.yml deleted file mode 100644 index 55065b5b1c..0000000000 --- a/.github/workflows/patch-from-comment.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: 'Patch from Comment' - -on: - issue_comment: - types: ['created'] - -jobs: - slash-command: - runs-on: 'ubuntu-latest' - steps: - - 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' - static-args: | - dry_run=false - - - name: 'Get PR Status' - id: 'pr_status' - if: "steps.slash_command.outputs.dispatched == 'true'" - 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'" - uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325' - with: - script: | - const args = JSON.parse('${{ steps.slash_command.outputs.command-arguments }}'); - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'create-patch-pr.yml', - ref: 'main', - inputs: { - commit: '${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}', - channel: args.channel, - dry_run: args.dry_run - } - }) - - - name: 'Comment on Failure' - if: "steps.pr_status.outputs.STATE != 'MERGED'" - uses: 'peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d' - with: - issue-number: '${{ github.event.issue.number }}' - body: | - :x: The `/patch` command failed. This pull request must be merged before a patch can be created. diff --git a/.github/workflows/patch-release.yml b/.github/workflows/patch-release.yml deleted file mode 100644 index 9a05bfad76..0000000000 --- a/.github/workflows/patch-release.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: 'Patch Release' - -on: - workflow_dispatch: - inputs: - type: - description: 'The type of release to patch from.' - required: true - type: 'choice' - options: - - 'stable' - - 'preview' - ref: - description: 'The branch or ref (full git sha) to release from.' - required: true - type: 'string' - default: 'main' - 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 - -jobs: - release: - runs-on: 'ubuntu-latest' - environment: - name: 'production-release' - url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.version.outputs.RELEASE_TAG }}' - if: |- - ${{ github.repository == 'google-gemini/gemini-cli' }} - permissions: - contents: 'write' - packages: 'write' - id-token: 'write' - issues: 'write' # For creating issues on failure - outputs: - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}' - - steps: - - name: 'Checkout' - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 - with: - ref: '${{ github.event.inputs.ref || github.sha }}' - 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: 'Install Dependencies' - run: |- - npm ci - - - name: 'Get the version' - id: 'version' - env: - GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - run: |- - VERSION_JSON="$(node scripts/get-release-version.js --type=patch --patch-from=${{ github.event.inputs.type }})" - echo "${VERSION_JSON}" - echo "RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag)" >> "${GITHUB_OUTPUT}" - echo "RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}" - echo "NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag)" >> "${GITHUB_OUTPUT}" - echo "PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}" - - - name: 'Print Calculated Version' - run: |- - echo "Calculated version: ${{ steps.version.outputs.RELEASE_VERSION }}" - - - name: 'Run Tests' - uses: './.github/actions/run-tests' - with: - force_skip_tests: '${{ github.event.inputs.force_skip_tests }}' - gemini_api_key: '${{ secrets.GEMINI_API_KEY }}' - - - name: 'Publish Release' - uses: './.github/actions/publish-release' - with: - release-version: '${{ steps.version.outputs.RELEASE_VERSION }}' - release-tag: '${{ steps.version.outputs.RELEASE_TAG }}' - npm-tag: '${{ steps.version.outputs.NPM_TAG }}' - wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}' - wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}' - github-token: '${{ secrets.GITHUB_TOKEN }}' - dry-run: '${{ github.event.inputs.dry_run }}' - previous-tag: '${{ steps.version.outputs.PREVIOUS_TAG }}' diff --git a/.github/workflows/release-change-tags.yml b/.github/workflows/release-change-tags.yml new file mode 100644 index 0000000000..50c2fd6c02 --- /dev/null +++ b/.github/workflows/release-change-tags.yml @@ -0,0 +1,56 @@ +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: + - 'latest' + - 'preview' + - 'nightly' + 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 + +jobs: + change-tags: + runs-on: 'ubuntu-latest' + permissions: + packages: 'write' + issues: 'write' + steps: + - name: 'Checkout repository' + uses: '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' + registry-url: 'https://wombat-dressing-room.appspot.com' + scope: '@google' + + - 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 }}' diff --git a/.github/workflows/release-manual.yml b/.github/workflows/release-manual.yml new file mode 100644 index 0000000000..6056e562cb --- /dev/null +++ b/.github/workflows/release-manual.yml @@ -0,0 +1,89 @@ +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: + - 'preview' + - 'nightly' + - 'latest' + - 'dev' + default: 'dev' + 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 and create a npm release only.' + required: false + type: 'boolean' + default: false + +jobs: + release: + runs-on: 'self-hosted' + permissions: + contents: 'write' + packages: 'write' + issues: 'write' + steps: + - name: 'Checkout' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' + with: + ref: '${{ github.event.inputs.ref }}' + fetch-depth: 0 + + - name: 'Setup Node.js' + uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: 'Install Dependencies' + run: 'npm ci' + + - name: 'Prepare Release Info' + id: 'release_info' + run: | + RELEASE_VERSION="${{ github.event.inputs.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 }}' + + - 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 }}' + github-token: '${{ secrets.GITHUB_TOKEN }}' + dry-run: '${{ github.event.inputs.dry_run }}' + previous-tag: '${{ steps.release_info.outputs.PREVIOUS_TAG }}' + skip-github-release: '${{ github.event.inputs.skip_github_release }}' diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/release-nightly.yml similarity index 84% rename from .github/workflows/nightly-release.yml rename to .github/workflows/release-nightly.yml index f0ca371671..65e75007a3 100644 --- a/.github/workflows/nightly-release.yml +++ b/.github/workflows/release-nightly.yml @@ -1,4 +1,4 @@ -name: 'Nightly Release' +name: 'Release: Nightly' on: schedule: @@ -25,7 +25,10 @@ jobs: release: runs-on: 'ubuntu-latest' permissions: + contents: 'write' + packages: 'write' issues: 'write' + pull-requests: 'write' steps: - name: 'Checkout' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' @@ -71,6 +74,16 @@ jobs: dry-run: '${{ github.event.inputs.dry_run }}' previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}' + - name: 'Create and Merge Pull Request' + 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.' + app-id: '${{ secrets.APP_ID }}' + private-key: '${{ secrets.PRIVATE_KEY }}' + dry-run: '${{ github.event.inputs.dry_run }}' + - name: 'Create Issue on Failure' if: '${{ failure() && github.event.inputs.dry_run == false }}' env: diff --git a/.github/workflows/release-patch-1-create-pr.yml b/.github/workflows/release-patch-1-create-pr.yml new file mode 100644 index 0000000000..43b6eef050 --- /dev/null +++ b/.github/workflows/release-patch-1-create-pr.yml @@ -0,0 +1,109 @@ +name: 'Release: Patch (1) Create 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' + +jobs: + create-patch: + runs-on: 'ubuntu-latest' + 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: 'Install Script Dependencies' + run: 'npm install yargs' + + - 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 }}' + permission-pull-requests: 'write' + permission-contents: 'write' + + - name: 'Configure Git User' + 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:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" + + - name: 'Create Patch' + id: 'create_patch' + env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + GH_TOKEN: '${{ steps.generate_token.outputs.token }}' + continue-on-error: true + run: | + # Capture output and display it in logs using tee + { + node scripts/releasing/create-patch-pr.js --commit=${{ github.event.inputs.commit }} --channel=${{ github.event.inputs.channel }} --dry-run=${{ github.event.inputs.dry_run }} + echo "EXIT_CODE=$?" >> "$GITHUB_OUTPUT" + } 2>&1 | tee >( + echo "LOG_CONTENT<> "$GITHUB_ENV" + cat >> "$GITHUB_ENV" + echo "EOF" >> "$GITHUB_ENV" + ) + + - 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 }}' + continue-on-error: true + run: | + git checkout '${{ github.event.inputs.ref }}' + node scripts/releasing/patch-create-comment.js + + - name: 'Fail Workflow if Main Task Failed' + if: 'always() && steps.create_patch.outputs.EXIT_CODE != 0' + run: | + echo "Patch creation failed with exit code: ${{ steps.create_patch.outputs.EXIT_CODE }}" + echo "Check the logs above and the comment posted to the original PR for details." + exit 1 diff --git a/.github/workflows/release-patch-2-trigger.yml b/.github/workflows/release-patch-2-trigger.yml new file mode 100644 index 0000000000..842a662c4f --- /dev/null +++ b/.github/workflows/release-patch-2-trigger.yml @@ -0,0 +1,72 @@ +name: 'Release: Patch (2) Trigger' + +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 patch-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 + +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' + 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 }}' + run: | + node scripts/releasing/patch-trigger.js diff --git a/.github/workflows/release-patch-3-release.yml b/.github/workflows/release-patch-3-release.yml new file mode 100644 index 0000000000..45627f34c9 --- /dev/null +++ b/.github/workflows/release-patch-3-release.yml @@ -0,0 +1,219 @@ +name: 'Release: Patch (3) Release' + +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' + +jobs: + release: + runs-on: 'ubuntu-latest' + permissions: + contents: 'write' + packages: '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: 'Install Dependencies' + working-directory: './release' + run: |- + npm ci + + - name: 'Print Inputs' + shell: 'bash' + run: |- + echo "${{ toJSON(inputs) }}" + + - name: 'Get Patch Version' + id: 'patch_version' + env: + GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + 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 --patch-from=${{ github.event.inputs.type }}) + 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 }}' + run: | + echo "๐Ÿ” Verifying no concurrent patch releases have occurred..." + + # Store original calculation for comparison + 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 }}" + + 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 --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 }}" + + - name: 'Run Tests' + uses: './.github/actions/run-tests' + with: + force_skip_tests: '${{ github.event.inputs.force_skip_tests }}' + 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 }}' + github-token: '${{ secrets.GITHUB_TOKEN }}' + dry-run: '${{ github.event.inputs.dry_run }}' + previous-tag: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}' + 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 'kind/bug,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 diff --git a/.github/workflows/release-patch-from-comment.yml b/.github/workflows/release-patch-from-comment.yml new file mode 100644 index 0000000000..a75f3a0f83 --- /dev/null +++ b/.github/workflows/release-patch-from-comment.yml @@ -0,0 +1,187 @@ +name: 'Release: Patch 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 }}' + } + }); + + 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(',')); + } + + - 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: | + โœ… **Patch workflow(s) dispatched successfully!** + + **๐Ÿ“‹ 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 }} + + **๐Ÿ”— Track Progress:** + - [View patch workflows](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml) + - [This 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: | + โœ… **Patch workflow(s) dispatched successfully!** + + **๐Ÿ“‹ 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 }} + + **๐Ÿ”— Track Progress:** + - [View patch workflows](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml) + - [This 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: | + โŒ **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) diff --git a/.github/workflows/promote-release.yml b/.github/workflows/release-promote.yml similarity index 95% rename from .github/workflows/promote-release.yml rename to .github/workflows/release-promote.yml index e93f9cf191..df66474921 100644 --- a/.github/workflows/promote-release.yml +++ b/.github/workflows/release-promote.yml @@ -1,4 +1,4 @@ -name: 'Promote Release' +name: 'Release: Promote' on: workflow_dispatch: @@ -314,20 +314,15 @@ jobs: echo "Dry run enabled. Skipping push." fi - - name: 'Create and Approve Pull Request' - if: |- - ${{ github.event.inputs.dry_run == 'false' }} - env: - GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}' - run: | - gh pr create \ - --title "chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}" \ - --body "Automated version bump to prepare for the next nightly release." \ - --base "main" \ - --head "${BRANCH_NAME}" \ - --fill - gh pr merge --auto --squash + - 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.' + app-id: '${{ secrets.APP_ID }}' + private-key: '${{ secrets.PRIVATE_KEY }}' + dry-run: '${{ github.event.inputs.dry_run }}' - name: 'Create Issue on Failure' if: '${{ failure() && github.event.inputs.dry_run == false }}' diff --git a/.github/workflows/release-sandbox.yml b/.github/workflows/release-sandbox.yml new file mode 100644 index 0000000000..e0c224e573 --- /dev/null +++ b/.github/workflows/release-sandbox.yml @@ -0,0 +1,33 @@ +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: + runs-on: 'ubuntu-latest' + 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: + github-actor: '${{ github.actor }}' + github-secret: '${{ secrets.GITHUB_TOKEN }}' + github-sha: '${{ github.event.inputs.ref || github.sha }}' + github-ref-name: '${{github.event.inputs.ref}}' + dry-run: '${{ github.event.inputs.dry-run }}' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 4b29a925ae..0000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,202 +0,0 @@ -name: 'Release' - -on: - workflow_dispatch: - inputs: - version: - description: 'The version to release (e.g., v0.1.11).' - required: true - type: 'string' - ref: - description: 'The branch or ref (full git sha) to release from.' - required: true - type: 'string' - default: 'main' - 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 - -jobs: - release: - runs-on: 'ubuntu-latest' - environment: - name: 'production-release' - url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.version.outputs.RELEASE_TAG }}' - if: |- - ${{ github.repository == 'google-gemini/gemini-cli' }} - permissions: - contents: 'write' - packages: 'write' - id-token: 'write' - issues: 'write' # For creating issues on failure - outputs: - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}' - - steps: - - name: 'Checkout' - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 - with: - ref: '${{ github.event.inputs.ref || github.sha }}' - fetch-depth: 0 - - - name: 'Set booleans for simplified logic' - env: - DRY_RUN_INPUT: '${{ github.event.inputs.dry_run }}' - id: 'vars' - run: |- - is_dry_run="false" - if [[ "${DRY_RUN_INPUT}" == "true" ]]; then - is_dry_run="true" - fi - echo "is_dry_run=${is_dry_run}" >> "${GITHUB_OUTPUT}" - - - name: 'Setup 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: 'Get the version' - id: 'version' - run: |- - RELEASE_TAG="${{ inputs.version }}" - # The version for npm should not have the 'v' prefix. - RELEASE_VERSION="${RELEASE_TAG#v}" - NPM_TAG="latest" - if [[ "${RELEASE_TAG}" == *"preview"* ]]; then - NPM_TAG="preview" - fi - PREVIOUS_TAG=$(git describe --tags --abbrev=0) - 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: 'Run Tests' - if: |- - ${{ github.event.inputs.force_skip_tests != 'true' }} - env: - GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}' - run: |- - npm run preflight - npm run test:integration:sandbox:none - npm run test:integration:sandbox:docker - - - 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 release branch' - id: 'release_branch' - env: - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}' - run: |- - BRANCH_NAME="release/${RELEASE_TAG}" - git switch -c "${BRANCH_NAME}" - echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}" - - - name: 'Update package versions' - env: - RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}' - run: |- - npm run release:version "${RELEASE_VERSION}" - - - name: 'Commit and Conditionally Push package versions' - env: - BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}' - IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}' - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}' - run: |- - git add package.json npm-shrinkwrap.json packages/*/package.json - git commit -m "chore(release): ${RELEASE_TAG}" - if [[ "${IS_DRY_RUN}" == "false" ]]; then - echo "Pushing release branch to remote..." - git push --set-upstream origin "${BRANCH_NAME}" --follow-tags - else - echo "Dry run enabled. Skipping push." - fi - - - name: 'Build and Prepare Packages' - run: |- - npm run build:packages - npm run prepare:package - - - name: 'Configure npm for publishing' - uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - registry-url: 'https://wombat-dressing-room.appspot.com' - scope: '@google' - - - name: 'Publish @google/gemini-cli-core' - env: - IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}' - NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CORE }}' - NPM_TAG: '${{ steps.version.outputs.NPM_TAG }}' - run: |- - npm publish \ - --dry-run="${IS_DRY_RUN}" \ - --workspace="@google/gemini-cli-core" \ - --tag="${NPM_TAG}" - - - name: 'Install latest core package' - if: |- - ${{ steps.vars.outputs.is_dry_run == 'false' }} - env: - RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}' - run: |- - npm install "@google/gemini-cli-core@${RELEASE_VERSION}" \ - --workspace="@google/gemini-cli" \ - --save-exact - - - name: 'Publish @google/gemini-cli' - env: - IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}' - NODE_AUTH_TOKEN: '${{ secrets.WOMBAT_TOKEN_CLI }}' - NPM_TAG: '${{ steps.version.outputs.NPM_TAG }}' - run: |- - npm publish \ - --dry-run="${IS_DRY_RUN}" \ - --workspace="@google/gemini-cli" \ - --tag="${NPM_TAG}" - - - name: 'Create GitHub Release and Tag' - if: |- - ${{ steps.vars.outputs.is_dry_run == 'false' }} - env: - GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}' - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}' - PREVIOUS_TAG: '${{ steps.version.outputs.PREVIOUS_TAG }}' - run: |- - gh release create "${RELEASE_TAG}" \ - bundle/gemini.js \ - --target "$RELEASE_BRANCH" \ - --title "Release ${RELEASE_TAG}" \ - --notes-start-tag "$PREVIOUS_TAG" \ - --generate-notes - - - name: 'Create Issue on Failure' - if: |- - ${{ failure() }} - env: - GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }} || "N/A"' - DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' - run: |- - gh issue create \ - --title "Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \ - --body "The release workflow failed. See the full run for details: ${DETAILS_URL}" \ - --label "kind/bug,release-failure,priority/p0" diff --git a/.github/workflows/trigger-patch-release.yml b/.github/workflows/trigger-patch-release.yml deleted file mode 100644 index 4270111bbd..0000000000 --- a/.github/workflows/trigger-patch-release.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: 'Trigger Patch Release' - -on: - pull_request: - types: - - 'closed' - -jobs: - trigger-patch-release: - if: "github.event.pull_request.merged == true && startsWith(github.head_ref, 'hotfix/')" - runs-on: 'ubuntu-latest' - steps: - - name: 'Trigger Patch Release' - uses: 'actions/github-script@00f12e3e20659f42342b1c0226afda7f7c042325' - with: - script: | - const body = context.payload.pull_request.body; - const isDryRun = body.includes('[DRY RUN]'); - const ref = context.payload.pull_request.base.ref; - const channel = ref.includes('preview') ? 'preview' : 'stable'; - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'patch-release.yml', - ref: ref, - inputs: { - type: channel, - dry_run: isDryRun.toString() - } - }) diff --git a/.github/workflows/verify-release.yml b/.github/workflows/verify-release.yml new file mode 100644 index 0000000000..0ebacd0d3f --- /dev/null +++ b/.github/workflows/verify-release.yml @@ -0,0 +1,31 @@ +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-package: + description: 'NPM package to verify' + required: true + type: 'string' + default: '@google/gemini-cli@latest' + ref: + description: 'The branch, tag, or SHA to release from.' + required: false + type: 'string' + default: 'main' + +jobs: + build: + runs-on: 'ubuntu-latest' + steps: + - uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' + - name: 'Verify release' + uses: './.github/actions/verify-release' + with: + npm-package: '${github.event.inputs.npm-package}' + expected-version: '${github.event.inputs.version}' + ref: '${github.event.inputs.ref}'