From a7256f630c7c9335ccd7a41e97c9322c0a33ea67 Mon Sep 17 00:00:00 2001 From: matt korwel Date: Sat, 5 Jul 2025 13:58:59 -0700 Subject: [PATCH] Relase: Clean up and condensing (#3321) --- .gcp/publish-dry-run.yaml | 31 ---- .gcp/release.yaml | 150 ------------------ .github/workflows/release.yml | 120 +++++++++----- .../workflows/scheduled-nightly-release.yml | 74 --------- docs/npm.md | 54 ++----- integration-tests/list_directory.test.js | 6 +- package-lock.json | 62 +++----- package.json | 7 +- scripts/get-release-version.js | 89 +++++++++++ scripts/tag-release.js | 58 ------- scripts/tests/get-release-version.test.js | 108 +++++++++++++ scripts/tests/test-setup.ts | 12 ++ scripts/tests/vitest.config.ts | 20 +++ 13 files changed, 349 insertions(+), 442 deletions(-) delete mode 100644 .gcp/publish-dry-run.yaml delete mode 100644 .gcp/release.yaml delete mode 100644 .github/workflows/scheduled-nightly-release.yml create mode 100644 scripts/get-release-version.js delete mode 100644 scripts/tag-release.js create mode 100644 scripts/tests/get-release-version.test.js create mode 100644 scripts/tests/test-setup.ts create mode 100644 scripts/tests/vitest.config.ts 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 + 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 "---" - echo "Initial RELEASE_TAG: ${RELEASE_TAG}" + - 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 }} - # 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 - - echo "Finalized RELEASE_VERSION: ${RELEASE_VERSION}" - echo "Finalized NPM_TAG: ${NPM_TAG}" - echo "---" - - 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 -# 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+nightly..`. -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'], + }, + }, +});