summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormatt korwel <[email protected]>2025-07-05 13:58:59 -0700
committerGitHub <[email protected]>2025-07-05 20:58:59 +0000
commita7256f630c7c9335ccd7a41e97c9322c0a33ea67 (patch)
treef1bdac3ec3d67171717de60032cf3dbbf24f437b
parent4be32d1f73bc2d9e4d22a71eee7b142e9b0aa577 (diff)
Relase: Clean up and condensing (#3321)
-rw-r--r--.gcp/publish-dry-run.yaml31
-rw-r--r--.gcp/release.yaml150
-rw-r--r--.github/workflows/release.yml120
-rw-r--r--.github/workflows/scheduled-nightly-release.yml74
-rw-r--r--docs/npm.md54
-rw-r--r--integration-tests/list_directory.test.js6
-rw-r--r--package-lock.json62
-rw-r--r--package.json7
-rw-r--r--scripts/get-release-version.js89
-rw-r--r--scripts/tag-release.js58
-rw-r--r--scripts/tests/get-release-version.test.js108
-rw-r--r--scripts/tests/test-setup.ts12
-rw-r--r--scripts/tests/vitest.config.ts20
13 files changed, 349 insertions, 442 deletions
diff --git a/.gcp/publish-dry-run.yaml b/.gcp/publish-dry-run.yaml
deleted file mode 100644
index 87d19fa1..00000000
--- a/.gcp/publish-dry-run.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-steps:
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- entrypoint: 'npm'
- args: ['install']
-
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- entrypoint: 'npm'
- args: ['run', 'auth']
-
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- entrypoint: 'npm'
- args:
- [
- 'run',
- 'prerelease:version',
- '--workspaces',
- '--',
- '--suffix="$SHORT_SHA.$_REVISION"',
- ]
-
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- entrypoint: 'npm'
- args: ['run', 'prerelease:deps', '--workspaces']
-
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- entrypoint: 'npm'
- args:
- ['publish', '--tag=head', '--dry-run', '--workspace=@google/gemini-cli']
-
-options:
- defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET
diff --git a/.gcp/release.yaml b/.gcp/release.yaml
deleted file mode 100644
index ad2c373a..00000000
--- a/.gcp/release.yaml
+++ /dev/null
@@ -1,150 +0,0 @@
-steps:
- # Step 1: Install root dependencies (includes workspaces)
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Install Dependencies'
- entrypoint: 'npm'
- args: ['install']
-
- # Step 2: Update version in root package.json
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Set version in workspace root'
- entrypoint: 'bash'
- args:
- - -c # Use bash -c to allow for command substitution and string manipulation
- - |
- current_version=$(npm pkg get version | sed 's/"//g')
- if [ "$_OFFICIAL_RELEASE" = "true" ]; then
- new_version="$current_version"
- else
- new_version="${current_version}-rc.$_REVISION"
- fi
- npm pkg set "version=${new_version}"
- echo "Set root package.json version to: ${new_version}"
-
- # Step 3: Binds the package versions to the version in the repo root's package.json
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Bind package versions to workspace root'
- entrypoint: 'npm'
- args: ['run', 'prerelease:dev'] # This will run prerelease:version and prerelease:deps
-
- # Step 4: Authenticate for Docker (so we can push images to the artifact registry)
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Authenticate docker'
- entrypoint: 'npm'
- args: ['run', 'auth']
-
- # Step 5: Build workspace packages
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Build packages'
- entrypoint: 'npm'
- args: ['run', 'build:packages']
-
- # Step 6: Prepare CLI package.json for publishing
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Prepare @google/gemini-cli and @google/gemini-cli-core packages'
- entrypoint: 'npm'
- args: ['run', 'prepare:packages']
- env:
- - 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
- - 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
- - 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
-
- # Step 7: Build sandbox container image
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Build sandbox Docker image'
- entrypoint: 'npm'
- args: ['run', 'build:sandbox:fast']
- env:
- - 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
- - 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
- - 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
-
- # Step 8: Publish sandbox container image
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Publish sandbox Docker image'
- entrypoint: 'npm'
- args: ['run', 'publish:sandbox']
- env:
- - 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
- - 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
- - 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
-
- # Pre-Step 9: authenticate to our intermediate npm registry
- # NOTE: when running locally, run this instead (from the `packages/core` directory):
- # - `npm login --registry https://wombat-dressing-room.appspot.com`
- # - use a 24hr token
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Setup @google/gemini-cli-core auth token for publishing'
- entrypoint: 'bash'
- args:
- - -c
- - |
- echo "//wombat-dressing-room.appspot.com/:_authToken=$$CORE_PACKAGE_PUBLISH_TOKEN" > $$HOME/.npmrc
- secretEnv: ['CORE_PACKAGE_PUBLISH_TOKEN']
-
- # Step 9: Publish @google/gemini-cli-core to NPM
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Publish @google/gemini-cli-core package'
- entrypoint: 'bash'
- args:
- - -c
- - |
- if [ "$_OFFICIAL_RELEASE" = "true" ]; then
- npm publish --workspace=@google/gemini-cli-core --tag=latest
- else
- npm publish --workspace=@google/gemini-cli-core --tag=rc
- fi
- env:
- - 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
- - 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
- - 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
-
- # Pre-Step 10: authenticate to our intermediate npm registry
- # NOTE: when running locally, run this instead (from the `packages/cli` directory)
- # - `npm login --registry https://wombat-dressing-room.appspot.com`
- # - use a 24hr token
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Setup @google/gemini-cli auth token for publishing'
- entrypoint: 'bash'
- args:
- - -c
- - |
- echo "//wombat-dressing-room.appspot.com/:_authToken=$$CLI_PACKAGE_PUBLISH_TOKEN" > $$HOME/.npmrc
- secretEnv: ['CLI_PACKAGE_PUBLISH_TOKEN']
-
- # Step 10: Publish @google/gemini-cli to NPM
- - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
- id: 'Publish @google/gemini-cli package'
- entrypoint: 'bash'
- args:
- - -c
- - |
- if [ "$_OFFICIAL_RELEASE" = "true" ]; then
- npm publish --workspace=@google/gemini-cli --tag=latest
- else
- npm publish --workspace=@google/gemini-cli --tag=rc
- fi
- env:
- - 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
- - 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
- - 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
-
-options:
- defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET
- dynamicSubstitutions: true
-
-availableSecrets:
- secretManager:
- - versionName: ${_CLI_PACKAGE_WOMBAT_TOKEN_RESOURCE_NAME}
- env: 'CLI_PACKAGE_PUBLISH_TOKEN'
- - versionName: ${_CORE_PACKAGE_WOMBAT_TOKEN_RESOURCE_NAME}
- env: 'CORE_PACKAGE_PUBLISH_TOKEN'
-
-substitutions:
- _REVISION: '0'
- _OFFICIAL_RELEASE: 'false'
- _CONTAINER_TOOL: 'docker'
- _SANDBOX_IMAGE_REGISTRY: ''
- _SANDBOX_IMAGE_NAME: ''
- _CLI_PACKAGE_WOMBAT_TOKEN_RESOURCE_NAME: ''
- _CORE_PACKAGE_WOMBAT_TOKEN_RESOURCE_NAME: ''
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 524dfaff..2ffbe4ee 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,14 +1,14 @@
name: Release
on:
- push:
- tags:
- - 'v*.*.*'
+ schedule:
+ # Runs every day at midnight UTC for the nightly release.
+ - cron: '0 0 * * *'
workflow_dispatch:
inputs:
version:
description: 'The version to release (e.g., v0.1.11). Required for manual patch releases.'
- required: true
+ required: false # Not required for scheduled runs
type: string
ref:
description: 'The branch or ref to release from.'
@@ -20,24 +20,54 @@ on:
required: true
type: boolean
default: true
+ create_nightly_release:
+ description: 'Simulate a scheduled nightly release. If true, the version input is ignored.'
+ required: false
+ type: boolean
+ default: false
+ force_skip_tests:
+ description: 'If true, skip the "Run Tests" step. This is only applicable for nightly releases.'
+ 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 code
uses: actions/checkout@v4
with:
- # For manual runs, checkout the specified ref (e.g., main). For tag pushes, checkout the tag itself.
- ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
+ ref: ${{ github.sha }}
fetch-depth: 0
+ - name: Set booleans for simplified logic
+ id: vars
+ run: |
+ is_nightly="false"
+ if [[ "${{ github.event_name }}" == "schedule" || "${{ github.event.inputs.create_nightly_release }}" == "true" ]]; then
+ is_nightly="true"
+ fi
+ echo "is_nightly=${is_nightly}" >> $GITHUB_OUTPUT
+
+ is_dry_run="false"
+ if [[ "${{ github.event.inputs.dry_run }}" == "true" ]]; then
+ is_dry_run="true"
+ fi
+ echo "is_dry_run=${is_dry_run}" >> $GITHUB_OUTPUT
+
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -50,39 +80,27 @@ jobs:
- name: Get the version
id: version
run: |
- echo "Workflow triggered by: ${{ github.event_name }}"
- if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
- echo "Input ref: ${{ inputs.ref }}"
- echo "Input version: ${{ inputs.version }}"
- RELEASE_TAG=${{ inputs.version }}
- else
- echo "Triggering ref: ${{ github.ref }}"
- RELEASE_TAG=${GITHUB_REF_NAME}
- fi
-
- echo "---"
- echo "Initial RELEASE_TAG: ${RELEASE_TAG}"
-
- # Validate that the tag starts with 'v' and follows semver
- if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then
- echo "Error: Version must be in the format vX.Y.Z, vX.Y.Z-prerelease, or vX.Y.Z+buildmeta"
- exit 1
- fi
-
- RELEASE_VERSION="${RELEASE_TAG#v}"
- if [[ $RELEASE_VERSION == *-* ]]; then
- NPM_TAG=$(echo $RELEASE_VERSION | cut -d'-' -f2 | cut -d'.' -f1)
- else
- NPM_TAG="latest"
- fi
+ VERSION_JSON=$(node scripts/get-release-version.js)
+ 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
+ env:
+ IS_NIGHTLY: ${{ steps.vars.outputs.is_nightly }}
+ MANUAL_VERSION: ${{ inputs.version }}
- echo "Finalized RELEASE_VERSION: ${RELEASE_VERSION}"
- echo "Finalized NPM_TAG: ${NPM_TAG}"
- echo "---"
+ - name: Run Tests
+ if: github.event.inputs.force_skip_tests != 'true'
+ run: |
+ npm run preflight
+ npm run test:integration:sandbox:none
+ npm run test:integration:sandbox:docker
+ env:
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
- echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_OUTPUT
- echo "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_OUTPUT
- echo "NPM_TAG=${NPM_TAG}" >> $GITHUB_OUTPUT
+ - name: Configure Git User
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Create and switch to a release branch
id: release_branch
@@ -95,13 +113,16 @@ jobs:
run: |
npm run release:version ${{ steps.version.outputs.RELEASE_VERSION }}
- - name: Commit package versions
+ - name: Commit and Conditionally Push package versions
run: |
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json packages/*/package.json
git commit -m "chore(release): ${{ steps.version.outputs.RELEASE_TAG }}"
- git push --set-upstream origin ${{ steps.release_branch.outputs.BRANCH_NAME }} --follow-tags
+ if [[ "${{ steps.vars.outputs.is_dry_run }}" == "false" ]]; then
+ echo "Pushing release branch to remote..."
+ git push --set-upstream origin ${{ steps.release_branch.outputs.BRANCH_NAME }} --follow-tags
+ else
+ echo "Dry run enabled. Skipping push."
+ fi
- name: Build and Prepare Packages
run: |
@@ -116,20 +137,21 @@ jobs:
scope: '@google'
- name: Publish @google/gemini-cli-core
- run: npm publish --workspace=@google/gemini-cli-core --tag=${{ steps.version.outputs.NPM_TAG }} ${{ inputs.dry_run && '--dry-run' || '' }}
+ run: npm publish --workspace=@google/gemini-cli-core --tag=${{ steps.version.outputs.NPM_TAG }} ${{ steps.vars.outputs.is_dry_run == 'true' && '--dry-run' || '' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.WOMBAT_TOKEN_CORE }}
- name: Install latest core package
+ if: steps.vars.outputs.is_dry_run == 'false'
run: npm install @google/gemini-cli-core@${{ steps.version.outputs.NPM_TAG }} --workspace=@google/gemini-cli --save-exact
- name: Publish @google/gemini-cli
- run: npm publish --workspace=@google/gemini-cli --tag=${{ steps.version.outputs.NPM_TAG }} ${{ inputs.dry_run && '--dry-run' || '' }}
+ run: npm publish --workspace=@google/gemini-cli --tag=${{ steps.version.outputs.NPM_TAG }} ${{ steps.vars.outputs.is_dry_run == 'true' && '--dry-run' || '' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.WOMBAT_TOKEN_CLI }}
- name: Create GitHub Release and Tag
- if: '!inputs.dry_run'
+ if: ${{ steps.vars.outputs.is_dry_run == 'false' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_BRANCH: ${{ steps.release_branch.outputs.BRANCH_NAME }}
@@ -139,3 +161,13 @@ jobs:
--target "$RELEASE_BRANCH" \
--title "Release ${{ steps.version.outputs.RELEASE_TAG }}" \
--generate-notes
+
+ - name: Create Issue on Failure
+ if: failure()
+ run: |
+ gh issue create \
+ --title "Release Failed for ${{ steps.version.outputs.RELEASE_TAG || 'N/A' }} on $(date +'%Y-%m-%d')" \
+ --body "The release workflow failed. See the full run for details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
+ --label "type: bug,release-failure"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/scheduled-nightly-release.yml b/.github/workflows/scheduled-nightly-release.yml
deleted file mode 100644
index 657416b1..00000000
--- a/.github/workflows/scheduled-nightly-release.yml
+++ /dev/null
@@ -1,74 +0,0 @@
-name: Scheduled Nightly Release
-
-on:
- schedule:
- # Runs every day at midnight UTC.
- - cron: '0 0 * * *'
- workflow_dispatch:
-
-jobs:
- nightly-release:
- runs-on: ubuntu-latest
- permissions:
- contents: write
- issues: write
-
- steps:
- - name: Checkout main branch
- uses: actions/checkout@v4
- with:
- ref: 'main'
- fetch-depth: 0 # Fetch all history for git tags
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
- cache: 'npm'
-
- - name: Install Dependencies
- run: npm ci
-
- - name: Run Preflight Checks
- run: npm run preflight
-
- - name: Run Integration Tests (without Docker)
- uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08
- env:
- GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
- with:
- timeout_minutes: 10
- max_attempts: 3
- retry_wait_seconds: 30
- command: npm run test:integration:sandbox:none
-
- - name: Run Integration Tests (with Docker)
- uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08
- env:
- GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
- with:
- timeout_minutes: 10
- max_attempts: 3
- retry_wait_seconds: 30
- command: npm run test:integration:sandbox:docker
-
- - name: Configure Git User
- run: |
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
-
- - name: Create and Push Nightly Tag
- if: success()
- run: npm run tag:release:nightly
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Create Issue on Failure
- if: failure()
- run: |
- gh issue create \
- --title "Nightly Release Failed on $(date +'%Y-%m-%d')" \
- --body "The scheduled nightly release workflow failed. See the full run for details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
- --label "type: bug,nightly-failure"
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/docs/npm.md b/docs/npm.md
index e39debc1..5e3b388f 100644
--- a/docs/npm.md
+++ b/docs/npm.md
@@ -18,21 +18,18 @@ This package is not bundled. When it is published, it is published as a standard
This project follows a structured release process to ensure that all packages are versioned and published correctly. The process is designed to be as automated as possible.
-## Current Theory
-
-For most all changes, simply patching the minor version is acceptable. We can and should release frequently; the more often we release the easier it is to tell what change broke something. Developers are encouraged to push a release as described below after their branch merges. I also think I'm open to doing the release publishing steps as a part of an existing PR, though this could have more churn if others are also releasing and version numbers change frequently.
-
## How To Release
-Releasing a new version is as simple as creating and pushing a new Git tag. The tag must follow semantic versioning and be prefixed with `v`, for example `v0.2.0` or `v1.0.0-alpha.1`. From the branch you want to release from, run the following commands:
+Releases are managed through the [release.yml](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) GitHub Actions workflow. To perform a manual release for a patch or hotfix:
-```bash
-# Create the new tag (e.g., v0.2.0)
-# Optional use git log to find an older commit sha to tag
-git tag v0.2.0 <optional sha>
-# Push the tag to the remote repository to trigger the release
-git push origin v0.2.0
-```
+1. Navigate to the **Actions** tab of the repository.
+2. Select the **Release** workflow from the list.
+3. Click the **Run workflow** dropdown button.
+4. Fill in the required inputs:
+ - **Version**: The exact version to release (e.g., `v0.2.1`).
+ - **Ref**: The branch or commit SHA to release from (defaults to `main`).
+ - **Dry Run**: Leave as `true` to test the workflow without publishing, or set to `false` to perform a live release.
+5. Click **Run workflow**.
## Nightly Releases
@@ -40,14 +37,14 @@ In addition to manual releases, this project has an automated nightly release pr
### Process
-Every night at midnight UTC, the [Scheduled Nightly Release workflow](https://github.com/google-gemini/gemini-cli/actions/workflows/scheduled-nightly-release.yml) runs automatically. It performs the following steps:
+Every night at midnight UTC, the [Release workflow](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) runs automatically on a schedule. It performs the following steps:
1. Checks out the latest code from the `main` branch.
2. Installs all dependencies.
-3. Runs the full suite of `preflight` checks (linting, type-checking, etc.).
-4. Runs the integration tests, both with and without Docker. The tests are automatically retried up to three times to handle any flakiness.
-5. If all checks and tests succeed, it runs the `npm run tag:release:nightly` script. This script creates and pushes a new annotated Git tag with the format `v<version>+nightly.<ddmmyy>.<sha>`.
-6. Pushing this tag triggers the main [release workflow](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml), which publishes the package to npm with the `nightly` tag.
+3. Runs the full suite of `preflight` checks and integration tests.
+4. If all tests succeed, it calculates the next nightly version number (e.g., `v0.2.1-nightly.20230101`).
+5. It then builds and publishes the packages to npm with the `nightly` dist-tag.
+6. Finally, it creates a GitHub Release for the nightly version.
### Failure Handling
@@ -61,32 +58,11 @@ To install the latest nightly build, use the `@nightly` tag:
npm install -g @google/gemini-cli@nightly
```
-The high-level process is:
-
-1. Ensure your local branch `main` or `release-xxx` if hotfixing a previous release is up-to-date with the remote repository.
-1. Decide on the new version number based on the changes since the last release.
-1. _Optionally_ `git log` to find the sha of the commit you want to push if not latest
-1. _Optionally_ run [integration tests](integration-tests.md) locally to increase confidence in the release.
-1. Create a new Git tag with the desired version number.
-1. Push the tag to the `google-gemini/gemini-cli` repository.
-1. The push will trigger the release workflow, which automates the rest of the process.
-1. Once the workflow is complete, it will have created a `release/vX.Y.Z` branch with the version bumps. Create a pull request from this branch to merge the version changes back into `main`.
-
-Pushing a new tag will trigger the [release workflow](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml), which will automatically:
-
-- Build and publish the packages to the npm registry.
-- Create a new GitHub release with generated release notes.
-- Create a new branch `release/vX.Y.Z` containing the version bump in the `package.json` files.
-
We also run a Google cloud build called [release-docker.yml](../.gcp/release-docker.yaml). Which publishes the sandbox docker to match your release. This will also be moved to GH and combined with the main release file once service account permissions are sorted out.
-### 2. Monitor the Release Workflow
-
-You can monitor the progress of the release workflow in the [GitHub Actions tab](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml). If the workflow fails, you will need to investigate the cause of the failure, fix the issue, and then create a new tag to trigger a new release.
-
### After the Release
-After the workflow has successfully completed, you should:
+After the workflow has successfully completed, you can monitor its progress in the [GitHub Actions tab](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml). Once complete, you should:
1. Go to the [pull requests page](https://github.com/google-gemini/gemini-cli/pulls) of the repository.
2. Create a new pull request from the `release/vX.Y.Z` branch to `main`.
diff --git a/integration-tests/list_directory.test.js b/integration-tests/list_directory.test.js
index 6bbcde63..3190e482 100644
--- a/integration-tests/list_directory.test.js
+++ b/integration-tests/list_directory.test.js
@@ -18,6 +18,8 @@ test('should be able to list a directory', async (t) => {
const prompt = `Can you list the files in the current directory`;
const result = await rig.run(prompt);
- assert.ok(result.includes('file1.txt'));
- assert.ok(result.includes('subdir'));
+ const lines = result.split('\n').filter((line) => line.trim() !== '');
+ assert.equal(lines.length, 2);
+ assert.ok(lines.includes('file1.txt'));
+ assert.ok(lines.includes('subdir'));
});
diff --git a/package-lock.json b/package-lock.json
index e4b63506..a87cd491 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,6 +35,7 @@
"prettier": "^3.5.3",
"react-devtools-core": "^4.28.5",
"typescript-eslint": "^8.30.1",
+ "vitest": "^3.2.4",
"yargs": "^17.7.2"
},
"engines": {
@@ -1879,8 +1880,7 @@
"optional": true,
"os": [
"android"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.44.0",
@@ -1894,8 +1894,7 @@
"optional": true,
"os": [
"android"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.44.0",
@@ -1909,8 +1908,7 @@
"optional": true,
"os": [
"darwin"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.44.0",
@@ -1924,8 +1922,7 @@
"optional": true,
"os": [
"darwin"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.44.0",
@@ -1939,8 +1936,7 @@
"optional": true,
"os": [
"freebsd"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.44.0",
@@ -1954,8 +1950,7 @@
"optional": true,
"os": [
"freebsd"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.44.0",
@@ -1969,8 +1964,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.44.0",
@@ -1984,8 +1978,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.44.0",
@@ -1999,8 +1992,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.44.0",
@@ -2014,8 +2006,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.44.0",
@@ -2029,8 +2020,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.44.0",
@@ -2044,8 +2034,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.44.0",
@@ -2059,8 +2048,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.44.0",
@@ -2074,8 +2062,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.44.0",
@@ -2089,8 +2076,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.44.0",
@@ -2104,8 +2090,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.44.0",
@@ -2119,8 +2104,7 @@
"optional": true,
"os": [
"linux"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.44.0",
@@ -2134,8 +2118,7 @@
"optional": true,
"os": [
"win32"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.44.0",
@@ -2149,8 +2132,7 @@
"optional": true,
"os": [
"win32"
- ],
- "peer": true
+ ]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.44.0",
@@ -2164,8 +2146,7 @@
"optional": true,
"os": [
"win32"
- ],
- "peer": true
+ ]
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
@@ -5398,7 +5379,6 @@
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
diff --git a/package.json b/package.json
index 0b64f85b..64035439 100644
--- a/package.json
+++ b/package.json
@@ -24,12 +24,13 @@
"clean": "node scripts/clean.js",
"prepare": "npm run bundle",
"test": "npm run test --workspaces",
- "test:ci": "npm run test:ci --workspaces --if-present",
+ "test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts",
"test:e2e": "npm run test:integration:sandbox:none -- --verbose --keep-output",
"test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman",
"test:integration:sandbox:none": "GEMINI_SANDBOX=false node integration-tests/run-tests.js",
"test:integration:sandbox:docker": "GEMINI_SANDBOX=docker node integration-tests/run-tests.js",
"test:integration:sandbox:podman": "GEMINI_SANDBOX=podman node integration-tests/run-tests.js",
+ "test:scripts": "vitest --config ./scripts/tests/vitest.config.ts",
"start": "node scripts/start.js",
"debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js",
"lint:fix": "eslint . --fix && eslint integration-tests --fix",
@@ -85,7 +86,7 @@
"prettier": "^3.5.3",
"react-devtools-core": "^4.28.5",
"typescript-eslint": "^8.30.1",
+ "vitest": "^3.2.4",
"yargs": "^17.7.2"
- },
- "dependencies": {}
+ }
}
diff --git a/scripts/get-release-version.js b/scripts/get-release-version.js
new file mode 100644
index 00000000..5aee50c4
--- /dev/null
+++ b/scripts/get-release-version.js
@@ -0,0 +1,89 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { execSync } from 'child_process';
+import fs from 'fs';
+import path from 'path';
+
+function getPackageVersion() {
+ const packageJsonPath = path.resolve(process.cwd(), 'package.json');
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
+ return packageJson.version;
+}
+
+function getShortSha() {
+ return execSync('git rev-parse --short HEAD').toString().trim();
+}
+
+export function getNightlyTagName() {
+ const version = getPackageVersion();
+ const now = new Date();
+ const year = now.getUTCFullYear().toString().slice(-2);
+ const month = (now.getUTCMonth() + 1).toString().padStart(2, '0');
+ const day = now.getUTCDate().toString().padStart(2, '0');
+ const date = `${year}${month}${day}`;
+
+ const sha = getShortSha();
+ return `v${version}-nightly.${date}.${sha}`;
+}
+
+export function getReleaseVersion() {
+ const isNightly = process.env.IS_NIGHTLY === 'true';
+ const manualVersion = process.env.MANUAL_VERSION;
+
+ let releaseTag;
+
+ if (isNightly) {
+ console.error('Calculating next nightly version...');
+ releaseTag = getNightlyTagName();
+ } else if (manualVersion) {
+ console.error(`Using manual version: ${manualVersion}`);
+ releaseTag = manualVersion;
+ } else {
+ throw new Error(
+ 'Error: No version specified and this is not a nightly release.',
+ );
+ }
+
+ if (!releaseTag) {
+ throw new Error('Error: Version could not be determined.');
+ }
+
+ if (!releaseTag.startsWith('v')) {
+ console.error("Version is missing 'v' prefix. Prepending it.");
+ releaseTag = `v${releaseTag}`;
+ }
+
+ if (releaseTag.includes('+')) {
+ throw new Error(
+ 'Error: Versions with build metadata (+) are not supported for releases. Please use a pre-release version (e.g., v1.2.3-alpha.4) instead.',
+ );
+ }
+
+ if (!releaseTag.match(/^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$/)) {
+ throw new Error(
+ 'Error: Version must be in the format vX.Y.Z or vX.Y.Z-prerelease',
+ );
+ }
+
+ const releaseVersion = releaseTag.substring(1);
+ let npmTag = 'latest';
+ if (releaseVersion.includes('-')) {
+ npmTag = releaseVersion.split('-')[1].split('.')[0];
+ }
+
+ return { releaseTag, releaseVersion, npmTag };
+}
+
+if (process.argv[1] === new URL(import.meta.url).pathname) {
+ try {
+ const versions = getReleaseVersion();
+ console.log(JSON.stringify(versions));
+ } catch (error) {
+ console.error(error.message);
+ process.exit(1);
+ }
+}
diff --git a/scripts/tag-release.js b/scripts/tag-release.js
deleted file mode 100644
index 40385264..00000000
--- a/scripts/tag-release.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * @license
- * Copyright 2025 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { execSync } from 'child_process';
-import { readFileSync } from 'fs';
-import path from 'path';
-
-function getVersion() {
- const packageJsonPath = path.resolve(process.cwd(), 'package.json');
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
- return packageJson.version;
-}
-
-function getShortSha() {
- return execSync('git rev-parse --short HEAD').toString().trim();
-}
-
-function getNightlyTagName() {
- const version = getVersion();
- const now = new Date();
- const year = now.getUTCFullYear().toString().slice(-2);
- const month = (now.getUTCMonth() + 1).toString().padStart(2, '0');
- const day = now.getUTCDate().toString().padStart(2, '0');
- const date = `${year}${month}${day}`;
-
- const sha = getShortSha();
- return `v${version}-nightly.${date}.${sha}`;
-}
-
-function createAndPushTag(tagName, isSigned) {
- const command = isSigned
- ? `git tag -s -a ${tagName} -m ''`
- : `git tag ${tagName}`;
-
- try {
- console.log(`Executing: ${command}`);
- execSync(command, { stdio: 'inherit' });
- console.log(`Successfully created tag: ${tagName}`);
-
- console.log(`Pushing tag to origin...`);
- execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
- console.log(`Successfully pushed tag: ${tagName}`);
- } catch (error) {
- console.error(`Failed to create or push tag: ${tagName}`);
- console.error(error);
- process.exit(1);
- }
-}
-
-const tagName = getNightlyTagName();
-// In GitHub Actions, the CI variable is set to true.
-// We will create a signed commit if not in a CI environment.
-const shouldSign = !process.env.CI;
-
-createAndPushTag(tagName, shouldSign);
diff --git a/scripts/tests/get-release-version.test.js b/scripts/tests/get-release-version.test.js
new file mode 100644
index 00000000..3b127644
--- /dev/null
+++ b/scripts/tests/get-release-version.test.js
@@ -0,0 +1,108 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { getReleaseVersion } from '../get-release-version';
+import { execSync } from 'child_process';
+import * as fs from 'fs';
+
+vi.mock('child_process', () => ({
+ execSync: vi.fn(),
+}));
+
+vi.mock('fs', async (importOriginal) => {
+ const mod = await importOriginal();
+ return {
+ ...mod,
+ readFileSync: vi.fn(),
+ };
+});
+
+describe('getReleaseVersion', () => {
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ vi.resetModules();
+ process.env = { ...originalEnv };
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ vi.clearAllMocks();
+ vi.useRealTimers();
+ });
+
+ it('should calculate nightly version when IS_NIGHTLY is true', () => {
+ process.env.IS_NIGHTLY = 'true';
+ const knownDate = new Date('2025-07-20T10:00:00.000Z');
+ vi.setSystemTime(knownDate);
+ vi.mocked(fs.readFileSync).mockReturnValue(
+ JSON.stringify({ version: '0.1.0' }),
+ );
+ vi.mocked(execSync).mockReturnValue('abcdef');
+ const { releaseTag, releaseVersion, npmTag } = getReleaseVersion();
+ expect(releaseTag).toBe('v0.1.9-nightly.250720.abcdef');
+ expect(releaseVersion).toBe('0.1.9-nightly.250720.abcdef');
+ expect(npmTag).toBe('nightly');
+ });
+
+ it('should use manual version when provided', () => {
+ process.env.MANUAL_VERSION = '1.2.3';
+ const { releaseTag, releaseVersion, npmTag } = getReleaseVersion();
+ expect(releaseTag).toBe('v1.2.3');
+ expect(releaseVersion).toBe('1.2.3');
+ expect(npmTag).toBe('latest');
+ });
+
+ it('should prepend v to manual version if missing', () => {
+ process.env.MANUAL_VERSION = '1.2.3';
+ const { releaseTag } = getReleaseVersion();
+ expect(releaseTag).toBe('v1.2.3');
+ });
+
+ it('should handle pre-release versions correctly', () => {
+ process.env.MANUAL_VERSION = 'v1.2.3-beta.1';
+ const { releaseTag, releaseVersion, npmTag } = getReleaseVersion();
+ expect(releaseTag).toBe('v1.2.3-beta.1');
+ expect(releaseVersion).toBe('1.2.3-beta.1');
+ expect(npmTag).toBe('beta');
+ });
+
+ it('should throw an error for invalid version format', () => {
+ process.env.MANUAL_VERSION = '1.2';
+ expect(() => getReleaseVersion()).toThrow(
+ 'Error: Version must be in the format vX.Y.Z or vX.Y.Z-prerelease',
+ );
+ });
+
+ it('should throw an error if no version is provided for non-nightly release', () => {
+ expect(() => getReleaseVersion()).toThrow(
+ 'Error: No version specified and this is not a nightly release.',
+ );
+ });
+
+ it('should throw an error for versions with build metadata', () => {
+ process.env.MANUAL_VERSION = 'v1.2.3+build456';
+ expect(() => getReleaseVersion()).toThrow(
+ 'Error: Versions with build metadata (+) are not supported for releases.',
+ );
+ });
+});
+
+describe('get-release-version script', () => {
+ it('should print version JSON to stdout when executed directly', () => {
+ const expectedJson = {
+ releaseTag: 'v0.1.0-nightly.20250705',
+ releaseVersion: '0.1.0-nightly.20250705',
+ npmTag: 'nightly',
+ };
+ execSync.mockReturnValue(JSON.stringify(expectedJson));
+
+ const result = execSync('node scripts/get-release-version.js').toString();
+ expect(JSON.parse(result)).toEqual(expectedJson);
+ });
+});
diff --git a/scripts/tests/test-setup.ts b/scripts/tests/test-setup.ts
new file mode 100644
index 00000000..d4c4b465
--- /dev/null
+++ b/scripts/tests/test-setup.ts
@@ -0,0 +1,12 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi } from 'vitest';
+
+vi.mock('fs', () => ({
+ ...vi.importActual('fs'),
+ appendFileSync: vi.fn(),
+}));
diff --git a/scripts/tests/vitest.config.ts b/scripts/tests/vitest.config.ts
new file mode 100644
index 00000000..a8e67005
--- /dev/null
+++ b/scripts/tests/vitest.config.ts
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: 'node',
+ include: ['scripts/tests/**/*.test.js'],
+ setupFiles: ['scripts/tests/test-setup.ts'],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'lcov'],
+ },
+ },
+});