diff --git a/.github/actions/publish-release/action.yml b/.github/actions/publish-release/action.yml index 9fee37cf73..e52024d0b6 100644 --- a/.github/actions/publish-release/action.yml +++ b/.github/actions/publish-release/action.yml @@ -144,26 +144,14 @@ runs: # TODO: Refactor this github specific publishing script to be generalized based upon inputs. - name: '📦 Prepare for GitHub release' - if: "inputs.npm-registry-url == 'https://npm.pkg.github.com/' && inputs.use-bundle-release == 'true'" + if: "inputs.npm-registry-url == 'https://npm.pkg.github.com/'" working-directory: '${{ inputs.working-directory }}' shell: 'bash' run: | node ${{ github.workspace }}/scripts/prepare-github-release.js - - name: '📦 Publish Root Package' - if: "${{ inputs.use-bundle-release == 'true' }}" - working-directory: '${{ inputs.working-directory }}' - env: - NODE_AUTH_TOKEN: '${{ inputs.github-token }}' - DRY_RUN: '${{ inputs.dry-run }}' - shell: 'bash' - run: | - npm publish \ - --dry-run="${DRY_RUN}" \ - --no-tag - - - name: 'Configure npm for publishing to npm' - if: "${{ inputs.use-bundle-release == 'false' }}" + - name: 'Configure npm for publishing to npm (non-bundle release)' + if: "inputs.use-bundle-release == 'false'" uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' with: node-version-file: '${{ inputs.working-directory }}/.nvmrc' @@ -171,15 +159,9 @@ runs: scope: '${{inputs.npm-registry-scope}}' - name: 'Get core Token' - if: "${{ inputs.use-bundle-release == 'false' }}" uses: './.github/actions/npm-auth-token' id: 'core-token' with: - package-name: '${{ inputs.core-package-name }}' - github-token: '${{ inputs.github-token }}' - wombat-token-core: '${{ inputs.wombat-token-core }}' - wombat-token-cli: '${{ inputs.wombat-token-cli }}' - wombat-token-a2a-server: '${{ inputs.wombat-token-a2a-server }}' - name: '📦 Publish CORE to NPM' if: "${{ inputs.use-bundle-release == 'false' }}" diff --git a/package-lock.json b/package-lock.json index 7c0cee5f5c..64a24f7cdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@google/gemini-cli", - "version": "0.12.0-nightly.20251022.0542de95", + "version": "0.12.0-nightly.20251022.0542de95-ci.12345.48a04d2d2da8ee3f0f7900ebd6cf0780040a0462", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@google/gemini-cli", - "version": "0.12.0-nightly.20251022.0542de95", + "version": "0.12.0-nightly.20251022.0542de95-ci.12345.48a04d2d2da8ee3f0f7900ebd6cf0780040a0462", "workspaces": [ "packages/*" ], diff --git a/package.json b/package.json index b431256b25..046ae822f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@google/gemini-cli", - "version": "0.12.0-nightly.20251022.0542de95", + "name": "@google-gemini/gemini-cli", + "version": "0.12.0-nightly.20251022.0542de95-ci.12345.48a04d2d2da8ee3f0f7900ebd6cf0780040a0462", "engines": { "node": ">=20.0.0" }, diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json index ec897db298..55b3873cd2 100644 --- a/packages/a2a-server/package.json +++ b/packages/a2a-server/package.json @@ -1,5 +1,5 @@ { - "name": "@google/gemini-cli-a2a-server", + "name": "@google-gemini/gemini-cli-a2a-server", "version": "0.12.0-nightly.20251022.0542de95", "description": "Gemini CLI A2A Server", "repository": { diff --git a/packages/core/package.json b/packages/core/package.json index 10adf56555..4f3be4a924 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,5 +1,5 @@ { - "name": "@google/gemini-cli-core", + "name": "@google-gemini/gemini-cli-core", "version": "0.12.0-nightly.20251022.0542de95", "description": "Gemini CLI Core", "repository": { diff --git a/scripts/build_sandbox.js b/scripts/build_sandbox.js new file mode 100644 index 0000000000..84ac96bc99 --- /dev/null +++ b/scripts/build_sandbox.js @@ -0,0 +1,192 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from 'node:child_process'; +import { + chmodSync, + existsSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { join } from 'node:path'; +import os from 'node:os'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import cliPkgJson from '../packages/cli/package.json' with { type: 'json' }; + +const argv = yargs(hideBin(process.argv)) + .option('s', { + alias: 'skip-npm-install-build', + type: 'boolean', + default: false, + description: 'skip npm install + npm run build', + }) + .option('f', { + alias: 'dockerfile', + type: 'string', + default: 'Dockerfile', + description: 'use for custom image', + }) + .option('i', { + alias: 'image', + type: 'string', + default: cliPkgJson.config.sandboxImageUri, + description: 'use name for custom image', + }) + .option('output-file', { + type: 'string', + description: + 'Path to write the final image URI. Used for CI/CD pipeline integration.', + }).argv; + +let sandboxCommand; +try { + sandboxCommand = execSync('node scripts/sandbox_command.js') + .toString() + .trim(); +} catch (e) { + console.warn('ERROR: could not detect sandbox container command'); + console.error(e); + process.exit(process.env.CI ? 1 : 0); +} + +if (sandboxCommand === 'sandbox-exec') { + console.warn( + 'WARNING: container-based sandboxing is disabled (see README.md#sandboxing)', + ); + process.exit(0); +} + +console.log(`using ${sandboxCommand} for sandboxing`); + +const image = argv.i; +const dockerFile = argv.f; + +if (!image.length) { + console.warn( + 'No default image tag specified in gemini-cli/packages/cli/package.json', + ); +} + +if (!argv.s) { + execSync('npm install', { stdio: 'inherit' }); + execSync('npm run build --workspaces', { stdio: 'inherit' }); +} + +console.log('packing @google/gemini-cli ...'); +const cliPackageDir = join('packages', 'cli'); +rmSync(join(cliPackageDir, 'dist', 'google-gemini-cli-*.tgz'), { force: true }); +execSync( + `npm pack -w @google/gemini-cli --pack-destination ./packages/cli/dist`, + { + stdio: 'ignore', + }, +); + +console.log('packing @google/gemini-cli-core ...'); +const corePackageDir = join('packages', 'core'); +rmSync(join(corePackageDir, 'dist', 'google-gemini-cli-core-*.tgz'), { + force: true, +}); +execSync( + `npm pack -w @google/gemini-cli-core --pack-destination ./packages/core/dist`, + { stdio: 'ignore' }, +); + +const packageVersion = JSON.parse( + readFileSync(join(process.cwd(), 'package.json'), 'utf-8'), +).version; + +chmodSync( + join(cliPackageDir, 'dist', `google-gemini-cli-${packageVersion}.tgz`), + 0o755, +); +chmodSync( + join(corePackageDir, 'dist', `google-gemini-cli-core-${packageVersion}.tgz`), + 0o755, +); + +const buildStdout = process.env.VERBOSE ? 'inherit' : 'ignore'; + +// Determine the appropriate shell based on OS +const isWindows = os.platform() === 'win32'; +const shellToUse = isWindows ? 'powershell.exe' : '/bin/bash'; + +function buildImage(imageName, dockerfile) { + console.log(`building ${imageName} ... (can be slow first time)`); + + let buildCommandArgs = ''; + let tempAuthFile = ''; + + if (sandboxCommand === 'podman') { + if (isWindows) { + // PowerShell doesn't support <() process substitution. + // Create a temporary auth file that we will clean up after. + tempAuthFile = join(os.tmpdir(), `gemini-auth-${Date.now()}.json`); + writeFileSync(tempAuthFile, '{}'); + buildCommandArgs = `--authfile="${tempAuthFile}"`; + } else { + // Use bash-specific syntax for Linux/macOS + buildCommandArgs = `--authfile=<(echo '{}')`; + } + } + + const npmPackageVersion = JSON.parse( + readFileSync(join(process.cwd(), 'package.json'), 'utf-8'), + ).version; + + const imageTag = + process.env.GEMINI_SANDBOX_IMAGE_TAG || imageName.split(':')[1]; + const finalImageName = `${imageName.split(':')[0]}:${imageTag}`; + + try { + execSync( + `${sandboxCommand} build ${buildCommandArgs} ${ + process.env.BUILD_SANDBOX_FLAGS || '' + } --build-arg CLI_VERSION_ARG=${npmPackageVersion} -f "${dockerfile}" -t "${finalImageName}" .`, + { stdio: buildStdout, shell: shellToUse }, + ); + console.log(`built ${finalImageName}`); + + // If an output file path was provided via command-line, write the final image URI to it. + if (argv.outputFile) { + console.log( + `Writing final image URI for CI artifact to: ${argv.outputFile}`, + ); + // The publish step only supports one image. If we build multiple, only the last one + // will be published. Throw an error to make this failure explicit if the file already exists. + if (existsSync(argv.outputFile)) { + throw new Error( + `CI artifact file ${argv.outputFile} already exists. Refusing to overwrite.`, + ); + } + writeFileSync(argv.outputFile, finalImageName); + } + } finally { + // If we created a temp file, delete it now. + if (tempAuthFile) { + rmSync(tempAuthFile, { force: true }); + } + } +} + +buildImage(image, dockerFile); + +execSync(`${sandboxCommand} image prune -f`, { stdio: 'ignore' }); diff --git a/scripts/sandbox_command.js b/scripts/sandbox_command.js new file mode 100644 index 0000000000..29fb8a3b17 --- /dev/null +++ b/scripts/sandbox_command.js @@ -0,0 +1,127 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import stripJsonComments from 'strip-json-comments'; +import os from 'node:os'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import dotenv from 'dotenv'; +import { GEMINI_DIR } from '@google/gemini-cli-core'; + +const argv = yargs(hideBin(process.argv)).option('q', { + alias: 'quiet', + type: 'boolean', + default: false, +}).argv; + +let geminiSandbox = process.env.GEMINI_SANDBOX; + +if (!geminiSandbox) { + const userSettingsFile = join(os.homedir(), GEMINI_DIR, 'settings.json'); + if (existsSync(userSettingsFile)) { + const settings = JSON.parse( + stripJsonComments(readFileSync(userSettingsFile, 'utf-8')), + ); + if (settings.sandbox) { + geminiSandbox = settings.sandbox; + } + } +} + +if (!geminiSandbox) { + let currentDir = process.cwd(); + while (true) { + const geminiEnv = join(currentDir, GEMINI_DIR, '.env'); + const regularEnv = join(currentDir, '.env'); + if (existsSync(geminiEnv)) { + dotenv.config({ path: geminiEnv, quiet: true }); + break; + } else if (existsSync(regularEnv)) { + dotenv.config({ path: regularEnv, quiet: true }); + break; + } + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + break; + } + currentDir = parentDir; + } + geminiSandbox = process.env.GEMINI_SANDBOX; +} + +geminiSandbox = (geminiSandbox || '').toLowerCase(); + +const commandExists = (cmd) => { + const checkCommand = os.platform() === 'win32' ? 'where' : 'command -v'; + try { + execSync(`${checkCommand} ${cmd}`, { stdio: 'ignore' }); + return true; + } catch { + if (os.platform() === 'win32') { + try { + execSync(`${checkCommand} ${cmd}.exe`, { stdio: 'ignore' }); + return true; + } catch { + return false; + } + } + return false; + } +}; + +let command = ''; +if (['1', 'true'].includes(geminiSandbox)) { + if (commandExists('docker')) { + command = 'docker'; + } else if (commandExists('podman')) { + command = 'podman'; + } else { + console.error( + 'ERROR: install docker or podman or specify command in GEMINI_SANDBOX', + ); + process.exit(1); + } +} else if (geminiSandbox && !['0', 'false'].includes(geminiSandbox)) { + if (commandExists(geminiSandbox)) { + command = geminiSandbox; + } else { + console.error( + `ERROR: missing sandbox command '${geminiSandbox}' (from GEMINI_SANDBOX)`, + ); + process.exit(1); + } +} else { + if (os.platform() === 'darwin' && process.env.SEATBELT_PROFILE !== 'none') { + if (commandExists('sandbox-exec')) { + command = 'sandbox-exec'; + } else { + process.exit(1); + } + } else { + process.exit(1); + } +} + +if (!argv.q) { + console.log(command); +} +process.exit(0);