mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* fix(scripts): handle missing NPM dist-tags gracefully in release versioning (#6476) getAndVerifyTags now returns null instead of throwing when no baseline version exists on NPM. getPreviewVersion and getStableVersion fall back to the package.json base version, matching the pattern already used by getNightlyVersion. This prevents the release workflow from failing when no nightly or preview dist-tag has been published yet. * fix(scripts): harden release versioning against transient NPM errors and missing dist-tags (#6476) - Distinguish 404 from transient errors in getVersionFromNPM so NPM outages halt the release instead of silently falling back - Consult getAllVersionsFromNPM when dist-tag is missing to derive baseline from published versions rather than returning empty - Add console.error logging when getAndVerifyTags returns null - Validate package.json fallback version in getStableVersion and getPreviewVersion - Add tests for promote-nightly/patch throw paths, true greenfield scenario, versions-list derivation, and transient error propagation * fix(scripts): propagate transient NPM errors in getAllVersionsFromNPM (#6476) getAllVersionsFromNPM silently swallowed all errors including transient network failures (ETIMEDOUT, ECONNRESET), which became load-bearing now that the missing-dist-tag fallback depends on it. Match the same 404-only pattern already used by getVersionFromNPM. Also fix a DRY violation in getPreviewVersion, correct misleading test comments, and add test coverage for the versions-list error path and latest filter branch. * fix(scripts): tighten 404 detection and tolerate transient versions-list failures (#6476) - Remove redundant '404' substring check; E404 is the canonical npm error code and bare '404' could false-match unrelated errors (port 4043, E4040) - Catch transient versions-list errors in detectRollbackAndGetBaseline when distTagVersion is already resolved, avoiding hard-blocking a release when rollback detection is merely a safety net - Update and add tests for the new fallback behavior and deprecated-versions path * fix(scripts): guard release version fallback edge cases * test(scripts): cover greenfield versions list E404 --------- Co-authored-by: Qwen Code Autofix <qwen-code-bot@alibabacloud.com> Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com> Co-authored-by: Qwen Code Autofix <qwen-code@autofix.bot> Co-authored-by: yiliang114 <effortyiliang@gmail.com>
474 lines
14 KiB
JavaScript
474 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { execSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
import semver from 'semver';
|
|
import {
|
|
getArgs,
|
|
isExpectedMissingGitHubRelease,
|
|
readJson,
|
|
validateVersion,
|
|
} from './lib/release-helpers.js';
|
|
|
|
function getVersionFromNPM(distTag) {
|
|
const command = `npm view @qwen-code/qwen-code version --tag=${distTag}`;
|
|
try {
|
|
return execSync(command).toString().trim();
|
|
} catch (error) {
|
|
if (error.message?.includes('E404')) {
|
|
return '';
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function getAllVersionsFromNPM() {
|
|
const command = `npm view @qwen-code/qwen-code versions --json`;
|
|
try {
|
|
const versionsJson = execSync(command).toString().trim();
|
|
return JSON.parse(versionsJson);
|
|
} catch (error) {
|
|
if (error.message?.includes('E404')) {
|
|
return [];
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function isVersionDeprecated(version) {
|
|
const command = `npm view @qwen-code/qwen-code@${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 detectRollbackAndGetBaseline(npmDistTag) {
|
|
// Get the current dist-tag version
|
|
const distTagVersion = getVersionFromNPM(npmDistTag);
|
|
|
|
// Get all published versions
|
|
let allVersions;
|
|
try {
|
|
allVersions = getAllVersionsFromNPM();
|
|
} catch (error) {
|
|
if (distTagVersion) {
|
|
console.error(
|
|
`Could not fetch versions list, proceeding with dist-tag: ${error.message}`,
|
|
);
|
|
return { baseline: distTagVersion, isRollback: false };
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (!distTagVersion) {
|
|
// Dist-tag is missing — try to derive baseline from published versions
|
|
if (allVersions.length === 0) return { baseline: '', isRollback: false };
|
|
|
|
let matchingVersions;
|
|
if (npmDistTag === 'latest') {
|
|
matchingVersions = allVersions.filter(
|
|
(v) => semver.valid(v) && !semver.prerelease(v),
|
|
);
|
|
} else if (npmDistTag === 'preview') {
|
|
matchingVersions = allVersions.filter(
|
|
(v) => semver.valid(v) && v.includes('-preview'),
|
|
);
|
|
} else if (npmDistTag === 'nightly') {
|
|
matchingVersions = allVersions.filter(
|
|
(v) => semver.valid(v) && v.includes('-nightly'),
|
|
);
|
|
} else {
|
|
return { baseline: '', isRollback: false };
|
|
}
|
|
|
|
if (matchingVersions.length === 0)
|
|
return { baseline: '', isRollback: false };
|
|
|
|
matchingVersions.sort((a, b) => semver.rcompare(a, b));
|
|
|
|
let highestExistingVersion = '';
|
|
for (const version of matchingVersions) {
|
|
if (!isVersionDeprecated(version)) {
|
|
highestExistingVersion = version;
|
|
break;
|
|
} else {
|
|
console.error(`Ignoring deprecated version: ${version}`);
|
|
}
|
|
}
|
|
|
|
if (!highestExistingVersion) return { baseline: '', isRollback: false };
|
|
|
|
return {
|
|
baseline: highestExistingVersion,
|
|
isRollback: false,
|
|
highestExistingVersion,
|
|
};
|
|
}
|
|
|
|
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 {
|
|
// For other dist-tags, just use the dist-tag version
|
|
return { baseline: distTagVersion, isRollback: false };
|
|
}
|
|
|
|
if (matchingVersions.length === 0)
|
|
return { baseline: distTagVersion, isRollback: false };
|
|
|
|
// Sort by semver to get a list from highest to lowest
|
|
matchingVersions.sort((a, b) => semver.rcompare(a, b));
|
|
|
|
// 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}`);
|
|
}
|
|
}
|
|
|
|
// If all matching versions were deprecated, fall back to the dist-tag version
|
|
if (!highestExistingVersion) {
|
|
highestExistingVersion = distTagVersion;
|
|
}
|
|
|
|
// Check if we're in a rollback scenario
|
|
const isRollback = semver.gt(highestExistingVersion, distTagVersion);
|
|
|
|
return {
|
|
baseline: isRollback ? highestExistingVersion : distTagVersion,
|
|
isRollback,
|
|
distTagVersion,
|
|
highestExistingVersion,
|
|
};
|
|
}
|
|
|
|
function doesVersionExist(version) {
|
|
// Check NPM
|
|
try {
|
|
const command = `npm view @qwen-code/qwen-code@${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(`Failed to check git tags for conflicts: ${error.message}`);
|
|
}
|
|
|
|
// Check GitHub releases
|
|
try {
|
|
const command = `gh release view "v${version}" --json tagName --jq .tagName`;
|
|
const output = execSync(command).toString().trim();
|
|
if (output === `v${version}`) {
|
|
console.error(`GitHub release v${version} already exists.`);
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
if (!isExpectedMissingGitHubRelease(error)) {
|
|
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) {
|
|
console.error(
|
|
`No baseline version found for dist-tag "${npmDistTag}" — returning null`,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
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 getLatestStableReleaseTag() {
|
|
try {
|
|
const result = getAndVerifyTags('latest', 'v[0-9].[0-9].[0-9]');
|
|
return result ? result.latestTag : '';
|
|
} catch (error) {
|
|
console.error(
|
|
`Failed to determine latest stable release tag: ${error.message}`,
|
|
);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function promoteNightlyVersion() {
|
|
const result = getAndVerifyTags('nightly', 'v*-nightly*');
|
|
if (!result) {
|
|
throw new Error(
|
|
'Unable to determine baseline version for nightly (required for promote-nightly)',
|
|
);
|
|
}
|
|
const baseVersion = result.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',
|
|
};
|
|
}
|
|
|
|
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}`;
|
|
return {
|
|
releaseVersion,
|
|
npmTag: 'nightly',
|
|
};
|
|
}
|
|
|
|
function getStableVersion(args) {
|
|
const tagResult = 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 if (tagResult) {
|
|
releaseVersion = tagResult.latestVersion.replace(/-preview.*/, '');
|
|
validateVersion(releaseVersion, 'X.Y.Z', 'derived from preview dist-tag');
|
|
const latestStable = getVersionFromNPM('latest');
|
|
if (
|
|
latestStable &&
|
|
semver.valid(latestStable) &&
|
|
semver.gt(latestStable, releaseVersion)
|
|
) {
|
|
throw new Error(
|
|
`Derived stable version ${releaseVersion} is lower than published latest ${latestStable}. Refusing retrograde baseline.`,
|
|
);
|
|
}
|
|
} else {
|
|
const packageJson = readJson('package.json');
|
|
releaseVersion = packageJson.version.split('-')[0];
|
|
validateVersion(releaseVersion, 'X.Y.Z', 'package.json version');
|
|
}
|
|
|
|
return {
|
|
releaseVersion,
|
|
npmTag: 'latest',
|
|
};
|
|
}
|
|
|
|
function getPreviewVersion(args) {
|
|
const tagResult = 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 if (tagResult) {
|
|
releaseVersion =
|
|
tagResult.latestVersion.replace(/-nightly.*/, '') + '-preview.0';
|
|
validateVersion(
|
|
releaseVersion,
|
|
'X.Y.Z-preview.N',
|
|
'derived from nightly dist-tag',
|
|
);
|
|
} else {
|
|
const packageJson = readJson('package.json');
|
|
const baseVersion = packageJson.version.split('-')[0];
|
|
releaseVersion = baseVersion + '-preview.0';
|
|
validateVersion(baseVersion, 'X.Y.Z', 'package.json version');
|
|
}
|
|
|
|
return {
|
|
releaseVersion,
|
|
npmTag: 'preview',
|
|
};
|
|
}
|
|
|
|
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 tagResult = getAndVerifyTags(distTag, pattern);
|
|
if (!tagResult) {
|
|
throw new Error(
|
|
`Unable to determine baseline version for ${distTag} (required for patch)`,
|
|
);
|
|
}
|
|
const { latestVersion } = tagResult;
|
|
|
|
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,
|
|
};
|
|
} 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,
|
|
};
|
|
}
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
result.previousReleaseTag = getLatestStableReleaseTag();
|
|
|
|
return result;
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
console.log(JSON.stringify(getVersion(getArgs()), null, 2));
|
|
}
|