Sync upstream Gemini-CLI v0.8.2 (#838)

This commit is contained in:
tanzhenxin
2025-10-23 09:27:04 +08:00
committed by GitHub
parent 096fabb5d6
commit eb95c131be
644 changed files with 70389 additions and 23709 deletions

View File

@@ -41,11 +41,13 @@ const argv = yargs(hideBin(process.argv))
.option('f', {
alias: 'dockerfile',
type: 'string',
default: 'Dockerfile',
description: 'use <dockerfile> for custom image',
})
.option('i', {
alias: 'image',
type: 'string',
default: cliPkgJson.config.sandboxImageUri,
description: 'use <image> name for custom image',
})
.option('output-file', {
@@ -62,7 +64,7 @@ try {
} catch (e) {
console.warn('ERROR: could not detect sandbox container command');
console.error(e);
process.exit(1);
process.exit(process.env.CI ? 1 : 0);
}
if (sandboxCommand === 'sandbox-exec') {
@@ -74,12 +76,10 @@ if (sandboxCommand === 'sandbox-exec') {
console.log(`using ${sandboxCommand} for sandboxing`);
const baseImage = cliPkgJson.config.sandboxImageUri;
const customImage = argv.i;
const baseDockerfile = 'Dockerfile';
const customDockerfile = argv.f;
const image = argv.i;
const dockerFile = argv.f;
if (!baseImage?.length) {
if (!image.length) {
console.warn(
'No default image tag specified in gemini-cli/packages/cli/package.json',
);
@@ -164,7 +164,7 @@ function buildImage(imageName, dockerfile) {
execSync(
`${sandboxCommand} build ${buildCommandArgs} ${
process.env.BUILD_SANDBOX_FLAGS || ''
} --build-arg CLI_VERSION_ARG=${npmPackageVersion} -f "${dockerfile}" -t "${imageName}" .`,
} --build-arg CLI_VERSION_ARG=${npmPackageVersion} -f "${dockerfile}" -t "${finalImageName}" .`,
{ stdio: buildStdout, shell: shellToUse },
);
console.log(`built ${finalImageName}`);
@@ -191,12 +191,6 @@ function buildImage(imageName, dockerfile) {
}
}
if (baseImage && baseDockerfile) {
buildImage(baseImage, baseDockerfile);
}
if (customDockerfile && customImage) {
buildImage(customImage, customDockerfile);
}
buildImage(image, dockerFile);
execSync(`${sandboxCommand} image prune -f`, { stdio: 'ignore' });

View File

@@ -24,7 +24,7 @@ import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
execSync('npx --yes @vscode/vsce package --no-dependencies', {
execSync('npm --workspace=qwen-code-vscode-ide-companion run package', {
stdio: 'inherit',
cwd: join(root, 'packages', 'vscode-ide-companion'),
cwd: root,
});

74
scripts/check-lockfile.js Normal file
View File

@@ -0,0 +1,74 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const lockfilePath = join(root, 'package-lock.json');
function readJsonFile(filePath) {
try {
const fileContent = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(fileContent);
} catch (error) {
console.error(`Error reading or parsing ${filePath}:`, error);
return null;
}
}
console.log('Checking lockfile...');
const lockfile = readJsonFile(lockfilePath);
if (lockfile === null) {
process.exit(1);
}
const packages = lockfile.packages || {};
const invalidPackages = [];
for (const [location, details] of Object.entries(packages)) {
// 1. Skip the root package itself.
if (location === '') {
continue;
}
// 2. Skip local workspace packages.
// They are identifiable in two ways:
// a) As a symlink within node_modules.
// b) As the source package definition, whose path is not in node_modules.
if (details.link === true || !location.includes('node_modules')) {
continue;
}
// 3. Any remaining package should be a third-party dependency.
// 1) Registry package with both "resolved" and "integrity" fields is valid.
if (details.resolved && details.integrity) {
continue;
}
// 2) Git and file dependencies only need a "resolved" field.
const isGitOrFileDep =
details.resolved?.startsWith('git') ||
details.resolved?.startsWith('file:');
if (isGitOrFileDep) {
continue;
}
// Mark the left dependency as invalid.
invalidPackages.push(location);
}
if (invalidPackages.length > 0) {
console.error(
'\nError: The following dependencies in package-lock.json are missing the "resolved" or "integrity" field:',
);
invalidPackages.forEach((pkg) => console.error(`- ${pkg}`));
process.exitCode = 1;
} else {
console.log('Lockfile check passed.');
process.exitCode = 0;
}

View File

@@ -46,7 +46,11 @@ for (const workspace of rootPackageJson.workspaces) {
}
}
// Clean up vsix files in vscode-ide-companion
// Clean up vscode-ide-companion package
rmSync(join(root, 'packages/vscode-ide-companion/node_modules'), {
recursive: true,
force: true,
});
const vsixFiles = globSync('packages/vscode-ide-companion/*.vsix', {
cwd: root,
});

View File

@@ -53,4 +53,25 @@ if (!fs.existsSync(sourceDir)) {
}
copyFilesRecursive(sourceDir, targetDir);
// Copy example extensions into the bundle.
const packageName = path.basename(process.cwd());
if (packageName === 'cli') {
const examplesSource = path.join(
sourceDir,
'commands',
'extensions',
'examples',
);
const examplesTarget = path.join(
targetDir,
'commands',
'extensions',
'examples',
);
if (fs.existsSync(examplesSource)) {
fs.cpSync(examplesSource, examplesTarget, { recursive: true });
}
}
console.log('Successfully copied files.');

View File

@@ -1,3 +1,5 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2025 Google LLC
@@ -5,166 +7,440 @@
*/
import { execSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import semver from 'semver';
function getPackageVersion() {
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version;
function readJson(filePath) {
return JSON.parse(readFileSync(filePath, 'utf-8'));
}
function getLatestStableTag() {
// Fetches all tags, then filters for the latest stable (non-prerelease) tag.
const tags = execSync('git tag --list "v*.*.*" --sort=-v:refname')
.toString()
.split('\n');
const latestStableTag = tags.find((tag) =>
tag.match(/^v[0-9]+\.[0-9]+\.[0-9]+$/),
);
if (!latestStableTag) {
throw new Error('Could not find a stable tag.');
}
return latestStableTag;
function getArgs() {
const args = {};
process.argv.slice(2).forEach((arg) => {
if (arg.startsWith('--')) {
const [key, value] = arg.substring(2).split('=');
args[key] = value === undefined ? true : value;
}
});
return args;
}
function incrementPatchVersion(version) {
const parts = version.split('.');
const major = parseInt(parts[0]);
const minor = parseInt(parts[1]);
const patch = parseInt(parts[2].split('-')[0]); // Handle pre-release versions
return `${major}.${minor}.${patch + 1}`;
}
function getLatestNightlyCount() {
function getLatestTag(pattern) {
const command = `git tag -l '${pattern}'`;
try {
// Try to get the latest nightly tag from git to determine the counter
const currentVersion = getPackageVersion();
const nextVersion = incrementPatchVersion(currentVersion);
const tags = execSync(`git tag -l "v${nextVersion}-nightly.*"`)
const tags = execSync(command)
.toString()
.trim();
.trim()
.split('\n')
.filter(Boolean);
if (tags.length === 0) return '';
if (!tags) return 0;
// Convert tags to versions (remove 'v' prefix) and sort by semver
const versions = tags
.map((tag) => tag.replace(/^v/, ''))
.filter((version) => semver.valid(version))
.sort((a, b) => semver.rcompare(a, b)); // rcompare for descending order
const nightlyTags = tags.split('\n').filter(Boolean);
const counts = nightlyTags.map((tag) => {
const match = tag.match(/nightly\.(\d+)$/);
return match ? parseInt(match[1]) : 0;
});
if (versions.length === 0) return '';
return Math.max(...counts, -1) + 1;
} catch (_error) {
// If we can't get tags, start from 0
return 0;
// Return the latest version with 'v' prefix restored
return `v${versions[0]}`;
} catch (error) {
console.error(
`Failed to get latest git tag for pattern "${pattern}": ${error.message}`,
);
return '';
}
}
function getNextVersionString(stableVersion, minorIncrement) {
const [major, minor] = stableVersion.substring(1).split('.');
const nextMinorVersion = parseInt(minor, 10) + minorIncrement;
return `${major}.${nextMinorVersion}.0`;
function getVersionFromNPM(distTag) {
const command = `npm view @google/gemini-cli version --tag=${distTag}`;
try {
return execSync(command).toString().trim();
} catch (error) {
console.error(
`Failed to get NPM version for dist-tag "${distTag}": ${error.message}`,
);
return '';
}
}
export function getNightlyTagName() {
const version = getPackageVersion();
const nextVersion = incrementPatchVersion(version);
const nightlyCount = getLatestNightlyCount();
return `v${nextVersion}-nightly.${nightlyCount}`;
function getAllVersionsFromNPM() {
const command = `npm view @google/gemini-cli versions --json`;
try {
const versionsJson = execSync(command).toString().trim();
return JSON.parse(versionsJson);
} catch (error) {
console.error(`Failed to get all NPM versions: ${error.message}`);
return [];
}
}
export function getPreviewTagName(stableVersion) {
const version = getNextVersionString(stableVersion, 1);
return `v${version}-preview`;
function isVersionDeprecated(version) {
const command = `npm view @google/gemini-cli@${version} deprecated`;
try {
const output = execSync(command).toString().trim();
return output.length > 0;
} catch (error) {
// This command shouldn't fail for existing versions, but as a safeguard:
console.error(
`Failed to check deprecation status for ${version}: ${error.message}`,
);
return false; // Assume not deprecated on error to avoid breaking the release.
}
}
function getPreviousReleaseTag(isNightly) {
if (isNightly) {
console.error('Finding latest nightly release...');
return execSync(
`gh release list --limit 100 --json tagName | jq -r '[.[] | select(.tagName | contains("nightly"))] | .[0].tagName'`,
)
.toString()
.trim();
function detectRollbackAndGetBaseline(npmDistTag) {
// Get the current dist-tag version
const distTagVersion = getVersionFromNPM(npmDistTag);
if (!distTagVersion) return { baseline: '', isRollback: false };
// Get all published versions
const allVersions = getAllVersionsFromNPM();
if (allVersions.length === 0)
return { baseline: distTagVersion, isRollback: false };
// Filter versions by type to match the dist-tag
let matchingVersions;
if (npmDistTag === 'latest') {
// Stable versions: no prerelease identifiers
matchingVersions = allVersions.filter(
(v) => semver.valid(v) && !semver.prerelease(v),
);
} else if (npmDistTag === 'preview') {
// Preview versions: contain -preview
matchingVersions = allVersions.filter(
(v) => semver.valid(v) && v.includes('-preview'),
);
} else if (npmDistTag === 'nightly') {
// Nightly versions: contain -nightly
matchingVersions = allVersions.filter(
(v) => semver.valid(v) && v.includes('-nightly'),
);
} else {
console.error('Finding latest STABLE release (excluding pre-releases)...');
return execSync(
`gh release list --limit 100 --json tagName | jq -r '[.[] | select(.tagName | (contains("nightly") or contains("preview")) | not)] | .[0].tagName'`,
)
.toString()
.trim();
}
}
export function getReleaseVersion() {
const isNightly = process.env.IS_NIGHTLY === 'true';
const isPreview = process.env.IS_PREVIEW === 'true';
const manualVersion = process.env.MANUAL_VERSION;
let releaseTag;
if (isNightly) {
console.error('Calculating next nightly version...');
const stableVersion = getLatestStableTag();
releaseTag = getNightlyTagName(stableVersion);
} else if (isPreview) {
console.error('Calculating next preview version...');
const stableVersion = getLatestStableTag();
releaseTag = getPreviewTagName(stableVersion);
} 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 or preview release.',
);
// For other dist-tags, just use the dist-tag version
return { baseline: distTagVersion, isRollback: false };
}
if (!releaseTag) {
throw new Error('Error: Version could not be determined.');
}
if (matchingVersions.length === 0)
return { baseline: distTagVersion, isRollback: false };
if (!releaseTag.startsWith('v')) {
console.error("Version is missing 'v' prefix. Prepending it.");
releaseTag = `v${releaseTag}`;
}
// Sort by semver to get a list from highest to lowest
matchingVersions.sort((a, b) => semver.rcompare(a, b));
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('-')) {
const prereleasePart = releaseVersion.split('-')[1];
npmTag = prereleasePart.split('.')[0];
// Ensure nightly releases use 'nightly' tag, not 'latest'
if (npmTag === 'nightly') {
npmTag = 'nightly';
// Find the highest non-deprecated version
let highestExistingVersion = '';
for (const version of matchingVersions) {
if (!isVersionDeprecated(version)) {
highestExistingVersion = version;
break; // Found the one we want
} else {
console.error(`Ignoring deprecated version: ${version}`);
}
}
const previousReleaseTag = getPreviousReleaseTag(isNightly);
// If all matching versions were deprecated, fall back to the dist-tag version
if (!highestExistingVersion) {
highestExistingVersion = distTagVersion;
}
return { releaseTag, releaseVersion, npmTag, previousReleaseTag };
// Check if we're in a rollback scenario
const isRollback = semver.gt(highestExistingVersion, distTagVersion);
return {
baseline: isRollback ? highestExistingVersion : distTagVersion,
isRollback,
distTagVersion,
highestExistingVersion,
};
}
if (process.argv[1] === new URL(import.meta.url).pathname) {
function doesVersionExist(version) {
// Check NPM
try {
const versions = getReleaseVersion();
console.log(JSON.stringify(versions));
const command = `npm view @google/gemini-cli@${version} version 2>/dev/null`;
const output = execSync(command).toString().trim();
if (output === version) {
console.error(`Version ${version} already exists on NPM.`);
return true;
}
} catch (_error) {
// This is expected if the version doesn't exist.
}
// Check Git tags
try {
const command = `git tag -l 'v${version}'`;
const tagOutput = execSync(command).toString().trim();
if (tagOutput === `v${version}`) {
console.error(`Git tag v${version} already exists.`);
return true;
}
} catch (error) {
console.error(error.message);
process.exit(1);
console.error(`Failed to check git tags for conflicts: ${error.message}`);
}
// Check GitHub releases
try {
const command = `gh release view "v${version}" --json tagName --jq .tagName 2>/dev/null`;
const output = execSync(command).toString().trim();
if (output === `v${version}`) {
console.error(`GitHub release v${version} already exists.`);
return true;
}
} catch (error) {
const isExpectedNotFound =
error.message.includes('release not found') ||
error.message.includes('Not Found') ||
error.message.includes('not found') ||
error.status === 1;
if (!isExpectedNotFound) {
console.error(
`Failed to check GitHub releases for conflicts: ${error.message}`,
);
}
}
return false;
}
function getAndVerifyTags(npmDistTag, _gitTagPattern) {
// Detect rollback scenarios and get the correct baseline
const rollbackInfo = detectRollbackAndGetBaseline(npmDistTag);
const baselineVersion = rollbackInfo.baseline;
if (!baselineVersion) {
throw new Error(`Unable to determine baseline version for ${npmDistTag}`);
}
if (rollbackInfo.isRollback) {
// Rollback scenario: warn about the rollback but don't fail
console.error(
`Rollback detected! NPM ${npmDistTag} tag is ${rollbackInfo.distTagVersion}, but using ${baselineVersion} as baseline for next version calculation (highest existing version).`,
);
}
// Not verifying against git tags or GitHub releases as per user request.
return {
latestVersion: baselineVersion,
latestTag: `v${baselineVersion}`,
};
}
function promoteNightlyVersion() {
const { latestVersion, latestTag } = getAndVerifyTags(
'nightly',
'v*-nightly*',
);
const baseVersion = latestVersion.split('-')[0];
const versionParts = baseVersion.split('.');
const major = versionParts[0];
const minor = versionParts[1] ? parseInt(versionParts[1]) : 0;
const nextMinor = minor + 1;
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
const gitShortHash = execSync('git rev-parse --short HEAD').toString().trim();
return {
releaseVersion: `${major}.${nextMinor}.0-nightly.${date}.${gitShortHash}`,
npmTag: 'nightly',
previousReleaseTag: latestTag,
};
}
function getNightlyVersion() {
const packageJson = readJson('package.json');
const baseVersion = packageJson.version.split('-')[0];
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
const gitShortHash = execSync('git rev-parse --short HEAD').toString().trim();
const releaseVersion = `${baseVersion}-nightly.${date}.${gitShortHash}`;
const previousReleaseTag = getLatestTag('v*-nightly*');
return {
releaseVersion,
npmTag: 'nightly',
previousReleaseTag,
};
}
function validateVersion(version, format, name) {
const versionRegex = {
'X.Y.Z': /^\d+\.\d+\.\d+$/,
'X.Y.Z-preview.N': /^\d+\.\d+\.\d+-preview\.\d+$/,
};
if (!versionRegex[format] || !versionRegex[format].test(version)) {
throw new Error(
`Invalid ${name}: ${version}. Must be in ${format} format.`,
);
}
}
function getStableVersion(args) {
const { latestVersion: latestPreviewVersion } = getAndVerifyTags(
'preview',
'v*-preview*',
);
let releaseVersion;
if (args.stable_version_override) {
const overrideVersion = args.stable_version_override.replace(/^v/, '');
validateVersion(overrideVersion, 'X.Y.Z', 'stable_version_override');
releaseVersion = overrideVersion;
} else {
releaseVersion = latestPreviewVersion.replace(/-preview.*/, '');
}
const { latestTag: previousStableTag } = getAndVerifyTags(
'latest',
'v[0-9].[0-9].[0-9]',
);
return {
releaseVersion,
npmTag: 'latest',
previousReleaseTag: previousStableTag,
};
}
function getPreviewVersion(args) {
const { latestVersion: latestNightlyVersion } = getAndVerifyTags(
'nightly',
'v*-nightly*',
);
let releaseVersion;
if (args.preview_version_override) {
const overrideVersion = args.preview_version_override.replace(/^v/, '');
validateVersion(
overrideVersion,
'X.Y.Z-preview.N',
'preview_version_override',
);
releaseVersion = overrideVersion;
} else {
releaseVersion =
latestNightlyVersion.replace(/-nightly.*/, '') + '-preview.0';
}
const { latestTag: previousPreviewTag } = getAndVerifyTags(
'preview',
'v*-preview*',
);
return {
releaseVersion,
npmTag: 'preview',
previousReleaseTag: previousPreviewTag,
};
}
function getPatchVersion(patchFrom) {
if (!patchFrom || (patchFrom !== 'stable' && patchFrom !== 'preview')) {
throw new Error(
'Patch type must be specified with --patch-from=stable or --patch-from=preview',
);
}
const distTag = patchFrom === 'stable' ? 'latest' : 'preview';
const pattern = distTag === 'latest' ? 'v[0-9].[0-9].[0-9]' : 'v*-preview*';
const { latestVersion, latestTag } = getAndVerifyTags(distTag, pattern);
if (patchFrom === 'stable') {
// For stable versions, increment the patch number: 0.5.4 -> 0.5.5
const versionParts = latestVersion.split('.');
const major = versionParts[0];
const minor = versionParts[1];
const patch = versionParts[2] ? parseInt(versionParts[2]) : 0;
const releaseVersion = `${major}.${minor}.${patch + 1}`;
return {
releaseVersion,
npmTag: distTag,
previousReleaseTag: latestTag,
};
} else {
// For preview versions, increment the preview number: 0.6.0-preview.2 -> 0.6.0-preview.3
const [version, prereleasePart] = latestVersion.split('-');
if (!prereleasePart || !prereleasePart.startsWith('preview.')) {
throw new Error(
`Invalid preview version format: ${latestVersion}. Expected format like "0.6.0-preview.2"`,
);
}
const previewNumber = parseInt(prereleasePart.split('.')[1]);
if (isNaN(previewNumber)) {
throw new Error(`Could not parse preview number from: ${prereleasePart}`);
}
const releaseVersion = `${version}-preview.${previewNumber + 1}`;
return {
releaseVersion,
npmTag: distTag,
previousReleaseTag: latestTag,
};
}
}
export function getVersion(options = {}) {
const args = { ...getArgs(), ...options };
const type = args.type || 'nightly';
let versionData;
switch (type) {
case 'nightly':
versionData = getNightlyVersion();
// Nightly versions include a git hash, so conflicts are highly unlikely
// and indicate a problem. We'll still validate but not auto-increment.
if (doesVersionExist(versionData.releaseVersion)) {
throw new Error(
`Version conflict! Nightly version ${versionData.releaseVersion} already exists.`,
);
}
break;
case 'promote-nightly':
versionData = promoteNightlyVersion();
break;
case 'stable':
versionData = getStableVersion(args);
break;
case 'preview':
versionData = getPreviewVersion(args);
break;
case 'patch':
versionData = getPatchVersion(args['patch-from']);
break;
default:
throw new Error(`Unknown release type: ${type}`);
}
// For patchable versions, check for existence and increment if needed.
if (type === 'stable' || type === 'preview' || type === 'patch') {
let releaseVersion = versionData.releaseVersion;
while (doesVersionExist(releaseVersion)) {
console.error(`Version ${releaseVersion} exists, incrementing.`);
if (releaseVersion.includes('-preview.')) {
// Increment preview number: 0.6.0-preview.2 -> 0.6.0-preview.3
const [version, prereleasePart] = releaseVersion.split('-');
const previewNumber = parseInt(prereleasePart.split('.')[1]);
releaseVersion = `${version}-preview.${previewNumber + 1}`;
} else {
// Increment patch number: 0.5.4 -> 0.5.5
const versionParts = releaseVersion.split('.');
const major = versionParts[0];
const minor = versionParts[1];
const patch = parseInt(versionParts[2]);
releaseVersion = `${major}.${minor}.${patch + 1}`;
}
}
versionData.releaseVersion = releaseVersion;
}
// All checks are done, construct the final result.
const result = {
releaseTag: `v${versionData.releaseVersion}`,
...versionData,
};
return result;
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
console.log(JSON.stringify(getVersion(getArgs()), null, 2));
}

205
scripts/lint.js Normal file
View File

@@ -0,0 +1,205 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { execSync } from 'node:child_process';
import { mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const ACTIONLINT_VERSION = '1.7.7';
const SHELLCHECK_VERSION = '0.11.0';
const YAMLLINT_VERSION = '1.35.1';
const TEMP_DIR = join(tmpdir(), 'qwen-code-linters');
function getPlatformArch() {
const platform = process.platform;
const arch = process.arch;
if (platform === 'linux' && arch === 'x64') {
return {
actionlint: 'linux_amd64',
shellcheck: 'linux.x86_64',
};
}
if (platform === 'darwin' && arch === 'x64') {
return {
actionlint: 'darwin_amd64',
shellcheck: 'darwin.x86_64',
};
}
if (platform === 'darwin' && arch === 'arm64') {
return {
actionlint: 'darwin_arm64',
shellcheck: 'darwin.aarch64',
};
}
throw new Error(`Unsupported platform/architecture: ${platform}/${arch}`);
}
const platformArch = getPlatformArch();
/**
* @typedef {{
* check: string;
* installer: string;
* run: string;
* }}
*/
/**
* @type {{[linterName: string]: Linter}}
*/
const LINTERS = {
actionlint: {
check: 'command -v actionlint',
installer: `
mkdir -p "${TEMP_DIR}/actionlint"
curl -sSLo "${TEMP_DIR}/.actionlint.tgz" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${platformArch.actionlint}.tar.gz"
tar -xzf "${TEMP_DIR}/.actionlint.tgz" -C "${TEMP_DIR}/actionlint"
`,
run: `
actionlint \
-color \
-ignore 'SC2002:' \
-ignore 'SC2016:' \
-ignore 'SC2129:' \
-ignore 'label ".+" is unknown'
`,
},
shellcheck: {
check: 'command -v shellcheck',
installer: `
mkdir -p "${TEMP_DIR}/shellcheck"
curl -sSLo "${TEMP_DIR}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.${platformArch.shellcheck}.tar.xz"
tar -xf "${TEMP_DIR}/.shellcheck.txz" -C "${TEMP_DIR}/shellcheck" --strip-components=1
`,
run: `
git ls-files | grep -v '^integration-tests/terminal-bench/' | grep -E '^([^.]+|.*\\.(sh|zsh|bash))' | xargs file --mime-type \
| grep "text/x-shellscript" | awk '{ print substr($1, 1, length($1)-1) }' \
| xargs shellcheck \
--check-sourced \
--enable=all \
--exclude=SC2002,SC2129,SC2310 \
--severity=style \
--format=gcc \
--color=never | sed -e 's/note:/warning:/g' -e 's/style:/warning:/g'
`,
},
yamllint: {
check: 'command -v yamllint',
installer: `pip3 install --user "yamllint==${YAMLLINT_VERSION}"`,
run: "git ls-files | grep -E '\\.(yaml|yml)' | xargs yamllint --format github",
},
};
function runCommand(command, stdio = 'inherit') {
try {
const env = { ...process.env };
const nodeBin = join(process.cwd(), 'node_modules', '.bin');
env.PATH = `${nodeBin}:${TEMP_DIR}/actionlint:${TEMP_DIR}/shellcheck:${env.PATH}`;
if (process.platform === 'darwin') {
env.PATH = `${env.PATH}:${process.env.HOME}/Library/Python/3.12/bin`;
} else if (process.platform === 'linux') {
env.PATH = `${env.PATH}:${process.env.HOME}/.local/bin`;
}
execSync(command, { stdio, env });
return true;
} catch (_e) {
return false;
}
}
export function setupLinters() {
console.log('Setting up linters...');
rmSync(TEMP_DIR, { recursive: true, force: true });
mkdirSync(TEMP_DIR, { recursive: true });
for (const linter in LINTERS) {
const { check, installer } = LINTERS[linter];
if (!runCommand(check, 'ignore')) {
console.log(`Installing ${linter}...`);
if (!runCommand(installer)) {
console.error(
`Failed to install ${linter}. Please install it manually.`,
);
process.exit(1);
}
}
}
console.log('All required linters are available.');
}
export function runESLint() {
console.log('\nRunning ESLint...');
if (!runCommand('npm run lint:ci')) {
process.exit(1);
}
}
export function runActionlint() {
console.log('\nRunning actionlint...');
if (!runCommand(LINTERS.actionlint.run)) {
process.exit(1);
}
}
export function runShellcheck() {
console.log('\nRunning shellcheck...');
if (!runCommand(LINTERS.shellcheck.run)) {
process.exit(1);
}
}
export function runYamllint() {
console.log('\nRunning yamllint...');
if (!runCommand(LINTERS.yamllint.run)) {
process.exit(1);
}
}
export function runPrettier() {
console.log('\nRunning Prettier...');
if (!runCommand('prettier --write .')) {
process.exit(1);
}
}
function main() {
const args = process.argv.slice(2);
if (args.includes('--setup')) {
setupLinters();
}
if (args.includes('--eslint')) {
runESLint();
}
if (args.includes('--actionlint')) {
runActionlint();
}
if (args.includes('--shellcheck')) {
runShellcheck();
}
if (args.includes('--yamllint')) {
runYamllint();
}
if (args.includes('--prettier')) {
runPrettier();
}
if (args.length === 0) {
setupLinters();
runESLint();
runActionlint();
runShellcheck();
runYamllint();
runPrettier();
console.log('\nAll linting checks passed!');
}
}
main();

22
scripts/pre-commit.js Normal file
View File

@@ -0,0 +1,22 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { execSync } from 'node:child_process';
import lintStaged from 'lint-staged';
try {
// Get repository root
const root = execSync('git rev-parse --show-toplevel').toString().trim();
// Run lint-staged with API directly
const passed = await lintStaged({ cwd: root });
// Exit with appropriate code
process.exit(passed ? 0 : 1);
} catch {
// Exit with error code
process.exit(1);
}

View File

@@ -10,7 +10,7 @@ import path from 'node:path';
import fs from 'node:fs';
import net from 'node:net';
import os from 'node:os';
import { execSync } from 'node:child_process';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import crypto from 'node:crypto';
@@ -44,10 +44,14 @@ export function getJson(url) {
`qwen-code-releases-${Date.now()}.json`,
);
try {
execSync(
`curl -sL -H "User-Agent: qwen-code-dev-script" -o "${tmpFile}" "${url}"`,
{ stdio: 'pipe' },
const result = spawnSync(
'curl',
['-sL', '-H', 'User-Agent: qwen-code-dev-script', '-o', tmpFile, url],
{ stdio: 'pipe', encoding: 'utf-8' },
);
if (result.status !== 0) {
throw new Error(result.stderr);
}
const content = fs.readFileSync(tmpFile, 'utf-8');
return JSON.parse(content);
} catch (e) {
@@ -62,9 +66,13 @@ export function getJson(url) {
export function downloadFile(url, dest) {
try {
execSync(`curl -fL -sS -o "${dest}" "${url}"`, {
const result = spawnSync('curl', ['-fL', '-sS', '-o', dest, url], {
stdio: 'pipe',
encoding: 'utf-8',
});
if (result.status !== 0) {
throw new Error(result.stderr);
}
return dest;
} catch (e) {
console.error(`Failed to download file from ${url}`);
@@ -252,10 +260,20 @@ export async function ensureBinary(
const actualExt = asset.name.endsWith('.zip') ? 'zip' : 'tar.gz';
let result;
if (actualExt === 'zip') {
execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`, { stdio: 'pipe' });
result = spawnSync('unzip', ['-o', archivePath, '-d', tmpDir], {
stdio: 'pipe',
encoding: 'utf-8',
});
} else {
execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, { stdio: 'pipe' });
result = spawnSync('tar', ['-xzf', archivePath, '-C', tmpDir], {
stdio: 'pipe',
encoding: 'utf-8',
});
}
if (result.status !== 0) {
throw new Error(result.stderr);
}
const nameToFind = binaryNameInArchive || executableName;

View File

@@ -4,146 +4,183 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { getReleaseVersion } from '../get-release-version';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { getVersion } from '../get-release-version.js';
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
// Mock child_process so we can spy on execSync
vi.mock('child_process', () => ({
execSync: vi.fn(),
}));
// Mock fs module
vi.mock('fs', () => ({
default: {
readFileSync: vi.fn(),
},
}));
describe('getReleaseVersion', async () => {
// Dynamically import execSync and fs after mocking
const { execSync } = await import('node:child_process');
const fs = await import('fs');
const originalEnv = { ...process.env };
vi.mock('node:child_process');
vi.mock('node:fs');
describe('getVersion', () => {
beforeEach(() => {
vi.resetAllMocks();
process.env = { ...originalEnv };
// Mock date to be consistent
vi.setSystemTime(new Date('2025-08-20T00:00:00.000Z'));
// Provide a default mock for execSync to avoid toString() on undefined
vi.mocked(execSync).mockReturnValue('');
});
afterEach(() => {
process.env = originalEnv;
vi.useRealTimers();
});
it('should generate a nightly version and get previous tag', () => {
process.env.IS_NIGHTLY = 'true';
vi.mocked(fs.default.readFileSync).mockReturnValue(
JSON.stringify({ version: '0.1.0' }),
vi.setSystemTime(new Date('2025-09-17T00:00:00.000Z'));
// Mock package.json being read by getNightlyVersion
vi.mocked(readFileSync).mockReturnValue(
JSON.stringify({ version: '0.8.0' }),
);
vi.mocked(execSync).mockImplementation((command) => {
if (command.includes('git tag --list "v*.*.*"')) {
return 'v0.1.0\nv0.0.1'; // Mock stable tags for getLatestStableTag
}
if (command.includes('git tag -l "v0.1.1-nightly.*"')) {
return ''; // No existing nightly tags
}
if (command.includes('gh release list')) {
return 'v0.1.0-nightly.5'; // Previous nightly release
}
return '';
});
const { releaseTag, releaseVersion, npmTag, previousReleaseTag } =
getReleaseVersion();
expect(releaseTag).toBe('v0.1.1-nightly.0');
expect(releaseVersion).toBe('0.1.1-nightly.0');
expect(npmTag).toBe('nightly');
expect(previousReleaseTag).toBe('v0.1.0-nightly.5');
});
it('should use the manual version and get previous tag', () => {
process.env.MANUAL_VERSION = 'v0.1.1';
// This is the base mock for a clean state with no conflicts or rollbacks
const mockExecSync = (command) => {
// NPM dist-tags
if (command.includes('npm view') && command.includes('--tag=latest'))
return '0.6.1';
if (command.includes('npm view') && command.includes('--tag=preview'))
return '0.7.0-preview.1';
if (command.includes('npm view') && command.includes('--tag=nightly'))
return '0.8.0-nightly.20250916.abcdef';
vi.mocked(execSync).mockImplementation((command) => {
if (command.includes('gh release list')) {
return 'v0.1.0'; // Previous stable release
}
return '';
// NPM versions list
if (command.includes('npm view') && command.includes('versions --json'))
return JSON.stringify([
'0.6.0',
'0.6.1',
'0.7.0-preview.0',
'0.7.0-preview.1',
'0.8.0-nightly.20250916.abcdef',
]);
// Deprecation checks (default to not deprecated)
if (command.includes('deprecated')) return '';
// Git Tag Mocks
if (command.includes("git tag -l 'v[0-9].[0-9].[0-9]'")) return 'v0.6.1';
if (command.includes("git tag -l 'v*-preview*'")) return 'v0.7.0-preview.1';
if (command.includes("git tag -l 'v*-nightly*'"))
return 'v0.8.0-nightly.20250916.abcdef';
// Git Hash Mock
if (command.includes('git rev-parse --short HEAD')) return 'd3bf8a3d';
// For doesVersionExist checks - default to not found
if (
command.includes('npm view') &&
command.includes('@google/gemini-cli@')
) {
throw new Error('NPM version not found');
}
if (command.includes('git tag -l')) return '';
if (command.includes('gh release view')) {
throw new Error('GH release not found');
}
return '';
};
describe('Happy Path - Version Calculation', () => {
it('should calculate the next stable version from the latest preview', () => {
vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({ type: 'stable' });
expect(result.releaseVersion).toBe('0.7.0');
expect(result.npmTag).toBe('latest');
expect(result.previousReleaseTag).toBe('v0.6.1');
});
const result = getReleaseVersion();
it('should calculate the next preview version from the latest nightly', () => {
vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({ type: 'preview' });
expect(result.releaseVersion).toBe('0.8.0-preview.0');
expect(result.npmTag).toBe('preview');
expect(result.previousReleaseTag).toBe('v0.7.0-preview.1');
});
expect(result).toEqual({
releaseTag: 'v0.1.1',
releaseVersion: '0.1.1',
npmTag: 'latest',
previousReleaseTag: 'v0.1.0',
it('should calculate the next nightly version from package.json', () => {
vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({ type: 'nightly' });
// Note: The base version now comes from package.json, not the previous nightly tag.
expect(result.releaseVersion).toBe('0.8.0-nightly.20250917.d3bf8a3d');
expect(result.npmTag).toBe('nightly');
expect(result.previousReleaseTag).toBe('v0.8.0-nightly.20250916.abcdef');
});
it('should calculate the next patch version for a stable release', () => {
vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({ type: 'patch', 'patch-from': 'stable' });
expect(result.releaseVersion).toBe('0.6.2');
expect(result.npmTag).toBe('latest');
expect(result.previousReleaseTag).toBe('v0.6.1');
});
it('should calculate the next patch version for a preview release', () => {
vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({ type: 'patch', 'patch-from': 'preview' });
expect(result.releaseVersion).toBe('0.7.0-preview.2');
expect(result.npmTag).toBe('preview');
expect(result.previousReleaseTag).toBe('v0.7.0-preview.1');
});
});
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');
});
describe('Advanced Scenarios', () => {
it('should ignore a deprecated version and use the next highest', () => {
const mockWithDeprecated = (command) => {
// The highest nightly is 0.9.0, but it's deprecated
if (command.includes('npm view') && command.includes('versions --json'))
return JSON.stringify([
'0.8.0-nightly.20250916.abcdef',
'0.9.0-nightly.20250917.deprecated', // This one is deprecated
]);
// Mock the deprecation check
if (
command.includes(
'npm view @google/gemini-cli@0.9.0-nightly.20250917.deprecated deprecated',
)
)
return 'This version is deprecated';
// The dist-tag still points to the older, valid version
if (command.includes('npm view') && command.includes('--tag=nightly'))
return '0.8.0-nightly.20250916.abcdef';
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');
});
return mockExecSync(command);
};
vi.mocked(execSync).mockImplementation(mockWithDeprecated);
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/preview release', () => {
expect(() => getReleaseVersion()).toThrow(
'Error: No version specified and this is not a nightly or preview 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.',
);
});
it('should generate nightly version with existing nightly tags', () => {
process.env.IS_NIGHTLY = 'true';
vi.mocked(fs.default.readFileSync).mockReturnValue(
JSON.stringify({ version: '1.2.3' }),
);
vi.mocked(execSync).mockImplementation((command) => {
if (command.includes('git tag --list "v*.*.*"')) {
return 'v1.2.3\nv1.2.2\nv1.2.1\nv1.2.0\nv1.1.0';
}
if (command.includes('git tag -l "v1.2.4-nightly.*"')) {
return 'v1.2.4-nightly.0\nv1.2.4-nightly.1\nv1.2.4-nightly.2'; // Existing nightly tags
}
if (command.includes('gh release list')) {
return 'v1.2.4-nightly.2'; // Previous nightly release
}
return '';
const result = getVersion({ type: 'preview' });
// It should base the preview off 0.8.0, not the deprecated 0.9.0
expect(result.releaseVersion).toBe('0.8.0-preview.0');
});
const result = getReleaseVersion();
it('should auto-increment patch version if the calculated one already exists', () => {
const mockWithConflict = (command) => {
// The calculated version 0.7.0 already exists as a git tag
if (command.includes("git tag -l 'v0.7.0'")) return 'v0.7.0';
// The next version, 0.7.1, is available
if (command.includes("git tag -l 'v0.7.1'")) return '';
expect(result.releaseTag).toBe('v1.2.4-nightly.3');
expect(result.releaseVersion).toBe('1.2.4-nightly.3');
expect(result.npmTag).toBe('nightly');
expect(result.previousReleaseTag).toBe('v1.2.4-nightly.2');
return mockExecSync(command);
};
vi.mocked(execSync).mockImplementation(mockWithConflict);
const result = getVersion({ type: 'stable' });
// Should have skipped 0.7.0 and landed on 0.7.1
expect(result.releaseVersion).toBe('0.7.1');
});
it('should auto-increment preview number if the calculated one already exists', () => {
const mockWithConflict = (command) => {
// The calculated preview 0.8.0-preview.0 already exists on NPM
if (
command.includes(
'npm view @google/gemini-cli@0.8.0-preview.0 version',
)
)
return '0.8.0-preview.0';
// The next one is available
if (
command.includes(
'npm view @google/gemini-cli@0.8.0-preview.1 version',
)
)
throw new Error('Not found');
return mockExecSync(command);
};
vi.mocked(execSync).mockImplementation(mockWithConflict);
const result = getVersion({ type: 'preview' });
// Should have skipped preview.0 and landed on preview.1
expect(result.releaseVersion).toBe('0.8.0-preview.1');
});
});
});

View File

@@ -16,5 +16,11 @@ export default defineConfig({
provider: 'v8',
reporter: ['text', 'lcov'],
},
poolOptions: {
threads: {
minThreads: 8,
maxThreads: 16,
},
},
},
});

View File

@@ -38,9 +38,35 @@ run(`npm version ${versionType} --no-git-tag-version --allow-same-version`);
// 3. Get all workspaces and filter out the one we don't want to version.
const workspacesToExclude = [];
const lsOutput = JSON.parse(
execSync('npm ls --workspaces --json --depth=0').toString(),
);
let lsOutput;
try {
lsOutput = JSON.parse(
execSync('npm ls --workspaces --json --depth=0').toString(),
);
} catch (e) {
// `npm ls` can exit with a non-zero status code if there are issues
// with dependencies, but it will still produce the JSON output we need.
// We'll try to parse the stdout from the error object.
if (e.stdout) {
console.warn(
'Warning: `npm ls` exited with a non-zero status code. Attempting to proceed with the output.',
);
try {
lsOutput = JSON.parse(e.stdout.toString());
} catch (parseError) {
console.error(
'Error: Failed to parse JSON from `npm ls` output even after `npm ls` failed.',
);
console.error('npm ls stderr:', e.stderr.toString());
console.error('Parse error:', parseError);
process.exit(1);
}
} else {
console.error('Error: `npm ls` failed with no output.');
console.error(e.stderr?.toString() || e);
process.exit(1);
}
}
const allWorkspaces = Object.keys(lsOutput.dependencies || {});
const workspacesToVersion = allWorkspaces.filter(
(wsName) => !workspacesToExclude.includes(wsName),
@@ -56,7 +82,7 @@ for (const workspaceName of workspacesToVersion) {
const rootPackageJsonPath = resolve(process.cwd(), 'package.json');
const newVersion = readJson(rootPackageJsonPath).version;
// 6. Update the sandboxImageUri in the root package.json
// 5. Update the sandboxImageUri in the root package.json
const rootPackageJson = readJson(rootPackageJsonPath);
if (rootPackageJson.config?.sandboxImageUri) {
rootPackageJson.config.sandboxImageUri =
@@ -65,7 +91,7 @@ if (rootPackageJson.config?.sandboxImageUri) {
writeJson(rootPackageJsonPath, rootPackageJson);
}
// 7. Update the sandboxImageUri in the cli package.json
// 6. 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) {
@@ -77,7 +103,9 @@ if (cliPackageJson.config?.sandboxImageUri) {
writeJson(cliPackageJsonPath, cliPackageJson);
}
// 8. Run `npm install` to update package-lock.json.
run('npm install');
// 7. Run `npm install` to update package-lock.json.
run(
'npm install --workspace packages/cli --workspace packages/core --package-lock-only',
);
console.log(`Successfully bumped versions to v${newVersion}.`);