From dc83ad67521fb0a3b78527dfd92d225cb1ba30e9 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Tue, 1 Jul 2025 09:36:28 -0500 Subject: [PATCH] working to move release into github actions --- .github/workflows/release.yml | 90 ++++++++++++ docs/npm.md | 211 +++++++++++++++++++++++++++ package.json | 23 +-- packages/cli/package.json | 10 +- packages/core/package.json | 7 +- scripts/bind_package_dependencies.js | 50 ------- scripts/bind_package_version.js | 42 ------ scripts/clean.js | 1 + scripts/prepare-core-package.js | 37 ----- scripts/prepare-package.js | 51 +++++++ scripts/prepublish.js | 9 +- scripts/version.js | 77 ++++++++++ 12 files changed, 453 insertions(+), 155 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 docs/npm.md delete mode 100644 scripts/bind_package_dependencies.js delete mode 100644 scripts/bind_package_version.js delete mode 100644 scripts/prepare-core-package.js create mode 100644 scripts/prepare-package.js create mode 100644 scripts/version.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..bc4ddfbbfb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Release + +on: + # Official Release: Triggered when a tag like v1.2.3 is pushed + # push: + # tags: + # - 'v[0-9]+.[0-9]+.[0-9]+' + + # Pre-Release: Manual trigger from the GitHub Actions UI + workflow_dispatch: + inputs: + pre_release_tag: + description: 'NPM pre-release identifier (e.g., "beta", "next").' + required: true + default: 'next' + type: string + dry_run: + description: 'Whether to run the publish step in dry-run mode.' + required: true + type: boolean + default: true + +jobs: + release: + runs-on: ubuntu-latest + # Use the current repository name dynamically + if: github.repository == 'google-gemini/gemini-cli' + permissions: + contents: write # Required to create a GitHub release + packages: write # Required to publish to GitHub Packages (if you use it) + + steps: + - name: Checkout code + uses: actions/checkout@v4 + # Fetch all history for versioning + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + registry-url: 'https://wombat-dressing-room.appspot.com' + + - name: Install Dependencies + run: npm ci + + - name: Set Release Version and Tag + id: version + run: | + if [[ "${{ github.ref_type }}" == "tag" ]]; then + # For official releases, use the git tag as the version + # Example: v1.2.3 -> 1.2.3 + RELEASE_VERSION="${{ github.ref_name#v }}" + NPM_TAG="latest" + else + # For pre-releases, create a version like 1.2.3-next.1 + # and set the tag to "next" + npm version --no-git-tag-version prerelease --preid=${{ github.event.inputs.pre_release_tag }} + RELEASE_VERSION=$(node -p "require('./package.json').version") + NPM_TAG="${{ github.event.inputs.pre_release_tag }}" + fi + echo "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_OUTPUT + echo "NPM_TAG=${NPM_TAG}" >> $GITHUB_OUTPUT + + - name: Determine Run Type + id: run_type + run: | + if [[ "${{ github.ref_type }}" == "tag" || "${{ inputs.dry_run }}" == "false" ]]; then + echo "NPM_DRY_RUN=" >> $GITHUB_OUTPUT + else + echo "NPM_DRY_RUN=--dry-run" >> $GITHUB_OUTPUT + fi + + - name: Build, Prepare, and Publish + run: npm run publish:release + env: + NODE_AUTH_TOKEN: ${{ secrets.WOMBAT_TOKEN }} + NPM_PUBLISH_TAG: ${{ steps.version.outputs.NPM_TAG }} + NPM_DRY_RUN: ${{ steps.run_type.outputs.NPM_DRY_RUN }} + + - name: Create GitHub Release + if: steps.run_type.outputs.NPM_DRY_RUN == '' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create v${{ steps.version.outputs.RELEASE_VERSION }} \ + --title "Release v${{ steps.version.outputs.RELEASE_VERSION }}" \ + --notes "See the [CHANGELOG.md](CHANGELOG.md) for details." diff --git a/docs/npm.md b/docs/npm.md new file mode 100644 index 0000000000..f91cd69ed1 --- /dev/null +++ b/docs/npm.md @@ -0,0 +1,211 @@ +# NPM Workspaces + +This project uses [NPM Workspaces](https://docs.npmjs.com/cli/v10/using-npm/workspaces) to manage the packages within this monorepo. This simplifies development by allowing us to manage dependencies and run scripts across multiple packages from the root of the project. + +## How it Works + +The root `package.json` file defines the workspaces for this project: + +```json +{ + "workspaces": ["packages/*"] +} +``` + +This tells NPM that any folder inside the `packages` directory is a separate package that should be managed as part of the workspace. + +## Benefits of Workspaces + +- **Simplified Dependency Management**: Running `npm install` from the root of the project will install all dependencies for all packages in the workspace and link them together. This means you don't need to run `npm install` in each package's directory. +- **Automatic Linking**: Packages within the workspace can depend on each other. When you run `npm install`, NPM will automatically create symlinks between the packages. This means that when you make changes to one package, the changes are immediately available to other packages that depend on it. +- **Simplified Script Execution**: You can run scripts in any package from the root of the project using the `--workspace` flag. For example, to run the `build` script in the `cli` package, you can run `npm run build --workspace @google/gemini-cli`. + +## Package Overview + +This monorepo contains two main packages: `@google/gemini-cli` and `@google/gemini-cli-core`. + +### `@google/gemini-cli` + +This is the main package for the Gemini CLI. It is responsible for the user interface, command parsing, and all other user-facing functionality. + +When this package is published, it is bundled into a single executable file. This bundle includes all of the package's dependencies, including `@google/gemini-cli-core`. This means that whether a user installs the package with `npm install -g @google/gemini-cli` or runs it directly with `npx @google/gemini-cli`, they are using this single, self-contained executable. + +### `@google/gemini-cli-core` + +This package contains the core logic for interacting with the Gemini API. It is responsible for making API requests, handling authentication, and managing the local cache. + +This package is not bundled. When it is published, it is published as a standard Node.js package with its own dependencies. This allows it to be used as a standalone package in other projects, if needed. All transpiled js code in the `dist` folder is included in the package. + +## Versioning and Publishing + +All packages in this monorepo are versioned together from the root `package.json` file. When a new version is released, the version number in the root `package.json` is updated, and all packages are published with that version. + +### Publishing to NPM + +When we are ready to publish a new version of the packages to npm, the following steps are taken: + +1. The version number in the root `package.json` is updated. +2. The `publish:release` script is run from the root of the project. This script builds all the packages, prepares them for publishing, and then publishes them to npm. + +When the packages are published, the `workspace:*` dependencies are replaced with the actual version number of the published package. This ensures that when a user installs a package from npm, they get the correct version of its dependencies. + +### NPX Installation + +When a user runs `npx @google/gemini-cli`, npm downloads the `@google/gemini-cli` package and its dependencies from the npm registry. Because the `workspace:*` dependencies were replaced with the actual version numbers during publishing, npm is able to resolve and download the correct versions of all the required packages. + +## Release Process + +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, but it still requires some manual steps. + +### 1. Create a Release Branch + +All releases should be prepared on a dedicated release branch. This allows the release to be reviewed and tested before it is merged into the main branch. + +```bash +git checkout -b release/vX.X.X +``` + +### 2. Run the Versioning Script + +The `npm run release:version` script is used to bump the version number of all packages in the monorepo. This script will also update the dependencies between the packages and create a git commit and tag for the new version. + +```bash +npm run release:version +``` + +This will do the following: + +1. Bump the version in the root `package.json`. +2. Run a script to update the `@google/gemini-cli-core` dependency in `@google/gemini-cli`'s `package.json`. +3. Create a `chore(release): vX.X.X` commit with all the file changes. +4. Create a `vX.X.X` git tag. + +### 3. Push the Commit and Tag + +Once the versioning script has been run, you need to push the commit and the new tag to the remote repository. The `push-release` script can be used to do this. + +```bash +npm run push-release +``` + +### 4. Create a Pull Request + +After pushing the release branch and tag, create a pull request to merge the release branch into the main branch. The pull request should be reviewed and approved by at least one other maintainer. + +The description of the pull request should include a summary of the changes in the release. This will be used to generate the release notes. + +### 5. Publish to the Registry + +Once the release pull request is merged into the main branch, the packages are automatically published to the npm registry by the CI/CD pipeline. You can monitor the progress of the publishing process in the GitHub Actions tab. + +## Local Testing and Validation + +It is crucial to test any changes to the packaging and publishing process locally before committing them. This ensures that the packages will be published correctly and that they will work as expected when installed by a user. + +To validate your changes, you can perform a dry run of the publishing process. This will simulate the publishing process without actually publishing the packages to the npm registry. + +```bash +npm_package_version=9.9.9 SANDBOX_IMAGE_REGISTRY="registry" SANDBOX_IMAGE_NAME="thename" npm run publish:npm --dry-run +``` + +This command will do the following: + +1. Build all the packages. +2. Run all the prepublish scripts. +3. Create the package tarballs that would be published to npm. +4. Print a summary of the packages that would be published. + +You can then inspect the generated tarballs to ensure that they contain the correct files and that the `package.json` files have been updated correctly. The tarballs will be created in the root of each package's directory (e.g., `packages/cli/google-gemini-cli-0.1.6.tgz`). + +By performing a dry run, you can be confident that your changes to the packaging process are correct and that the packages will be published successfully. + +## Release Deep Dive + +can you walk me through all the steps that need to happen to do a release? not specifically which command runs but which +files need to move where and why? + +✦ Of course. I can walk you through the conceptual steps of the release process. Based on the file structure and typical +monorepo release patterns, here is a breakdown of how files are gathered, transformed, and published. + +The main goal of the release process is to take the source code from the packages/ directory, build it, and assemble a +clean, self-contained package in a temporary dist directory at the root of the project. This dist directory is what +actually gets published to NPM. + +Here are the key stages: + +Stage 1: Pre-Release Sanity Checks and Versioning + +- What happens: Before any files are moved, the process ensures the project is in a good state. This involves running tests, + linting, and type-checking (npm run preflight). The version number in the root package.json and packages/cli/package.json + is updated to the new release version. +- Why: This guarantees that only high-quality, working code is released. Versioning is the first step to signify a new + release. + +Stage 2: Building the Source Code + +- What happens: The TypeScript source code in packages/core/src and packages/cli/src is compiled into JavaScript. +- File movement: + - packages/core/src/\*_/_.ts -> compiled to -> packages/core/dist/ + - packages/cli/src/\*_/_.ts -> compiled to -> packages/cli/dist/ +- Why: The TypeScript code written during development needs to be converted into plain JavaScript that can be run by + Node.js. The core package is built first as the cli package depends on it. + +Stage 3: Assembling the Final Publishable Package + +This is the most critical stage where files are moved and transformed into their final state for publishing. A temporary +dist folder is created at the project root to house the final package contents. + +1. The `package.json` is Transformed: + - What happens: The package.json from packages/cli/ is read, modified, and written into the root dist/ directory. The + script scripts/prepare-cli-packagejson.js is responsible for this. + - File movement: packages/cli/package.json -> (in-memory transformation) -> dist/package.json + - Why: The final package.json must be different from the one used in development. Key changes include: + - Removing devDependencies. + - Removing workspace-specific "dependencies": { "@gemini-cli/core": "workspace:\*" } and ensuring the core code is + bundled directly into the final JavaScript file. + - Ensuring the bin, main, and files fields point to the correct locations within the final package structure. + +2. The JavaScript Bundle is Created: + - What happens: The built JavaScript from both packages/core/dist and packages/cli/dist are bundled into a single, + executable JavaScript file. + - File movement: packages/cli/dist/index.js + packages/core/dist/index.js -> (bundled by esbuild) -> dist/cli.js (or a + similar name). + - Why: This creates a single, optimized file that contains all the necessary application code. It simplifies the package + by removing the need for the core package to be a separate dependency on NPM, as its code is now included directly. + +3. Static and Supporting Files are Copied: + - What happens: Essential files that are not part of the source code but are required for the package to work correctly + or be well-described are copied into the dist directory. + - File movement: + - README.md -> dist/README.md + - LICENSE -> dist/LICENSE + - packages/cli/src/utils/\*.sb (sandbox profiles) -> dist/ + - Why: + - The README.md and LICENSE are standard files that should be included in any NPM package. + - The sandbox profiles (.sb files) are critical runtime assets required for the CLI's sandboxing feature to + function. They must be located next to the final executable. + +Stage 4: Publishing to NPM + +- What happens: The npm publish command is run from inside the root dist directory. +- Why: By running npm publish from within the dist directory, only the files we carefully assembled in Stage 3 are uploaded + to the NPM registry. This prevents any source code, test files, or development configurations from being accidentally + published, resulting in a clean and minimal package for users. + +Summary of File Flow + + 1 [Project Root] + 2 ├── packages/core/src/*.ts ───────┐ + 3 └── packages/cli/src/*.ts ───────┼──(Build)──> [Bundled JS] ─────┐ + 4 │ │ + 5 ├── packages/cli/package.json ──(Transform)──> [Final package.json] │ + 6 │ │ + 7 ├── README.md ────────────────────┤ ├─(Assemble)─> dist/ + 8 ├── LICENSE ─────────────────────┤ │ + 9 └── packages/cli/src/utils/*.sb ─┴───────────────────────────────>│ + +10 │ +11 └─(Publish)─> NPM Registry + +This process ensures that the final published artifact is a purpose-built, clean, and efficient representation of the +project, rather than a direct copy of the development workspace. diff --git a/package.json b/package.json index 20dadd96a6..7118495eab 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,13 @@ "workspaces": [ "packages/*" ], - "repository": "google-gemini/gemini-cli", + "private": "true", + "repository": { + "type": "git", + "url": "git+https://github.com/google-gemini/gemini-cli.git" + }, "config": { - "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.7" + "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.8" }, "scripts": { "generate": "node scripts/generate-git-commit-info.js", @@ -39,18 +43,15 @@ "auth": "npm run auth:npm && npm run auth:docker", "prerelease:dev": "npm run prerelease:version --workspaces && npm run prerelease:deps --workspaces", "bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js", - "build:cli": "npm run build --workspace packages/cli", - "build:core": "npm run build --workspace packages/core", - "build:packages": "npm run build:core && npm run build:cli", + "build:packages": "npm run build --workspaces", "build:sandbox:fast": "node scripts/build_sandbox.js --skip-npm-install-build", - "prepare:cli-packagejson": "node scripts/prepare-cli-packagejson.js", - "prepare:packages": "node scripts/prepare-cli-packagejson.js && node scripts/prepare-core-package.js", + "prepare:package": "node scripts/prepare-package.js", "publish:sandbox": "node scripts/publish-sandbox.js", "publish:npm": "npm publish --workspaces ${NPM_PUBLISH_TAG:+--tag=$NPM_PUBLISH_TAG} ${NPM_DRY_RUN:+--dry-run}", - "publish:release": "npm run build:packages && npm run prepare:cli-packagejson && npm run build:sandbox:fast && npm run publish:sandbox && npm run publish:npm", - "telemetry": "node scripts/telemetry.js", - "start:gcp": "concurrently --raw --kill-others \"npm run telemetry -- --target=gcp\" \"npm start\"", - "prepublishOnly": "node scripts/prepublish.js" + "publish:release": "npm run prepare:package && npm run build:packages && npm run build:sandbox:fast && npm run publish:sandbox && npm run publish:npm", + "prepublishOnly": "node scripts/prepublish.js", + "release:version": "node scripts/version.js", + "push-release": "git push && git push --tags" }, "bin": { "gemini": "bundle/gemini.js" diff --git a/packages/cli/package.json b/packages/cli/package.json index ade14e16cb..e4a6a7ee54 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -17,20 +17,16 @@ "format": "prettier --write .", "test": "vitest run", "test:ci": "vitest run --coverage", - "typecheck": "tsc --noEmit", - "prerelease:version": "node ../../scripts/bind_package_version.js", - "prerelease:deps": "node ../../scripts/bind_package_dependencies.js", - "prepack": "npm run build", - "prepublishOnly": "node ../../scripts/prepublish.js" + "typecheck": "tsc --noEmit" }, "files": [ "dist" ], "config": { - "sandboxImageUri": "gemini-cli-sandbox" + "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.8" }, "dependencies": { - "@google/gemini-cli-core": "*", + "@google/gemini-cli-core": "file:../core", "@types/update-notifier": "^6.0.8", "command-exists": "^1.2.9", "diff": "^7.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index e4a3a33474..08e5a925f2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -6,18 +6,13 @@ "type": "module", "main": "dist/index.js", "scripts": { - "start": "node dist/src/index.js", "build": "node ../../scripts/build_package.js", "clean": "rm -rf dist", "lint": "eslint . --ext .ts,.tsx", "format": "prettier --write .", "test": "vitest run", "test:ci": "vitest run --coverage", - "typecheck": "tsc --noEmit", - "prerelease:version": "node ../../scripts/bind_package_version.js", - "prerelease:deps": "node ../../scripts/bind_package_dependencies.js", - "prepack": "npm run build", - "prepublishOnly": "node ../../scripts/prepublish.js" + "typecheck": "tsc --noEmit" }, "files": [ "dist" diff --git a/scripts/bind_package_dependencies.js b/scripts/bind_package_dependencies.js deleted file mode 100644 index eb9a4cc643..0000000000 --- a/scripts/bind_package_dependencies.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import fs from 'node:fs'; -import path from 'node:path'; -import _ from 'lodash'; - -function bindPackageDependencies() { - const scriptDir = process.cwd(); - const currentPkgJsonPath = path.join(scriptDir, 'package.json'); - const currentPkg = JSON.parse(fs.readFileSync(currentPkgJsonPath)); - // assume packages are all under //packages/ - const packagesDir = path.join(path.dirname(scriptDir)); - - const geminiCodePkgs = fs - .readdirSync(packagesDir) - .filter( - (name) => - fs.statSync(path.join(packagesDir, name)).isDirectory() && - fs.existsSync(path.join(packagesDir, name, 'package.json')), - ) - .map((packageDirname) => { - const packageJsonPath = path.join( - packagesDir, - packageDirname, - 'package.json', - ); - return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - }) - .reduce((pkgs, pkg) => ({ ...pkgs, [pkg.name]: pkg }), {}); - currentPkg.dependencies = _.mapValues( - currentPkg.dependencies, - (value, key) => { - if (geminiCodePkgs[key]) { - console.log( - `Package ${currentPkg.name} has a dependency on ${key}. Updating dependent version.`, - ); - return geminiCodePkgs[key].version; - } - return value; - }, - ); - const updatedPkgJson = JSON.stringify(currentPkg, null, 2) + '\n'; - fs.writeFileSync(currentPkgJsonPath, updatedPkgJson); -} - -bindPackageDependencies(); diff --git a/scripts/bind_package_version.js b/scripts/bind_package_version.js deleted file mode 100644 index 4e7a2ff7cd..0000000000 --- a/scripts/bind_package_version.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -// Assuming script is run from a package directory (e.g., packages/cli) -const packageDir = process.cwd(); -const rootDir = path.join(packageDir, '..', '..'); // Go up two directories to find the repo root - -function getRepoVersion() { - // Read root package.json - const rootPackageJsonPath = path.join(rootDir, 'package.json'); - const rootPackage = JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf8')); - return rootPackage.version; // This version is now expected to be the full version string -} - -const newVersion = getRepoVersion(); -console.log(`Setting package version to: ${newVersion}`); - -const packageJsonPath = path.join(packageDir, 'package.json'); - -if (fs.existsSync(packageJsonPath)) { - console.log(`Updating version for ${packageJsonPath}`); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - packageJson.version = newVersion; - fs.writeFileSync( - packageJsonPath, - JSON.stringify(packageJson, null, 2) + '\n', - 'utf8', - ); -} else { - console.error( - `Error: package.json not found in the current directory: ${packageJsonPath}`, - ); - process.exit(1); -} - -console.log('Done.'); diff --git a/scripts/clean.js b/scripts/clean.js index 51073edf4e..196384b0a2 100644 --- a/scripts/clean.js +++ b/scripts/clean.js @@ -27,6 +27,7 @@ const root = join(__dirname, '..'); // remove npm install/build artifacts rmSync(join(root, 'node_modules'), { recursive: true, force: true }); +rmSync(join(root, 'bundle'), { recursive: true, force: true }); rmSync(join(root, 'packages/cli/src/generated/'), { recursive: true, force: true, diff --git a/scripts/prepare-core-package.js b/scripts/prepare-core-package.js deleted file mode 100644 index 7d2e1e084b..0000000000 --- a/scripts/prepare-core-package.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -// ES module equivalent of __dirname -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Copy README.md to packages/core -const rootReadmePath = path.resolve(__dirname, '../README.md'); -const coreReadmePath = path.resolve(__dirname, '../packages/core/README.md'); - -try { - fs.copyFileSync(rootReadmePath, coreReadmePath); - console.log('Copied root README.md to packages/core/'); -} catch (err) { - console.error('Error copying README.md:', err); - process.exit(1); -} - -// Copy README.md to packages/cli -const rootLicensePath = path.resolve(__dirname, '../LICENSE'); -const coreLicensePath = path.resolve(__dirname, '../packages/core/LICENSE'); - -try { - fs.copyFileSync(rootLicensePath, coreLicensePath); - console.log('Copied root LICENSE to packages/core/'); -} catch (err) { - console.error('Error copying LICENSE:', err); - process.exit(1); -} diff --git a/scripts/prepare-package.js b/scripts/prepare-package.js new file mode 100644 index 0000000000..5498499be6 --- /dev/null +++ b/scripts/prepare-package.js @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// ES module equivalent of __dirname +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const rootDir = path.resolve(__dirname, '..'); + +function copyFiles(packageName, filesToCopy) { + const packageDir = path.resolve(rootDir, 'packages', packageName); + if (!fs.existsSync(packageDir)) { + console.error(`Error: Package directory not found at ${packageDir}`); + process.exit(1); + } + + console.log(`Preparing package: ${packageName}`); + for (const [source, dest] of Object.entries(filesToCopy)) { + const sourcePath = path.resolve(rootDir, source); + const destPath = path.resolve(packageDir, dest); + try { + fs.copyFileSync(sourcePath, destPath); + console.log(`Copied ${source} to packages/${packageName}/`); + } catch (err) { + console.error(`Error copying ${source}:`, err); + process.exit(1); + } + } +} + +// Prepare 'core' package +copyFiles('core', { + 'README.md': 'README.md', + LICENSE: 'LICENSE', + '.npmrc': '.npmrc', +}); + +// Prepare 'cli' package +copyFiles('cli', { + 'README.md': 'README.md', + LICENSE: 'LICENSE', +}); + +console.log('Successfully prepared all packages.'); diff --git a/scripts/prepublish.js b/scripts/prepublish.js index f97e450fc4..e30901b61b 100644 --- a/scripts/prepublish.js +++ b/scripts/prepublish.js @@ -19,9 +19,14 @@ if (!fs.existsSync(packageJsonPath)) { errors.push(`Error: package.json not found in ${process.cwd()}`); } else { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - if (packageJson.repository !== 'google-gemini/gemini-cli') { + if ( + !packageJson.repository || + typeof packageJson.repository !== 'object' || + packageJson.repository.type !== 'git' || + !packageJson.repository.url.includes('google-gemini/gemini-cli') + ) { errors.push( - `Error: The "repository" field in ${packageJsonPath} must be "google-gemini/gemini-cli".`, + `Error: The "repository" field in ${packageJsonPath} must be an object pointing to the "google-gemini/gemini-cli" git repository.`, ); } } diff --git a/scripts/version.js b/scripts/version.js new file mode 100644 index 0000000000..7792585d01 --- /dev/null +++ b/scripts/version.js @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execSync } from 'child_process'; +import { readFileSync, writeFileSync } from 'fs'; +import { resolve } from 'path'; + +// A script to handle versioning and ensure all related changes are in a single, atomic commit. + +function run(command) { + console.log(`> ${command}`); + execSync(command, { stdio: 'inherit' }); +} + +function readJson(filePath) { + return JSON.parse(readFileSync(filePath, 'utf-8')); +} + +function writeJson(filePath, data) { + writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n'); +} + +// 1. Get the version type from the command line arguments. +const versionType = process.argv[2]; +if (!versionType) { + console.error('Error: No version type specified.'); + console.error('Usage: npm run version '); + process.exit(1); +} + +// 2. Bump the version in the root and all workspace package.json files. +run(`npm version ${versionType} --workspaces --no-git-tag-version`); + +// 3. Get the new version number from the root package.json +const rootPackageJsonPath = resolve(process.cwd(), 'package.json'); +const newVersion = readJson(rootPackageJsonPath).version; + +// 4. Update the sandboxImageUri in the root package.json +const rootPackageJson = readJson(rootPackageJsonPath); +if (rootPackageJson.config?.sandboxImageUri) { + rootPackageJson.config.sandboxImageUri = rootPackageJson.config.sandboxImageUri.replace(/:.*$/, `:${newVersion}`); + console.log(`Updated sandboxImageUri in root to use version ${newVersion}`); + writeJson(rootPackageJsonPath, rootPackageJson); +} + +// 5. Update the sandboxImageUri in the cli package.json +const cliPackageJsonPath = resolve(process.cwd(), 'packages/cli/package.json'); +const cliPackageJson = readJson(cliPackageJsonPath); +if (cliPackageJson.config?.sandboxImageUri) { + cliPackageJson.config.sandboxImageUri = cliPackageJson.config.sandboxImageUri.replace(/:.*$/, `:${newVersion}`); + console.log(`Updated sandboxImageUri in cli package to use version ${newVersion}`); + writeJson(cliPackageJsonPath, cliPackageJson); +} + +// 6. Run `npm install` to update package-lock.json. +run('npm install'); + +const commitMessage = `chore(release): v${newVersion}`; +console.log( + `All files updated. Committing version v${newVersion} with message: "${commitMessage}"...`, +); + +// 7. Add all the changed files to the git staging area. +run( + 'git add package.json package-lock.json packages/cli/package.json packages/core/package.json', +); + +// 8. Create the atomic commit with all changes. +run(`git commit --no-edit -m "${commitMessage}"`); + +// 9. Create the git tag to match the commit. +run(`git tag -a "v${newVersion}" -m "${commitMessage}"`); + +console.log(`Successfully committed and tagged v${newVersion}.`);