From c9a3d28d996ec02592f4ff2d21154db365e8a396 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Thu, 23 Oct 2025 10:14:32 -0700 Subject: [PATCH] refactor(ci): simplify sandbox build process - Replaces the `build_sandbox.js` script with a direct `docker build` command in the `build-and-publish` workflow. - Deletes the now-redundant `build_sandbox.js` and `sandbox_command.js` scripts. - This makes the sandbox build process more transparent and removes unnecessary complexity. --- .github/workflows/build-and-publish.yml | 12 +- scripts/build_sandbox.js | 192 ------------------------ scripts/sandbox_command.js | 127 ---------------- 3 files changed, 11 insertions(+), 320 deletions(-) delete mode 100644 scripts/build_sandbox.js delete mode 100644 scripts/sandbox_command.js diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 7e89f2ba60..35270c9010 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -104,8 +104,18 @@ jobs: - name: 'Build and Push Sandbox Image' id: 'docker_build' + env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' shell: 'bash' run: | IMAGE_TAG="ghcr.io/${{ github.repository }}/sandbox:${{ needs.publish-bundle.outputs.version }}" - node scripts/build_sandbox.js --image "${IMAGE_TAG}" + docker build . \ + --file Dockerfile \ + --tag "${IMAGE_TAG}" \ + --build-arg CLI_VERSION=${{ needs.publish-bundle.outputs.version }} \ + --build-arg NPM_REGISTRY_SCOPE=${{ vars.NPM_REGISTRY_SCOPE }} \ + --build-arg NPM_REGISTRY_URL=${{ vars.NPM_REGISTRY_URL }} \ + --build-arg CLI_PACKAGE_NAME=${{ vars.CLI_PACKAGE_NAME }} \ + --secret id=GITHUB_TOKEN docker push "${IMAGE_TAG}" + echo "uri=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" diff --git a/scripts/build_sandbox.js b/scripts/build_sandbox.js deleted file mode 100644 index 409bcab459..0000000000 --- a/scripts/build_sandbox.js +++ /dev/null @@ -1,192 +0,0 @@ -/** - * @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=${npmPackageVersion} --build-arg NPM_REGISTRY_SCOPE=${process.env.NPM_REGISTRY_SCOPE} --build-arg NPM_REGISTRY_URL=${process.env.NPM_REGISTRY_URL} --build-arg CLI_PACKAGE_NAME=${process.env.CLI_PACKAGE_NAME} --secret id=GITHUB_TOKEN,env=GITHUB_TOKEN -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 deleted file mode 100644 index 29fb8a3b17..0000000000 --- a/scripts/sandbox_command.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * @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);