qwen-code/scripts/version.js
易良 bbaf13d456
fix(release): pin channel-base dep to exact version during release bump (#7953)
The channel adapters depend on @qwen-code/channel-base via a caret range
(^0.21.0). A prerelease bump such as 0.21.1-preview.0 does not satisfy
that range, so the per-workspace npm version reifies replaced the
workspace link with the stale registry package, and the release build
compiled the adapters against outdated types (missing
PollingChannelBase), failing the publish job.

Pin the adapters' channel-base dependency to the exact new version
during scripts/version.js, refresh the install, and drop the stale
nested copies npm leaves under the adapters.
2026-07-28 16:58:28 +00:00

151 lines
5.4 KiB
JavaScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { execSync } from 'node:child_process';
import {
existsSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { join, resolve } from 'node: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 from the command line arguments.
const versionType = process.argv[2];
if (!versionType) {
console.error('Error: No version specified.');
console.error(
'Usage: npm run version <version> (e.g., 1.2.3 or patch|minor|major|prerelease)',
);
process.exit(1);
}
// 2. Bump the version in the root and all workspace package.json files.
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.
// We intend to maintain sdk and mobile-mcp versions independently.
const workspacesToExclude = ['@qwen-code/sdk', '@qwen-code/mobile-mcp'];
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),
);
for (const workspaceName of workspacesToVersion) {
run(
`npm version ${versionType} --workspace ${workspaceName} --no-git-tag-version --allow-same-version`,
);
}
// 4. Get the new version number from the root package.json
const rootPackageJsonPath = resolve(process.cwd(), 'package.json');
const newVersion = readJson(rootPackageJsonPath).version;
// 5. 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);
}
// 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) {
cliPackageJson.config.sandboxImageUri =
cliPackageJson.config.sandboxImageUri.replace(/:.*$/, `:${newVersion}`);
console.log(
`Updated sandboxImageUri in cli package to use version ${newVersion}`,
);
writeJson(cliPackageJsonPath, cliPackageJson);
}
// 7. Pin channel adapters' semver dependency on @qwen-code/channel-base to
// the exact new version. A caret range like ^0.21.0 does not match a
// prerelease bump (e.g. 0.21.1-preview.0), so npm would replace the workspace
// link with the stale registry package and the release build would compile
// against outdated types.
const channelsDir = resolve(process.cwd(), 'packages/channels');
for (const entry of readdirSync(channelsDir)) {
const pkgPath = join(channelsDir, entry, 'package.json');
if (!existsSync(pkgPath)) continue;
const pkg = readJson(pkgPath);
const dep = pkg.dependencies?.['@qwen-code/channel-base'];
if (dep && !dep.startsWith('file:')) {
pkg.dependencies['@qwen-code/channel-base'] = newVersion;
writeJson(pkgPath, pkg);
console.log(
`Pinned @qwen-code/channel-base to ${newVersion} in ${pkg.name}`,
);
}
}
// 8. Refresh node_modules and package-lock.json against the pinned exact
// versions so the adapters resolve channel-base to the workspace link again.
// --ignore-scripts prevents the root `prepare` lifecycle from triggering a
// redundant full build that fails with TS5055 when dist/ already exists from
// the initial `npm ci` install.
run('npm install --ignore-scripts');
// 9. The per-workspace `npm version` reifies above nested a stale registry
// copy of channel-base under each adapter while ranges briefly mismatched.
// The install above cleans both lockfiles but can leave that directory on
// disk, where it shadows the workspace link during tsc. Remove it.
for (const entry of readdirSync(channelsDir)) {
rmSync(join(channelsDir, entry, 'node_modules', '@qwen-code'), {
recursive: true,
force: true,
});
}
console.log(`Successfully bumped versions to v${newVersion}.`);