mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-09 02:50:58 +00:00
Move four duplicated utility functions (getArgs, readJson,
validateVersion, isExpectedMissingGitHubRelease) from the three
get-release-version.js scripts into a shared module at
scripts/lib/release-helpers.js so that changes only need to happen
in one place.
Also fixes a pre-existing bug in getArgs where argument values
containing '=' were silently truncated (e.g. --msg=a=b produced
{msg:'a'} instead of {msg:'a=b'}).
Closes #3795
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
542 lines
15 KiB
JavaScript
542 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { execSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
getArgs,
|
|
isExpectedMissingGitHubRelease,
|
|
validateVersion,
|
|
} from '../../../scripts/lib/release-helpers.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
const PACKAGE_NAME = 'qwen-code-sdk';
|
|
const TAG_PREFIX = 'sdk-python-';
|
|
|
|
function readPyprojectVersion() {
|
|
const pyprojectPath = join(__dirname, '..', 'pyproject.toml');
|
|
const content = readFileSync(pyprojectPath, 'utf8');
|
|
const match = content.match(/^version = "([^"]+)"$/m);
|
|
if (!match) {
|
|
throw new Error(`Could not find version in ${pyprojectPath}`);
|
|
}
|
|
return match[1];
|
|
}
|
|
|
|
function parseVersion(version) {
|
|
let match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
if (match) {
|
|
return {
|
|
major: Number(match[1]),
|
|
minor: Number(match[2]),
|
|
patch: Number(match[3]),
|
|
stage: 'stable',
|
|
stageNumber: 0,
|
|
raw: version,
|
|
};
|
|
}
|
|
|
|
match = version.match(/^(\d+)\.(\d+)\.(\d+)rc(\d+)$/);
|
|
if (match) {
|
|
return {
|
|
major: Number(match[1]),
|
|
minor: Number(match[2]),
|
|
patch: Number(match[3]),
|
|
stage: 'preview',
|
|
stageNumber: Number(match[4]),
|
|
raw: version,
|
|
};
|
|
}
|
|
|
|
match = version.match(/^(\d+)\.(\d+)\.(\d+)\.dev(\d+)$/);
|
|
if (match) {
|
|
return {
|
|
major: Number(match[1]),
|
|
minor: Number(match[2]),
|
|
patch: Number(match[3]),
|
|
stage: 'nightly',
|
|
stageNumber: Number(match[4]),
|
|
raw: version,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function compareVersions(a, b) {
|
|
const parsedA = parseVersion(a);
|
|
const parsedB = parseVersion(b);
|
|
if (!parsedA || !parsedB) {
|
|
throw new Error(`Cannot compare unsupported versions: ${a}, ${b}`);
|
|
}
|
|
|
|
if (parsedA.major !== parsedB.major) {
|
|
return parsedA.major - parsedB.major;
|
|
}
|
|
if (parsedA.minor !== parsedB.minor) {
|
|
return parsedA.minor - parsedB.minor;
|
|
}
|
|
if (parsedA.patch !== parsedB.patch) {
|
|
return parsedA.patch - parsedB.patch;
|
|
}
|
|
|
|
const stageOrder = {
|
|
nightly: 0,
|
|
preview: 1,
|
|
stable: 2,
|
|
};
|
|
|
|
if (stageOrder[parsedA.stage] !== stageOrder[parsedB.stage]) {
|
|
return stageOrder[parsedA.stage] - stageOrder[parsedB.stage];
|
|
}
|
|
|
|
return parsedA.stageNumber - parsedB.stageNumber;
|
|
}
|
|
|
|
function sortDescending(versions) {
|
|
return [...versions].sort((a, b) => compareVersions(b, a));
|
|
}
|
|
|
|
function toBaseVersion(version) {
|
|
const parsed = parseVersion(version);
|
|
if (!parsed) {
|
|
throw new Error(`Unsupported version format: ${version}`);
|
|
}
|
|
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
}
|
|
|
|
async function getAllVersionsFromPyPI() {
|
|
const response = await fetch(`https://pypi.org/pypi/${PACKAGE_NAME}/json`, {
|
|
headers: { Accept: 'application/json' },
|
|
signal: AbortSignal.timeout(30_000),
|
|
});
|
|
|
|
if (response.status === 404) {
|
|
return { versions: [], allVersions: [] };
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Failed to fetch PyPI metadata: ${response.status} ${response.statusText}`,
|
|
);
|
|
}
|
|
|
|
const payload = await response.json();
|
|
const releases = payload.releases ?? {};
|
|
const allVersions = Object.keys(releases).filter(
|
|
(version) => parseVersion(version) !== null,
|
|
);
|
|
// Yanked versions still occupy PyPI slots (re-upload fails), so allVersions
|
|
// includes them for conflict detection. The filtered list excludes yanked
|
|
// versions so base-version computation uses only live releases.
|
|
const versions = allVersions.filter((version) => {
|
|
const files = releases[version];
|
|
if (Array.isArray(files) && files.length > 0) {
|
|
return !files.every((file) => file.yanked === true);
|
|
}
|
|
return true;
|
|
});
|
|
return { versions, allVersions };
|
|
}
|
|
|
|
function getCurrentPackageBaseVersion() {
|
|
return toBaseVersion(readPyprojectVersion());
|
|
}
|
|
|
|
function getLatestStableVersion(versions) {
|
|
const stableVersions = versions.filter(
|
|
(version) => parseVersion(version)?.stage === 'stable',
|
|
);
|
|
|
|
if (stableVersions.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
return sortDescending(stableVersions)[0];
|
|
}
|
|
|
|
function getLatestPreviewBaseVersion(versions) {
|
|
const previewVersions = versions.filter(
|
|
(version) => parseVersion(version)?.stage === 'preview',
|
|
);
|
|
|
|
if (previewVersions.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
return toBaseVersion(sortDescending(previewVersions)[0]);
|
|
}
|
|
|
|
function getLatestNightlyBaseVersion(versions) {
|
|
const nightlyVersions = versions.filter(
|
|
(version) => parseVersion(version)?.stage === 'nightly',
|
|
);
|
|
|
|
if (nightlyVersions.length === 0) {
|
|
return '';
|
|
}
|
|
|
|
return toBaseVersion(sortDescending(nightlyVersions)[0]);
|
|
}
|
|
|
|
function incrementPatchVersion(version) {
|
|
const parsed = parseVersion(version);
|
|
if (!parsed) {
|
|
throw new Error(`Unsupported baseline version: ${version}`);
|
|
}
|
|
return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;
|
|
}
|
|
|
|
function getNextBaseVersion(versions) {
|
|
const stableVersions = versions.filter(
|
|
(version) => parseVersion(version)?.stage === 'stable',
|
|
);
|
|
const stableBaseline = sortDescending([
|
|
...stableVersions,
|
|
getCurrentPackageBaseVersion(),
|
|
])[0];
|
|
const latestPrereleaseBase = sortDescending(
|
|
[
|
|
getLatestPreviewBaseVersion(versions),
|
|
getLatestNightlyBaseVersion(versions),
|
|
].filter(Boolean),
|
|
)[0];
|
|
|
|
if (
|
|
latestPrereleaseBase &&
|
|
compareVersions(latestPrereleaseBase, stableBaseline) >= 0
|
|
) {
|
|
return latestPrereleaseBase;
|
|
}
|
|
|
|
// On first release (no stable versions on PyPI), use the pyproject.toml
|
|
// version directly instead of incrementing it.
|
|
if (stableVersions.length === 0) {
|
|
return stableBaseline;
|
|
}
|
|
|
|
return incrementPatchVersion(stableBaseline);
|
|
}
|
|
|
|
function getUtcTimestamp() {
|
|
const now = new Date();
|
|
const pad = (value) => String(value).padStart(2, '0');
|
|
return [
|
|
now.getUTCFullYear(),
|
|
pad(now.getUTCMonth() + 1),
|
|
pad(now.getUTCDate()),
|
|
pad(now.getUTCHours()),
|
|
pad(now.getUTCMinutes()),
|
|
pad(now.getUTCSeconds()),
|
|
].join('');
|
|
}
|
|
|
|
function getGitShortHash() {
|
|
return execSync('git rev-parse --short HEAD').toString().trim();
|
|
}
|
|
|
|
async function getReleaseState({ packageVersion, releaseTag }, allVersions) {
|
|
const state = {
|
|
packageVersionExistsOnPyPI: allVersions.includes(packageVersion),
|
|
gitTagExists: false,
|
|
githubReleaseExists: false,
|
|
};
|
|
const fullTag = `${TAG_PREFIX}${releaseTag}`;
|
|
try {
|
|
const tagOutput = execSync(`git tag -l '${fullTag}'`).toString().trim();
|
|
if (tagOutput === fullTag) {
|
|
state.gitTagExists = true;
|
|
}
|
|
} catch (error) {
|
|
throw new Error(`Failed to check git tags for conflicts: ${error.message}`);
|
|
}
|
|
|
|
try {
|
|
const output = execSync(
|
|
`gh release view "${fullTag}" --json tagName --jq .tagName`,
|
|
)
|
|
.toString()
|
|
.trim();
|
|
if (output === fullTag) {
|
|
state.githubReleaseExists = true;
|
|
}
|
|
} catch (error) {
|
|
if (!isExpectedMissingGitHubRelease(error)) {
|
|
throw new Error(
|
|
`Failed to check GitHub releases for conflicts: ${error.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
function getNightlyVersion(versions) {
|
|
const baseVersion = getNextBaseVersion(versions);
|
|
const timestamp = getUtcTimestamp();
|
|
const gitShortHash = getGitShortHash();
|
|
|
|
return {
|
|
releaseVersion: `${baseVersion}-nightly.${timestamp}.${gitShortHash}`,
|
|
packageVersion: `${baseVersion}.dev${timestamp}`,
|
|
publishChannel: 'nightly',
|
|
};
|
|
}
|
|
|
|
function getPreviewVersion(args, versions) {
|
|
if (args.preview_version_override) {
|
|
const overrideVersion = args.preview_version_override.replace(/^v/, '');
|
|
validateVersion(
|
|
overrideVersion,
|
|
'X.Y.Z-preview.N',
|
|
'preview_version_override',
|
|
);
|
|
const match = overrideVersion.match(/^(\d+\.\d+\.\d+)-preview\.(\d+)$/);
|
|
if (!match) {
|
|
throw new Error(`Invalid preview override: ${overrideVersion}`);
|
|
}
|
|
return {
|
|
releaseVersion: overrideVersion,
|
|
packageVersion: `${match[1]}rc${match[2]}`,
|
|
publishChannel: 'preview',
|
|
};
|
|
}
|
|
|
|
const baseVersion = getNextBaseVersion(versions);
|
|
return {
|
|
releaseVersion: `${baseVersion}-preview.0`,
|
|
packageVersion: `${baseVersion}rc0`,
|
|
publishChannel: 'preview',
|
|
};
|
|
}
|
|
|
|
function getStableVersion(args, versions) {
|
|
if (args.stable_version_override) {
|
|
const overrideVersion = args.stable_version_override.replace(/^v/, '');
|
|
validateVersion(overrideVersion, 'X.Y.Z', 'stable_version_override');
|
|
const latestStable = getLatestStableVersion(versions);
|
|
if (latestStable && compareVersions(overrideVersion, latestStable) < 0) {
|
|
throw new Error(
|
|
`stable_version_override ${overrideVersion} is older than latest stable ${latestStable}. ` +
|
|
`Publishing an older stable version is unusual — provide a newer version or contact a maintainer.`,
|
|
);
|
|
}
|
|
return {
|
|
releaseVersion: overrideVersion,
|
|
packageVersion: overrideVersion,
|
|
publishChannel: 'latest',
|
|
};
|
|
}
|
|
|
|
const latestPrerelease = [
|
|
{ baseVersion: getLatestPreviewBaseVersion(versions), source: 'preview' },
|
|
{ baseVersion: getLatestNightlyBaseVersion(versions), source: 'nightly' },
|
|
]
|
|
.filter(({ baseVersion }) => Boolean(baseVersion))
|
|
.sort((a, b) => compareVersions(b.baseVersion, a.baseVersion))[0];
|
|
const latestStable = getLatestStableVersion(versions);
|
|
|
|
if (latestPrerelease) {
|
|
if (latestPrerelease.source !== 'preview') {
|
|
console.error(
|
|
`::warning::Stable release ${latestPrerelease.baseVersion} derived from ${latestPrerelease.source} (no preview release found with this base version).`,
|
|
);
|
|
}
|
|
if (
|
|
latestStable &&
|
|
compareVersions(latestPrerelease.baseVersion, latestStable) < 0
|
|
) {
|
|
throw new Error(
|
|
`Latest ${latestPrerelease.source} base ${latestPrerelease.baseVersion} is not newer than latest stable ${latestStable}. Provide stable_version_override to continue.`,
|
|
);
|
|
}
|
|
return {
|
|
releaseVersion: latestPrerelease.baseVersion,
|
|
packageVersion: latestPrerelease.baseVersion,
|
|
publishChannel: 'latest',
|
|
source: latestPrerelease.source,
|
|
};
|
|
}
|
|
|
|
const releaseVersion = getCurrentPackageBaseVersion();
|
|
return {
|
|
releaseVersion,
|
|
packageVersion: releaseVersion,
|
|
publishChannel: 'latest',
|
|
source: 'current',
|
|
};
|
|
}
|
|
|
|
function getConflictSources(releaseState) {
|
|
const sources = [];
|
|
if (releaseState.packageVersionExistsOnPyPI) {
|
|
sources.push('PyPI');
|
|
}
|
|
if (releaseState.githubReleaseExists) {
|
|
sources.push('GitHub releases');
|
|
}
|
|
if (releaseState.gitTagExists) {
|
|
sources.push('git tags');
|
|
}
|
|
return sources.length > 0 ? sources.join(', ') : 'unknown release state';
|
|
}
|
|
|
|
function bumpVersion(versionData) {
|
|
const match = versionData.releaseVersion.match(
|
|
/^(\d+\.\d+\.\d+)-preview\.(\d+)$/,
|
|
);
|
|
if (!match) {
|
|
throw new Error(
|
|
`Cannot bump preview version: ${versionData.releaseVersion}`,
|
|
);
|
|
}
|
|
const nextNumber = Number(match[2]) + 1;
|
|
return {
|
|
...versionData,
|
|
releaseVersion: `${match[1]}-preview.${nextNumber}`,
|
|
packageVersion: `${match[1]}rc${nextNumber}`,
|
|
};
|
|
}
|
|
|
|
async function getVersion(options = {}) {
|
|
const args = { ...getArgs(), ...options };
|
|
const type = args.type || 'nightly';
|
|
const { versions, allVersions } = await getAllVersionsFromPyPI();
|
|
const hasManualOverride =
|
|
(type === 'preview' && Boolean(args.preview_version_override)) ||
|
|
(type === 'stable' && Boolean(args.stable_version_override));
|
|
|
|
let versionData;
|
|
let resumeExistingRelease = false;
|
|
switch (type) {
|
|
case 'nightly':
|
|
versionData = getNightlyVersion(versions);
|
|
break;
|
|
case 'preview':
|
|
versionData = getPreviewVersion(args, versions);
|
|
break;
|
|
case 'stable':
|
|
versionData = getStableVersion(args, versions);
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown release type: ${type}`);
|
|
}
|
|
|
|
while (true) {
|
|
const releaseState = await getReleaseState(
|
|
{
|
|
packageVersion: versionData.packageVersion,
|
|
releaseTag: `v${versionData.releaseVersion}`,
|
|
},
|
|
allVersions,
|
|
);
|
|
|
|
const versionExists =
|
|
releaseState.packageVersionExistsOnPyPI ||
|
|
releaseState.gitTagExists ||
|
|
releaseState.githubReleaseExists;
|
|
if (!versionExists) {
|
|
break;
|
|
}
|
|
|
|
if (
|
|
!hasManualOverride &&
|
|
releaseState.packageVersionExistsOnPyPI &&
|
|
!releaseState.githubReleaseExists
|
|
) {
|
|
console.error(
|
|
`PyPI version ${versionData.packageVersion} already exists without a matching GitHub release. Reusing the same release version.`,
|
|
);
|
|
resumeExistingRelease = true;
|
|
break;
|
|
}
|
|
|
|
if (
|
|
!hasManualOverride &&
|
|
type === 'stable' &&
|
|
releaseState.packageVersionExistsOnPyPI &&
|
|
releaseState.githubReleaseExists
|
|
) {
|
|
console.error(
|
|
`Stable release ${versionData.releaseVersion} already has a matching GitHub release. Reusing the same release version for post-release recovery.`,
|
|
);
|
|
resumeExistingRelease = true;
|
|
break;
|
|
}
|
|
|
|
if (hasManualOverride) {
|
|
throw new Error(
|
|
`Requested ${type} release ${versionData.releaseVersion} already exists on ${getConflictSources(releaseState)}.`,
|
|
);
|
|
}
|
|
|
|
if (releaseState.githubReleaseExists) {
|
|
console.error(
|
|
`GitHub release ${TAG_PREFIX}v${versionData.releaseVersion} already exists.`,
|
|
);
|
|
} else if (releaseState.gitTagExists) {
|
|
console.error(
|
|
`::warning::Orphan git tag ${TAG_PREFIX}v${versionData.releaseVersion} exists without a PyPI version or GitHub release. Skipping to next version slot.`,
|
|
);
|
|
} else if (releaseState.packageVersionExistsOnPyPI) {
|
|
console.error(
|
|
`PyPI version ${versionData.packageVersion} already exists.`,
|
|
);
|
|
}
|
|
|
|
if (type === 'stable') {
|
|
if (
|
|
versionData.source === 'preview' ||
|
|
versionData.source === 'nightly'
|
|
) {
|
|
throw new Error(
|
|
`Stable release ${versionData.releaseVersion} derived from the latest ${versionData.source} already exists.`,
|
|
);
|
|
}
|
|
|
|
throw new Error(
|
|
`Stable release ${versionData.releaseVersion} already exists. Provide stable_version_override to release a different stable version.`,
|
|
);
|
|
}
|
|
|
|
if (type === 'nightly') {
|
|
throw new Error(
|
|
`Nightly version conflict for ${versionData.packageVersion}`,
|
|
);
|
|
}
|
|
|
|
versionData = bumpVersion(versionData);
|
|
}
|
|
|
|
const previousVersion =
|
|
type === 'stable'
|
|
? getLatestStableVersion(
|
|
versions.filter((v) => v !== versionData.releaseVersion),
|
|
)
|
|
: '';
|
|
|
|
return {
|
|
releaseTag: `v${versionData.releaseVersion}`,
|
|
releaseVersion: versionData.releaseVersion,
|
|
packageVersion: versionData.packageVersion,
|
|
previousReleaseTag: previousVersion ? `v${previousVersion}` : '',
|
|
publishChannel: versionData.publishChannel,
|
|
resumeExistingRelease,
|
|
};
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
const result = await getVersion(getArgs());
|
|
console.log(JSON.stringify(result, null, 2));
|
|
}
|
|
|
|
export { getVersion };
|