basant307/AI_Governance_Project
045
1#!/usr/bin/env node2 3/**4 * @license5 * Copyright 2026 Qwen Team6 * SPDX-License-Identifier: Apache-2.07 */8 9import { execSync } from 'node:child_process';10import { readFileSync } from 'node:fs';11import { dirname, join } from 'node:path';12import { fileURLToPath } from 'node:url';13import {14 getArgs,15 isExpectedMissingGitHubRelease,16 validateVersion,17} from '../../../scripts/lib/release-helpers.js';18 19const __filename = fileURLToPath(import.meta.url);20const __dirname = dirname(__filename);21 22const PACKAGE_NAME = 'qwen-code-sdk';23const TAG_PREFIX = 'sdk-python-v';24const NETWORK_COMMAND_TIMEOUT_MS = 30_000;25const LOCAL_COMMAND_TIMEOUT_MS = 10_000;26 27function readPyprojectVersion() {28 const pyprojectPath = join(__dirname, '..', 'pyproject.toml');29 const content = readFileSync(pyprojectPath, 'utf8');30 const match = content.match(/^version = "([^"]+)"$/m);31 if (!match) {32 throw new Error(`Could not find version in ${pyprojectPath}`);33 }34 return match[1];35}36 37function parseVersion(version) {38 let match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);39 if (match) {40 return {41 major: Number(match[1]),42 minor: Number(match[2]),43 patch: Number(match[3]),44 stage: 'stable',45 stageNumber: 0,46 raw: version,47 };48 }49 50 match = version.match(/^(\d+)\.(\d+)\.(\d+)rc(\d+)$/);51 if (match) {52 return {53 major: Number(match[1]),54 minor: Number(match[2]),55 patch: Number(match[3]),56 stage: 'preview',57 stageNumber: Number(match[4]),58 raw: version,59 };60 }61 62 match = version.match(/^(\d+)\.(\d+)\.(\d+)\.dev(\d+)$/);63 if (match) {64 return {65 major: Number(match[1]),66 minor: Number(match[2]),67 patch: Number(match[3]),68 stage: 'nightly',69 stageNumber: Number(match[4]),70 raw: version,71 };72 }73 74 return null;75}76 77function compareVersions(a, b) {78 const parsedA = parseVersion(a);79 const parsedB = parseVersion(b);80 if (!parsedA || !parsedB) {81 throw new Error(`Cannot compare unsupported versions: ${a}, ${b}`);82 }83 84 if (parsedA.major !== parsedB.major) {85 return parsedA.major - parsedB.major;86 }87 if (parsedA.minor !== parsedB.minor) {88 return parsedA.minor - parsedB.minor;89 }90 if (parsedA.patch !== parsedB.patch) {91 return parsedA.patch - parsedB.patch;92 }93 94 const stageOrder = {95 nightly: 0,96 preview: 1,97 stable: 2,98 };99 100 if (stageOrder[parsedA.stage] !== stageOrder[parsedB.stage]) {101 return stageOrder[parsedA.stage] - stageOrder[parsedB.stage];102 }103 104 return parsedA.stageNumber - parsedB.stageNumber;105}106 107function sortDescending(versions) {108 return [...versions].sort((a, b) => compareVersions(b, a));109}110 111function toBaseVersion(version) {112 const parsed = parseVersion(version);113 if (!parsed) {114 throw new Error(`Unsupported version format: ${version}`);115 }116 return `${parsed.major}.${parsed.minor}.${parsed.patch}`;117}118 119async function getAllVersionsFromPyPI() {120 const response = await fetch(`https://pypi.org/pypi/${PACKAGE_NAME}/json`, {121 headers: { Accept: 'application/json' },122 signal: AbortSignal.timeout(NETWORK_COMMAND_TIMEOUT_MS),123 });124 125 if (response.status === 404) {126 return { versions: [], allVersions: [] };127 }128 129 if (!response.ok) {130 throw new Error(131 `Failed to fetch PyPI metadata: ${response.status} ${response.statusText}`,132 );133 }134 135 const payload = await response.json();136 const releases = payload.releases ?? {};137 const allVersions = Object.keys(releases).filter(138 (version) => parseVersion(version) !== null,139 );140 // Yanked versions still occupy PyPI slots (re-upload fails), so allVersions141 // includes them for conflict detection. The filtered list excludes yanked142 // versions so base-version computation uses only live releases.143 const versions = allVersions.filter((version) => {144 const files = releases[version];145 if (Array.isArray(files) && files.length > 0) {146 return !files.every((file) => file.yanked === true);147 }148 return true;149 });150 return { versions, allVersions };151}152 153function getCurrentPackageBaseVersion() {154 return toBaseVersion(readPyprojectVersion());155}156 157function getLatestStableVersion(versions) {158 const stableVersions = versions.filter(159 (version) => parseVersion(version)?.stage === 'stable',160 );161 162 if (stableVersions.length === 0) {163 return '';164 }165 166 return sortDescending(stableVersions)[0];167}168 169function getLatestPreviewBaseVersion(versions) {170 const previewVersions = versions.filter(171 (version) => parseVersion(version)?.stage === 'preview',172 );173 174 if (previewVersions.length === 0) {175 return '';176 }177 178 return toBaseVersion(sortDescending(previewVersions)[0]);179}180 181function getLatestNightlyBaseVersion(versions) {182 const nightlyVersions = versions.filter(183 (version) => parseVersion(version)?.stage === 'nightly',184 );185 186 if (nightlyVersions.length === 0) {187 return '';188 }189 190 return toBaseVersion(sortDescending(nightlyVersions)[0]);191}192 193function incrementPatchVersion(version) {194 const parsed = parseVersion(version);195 if (!parsed) {196 throw new Error(`Unsupported baseline version: ${version}`);197 }198 return `${parsed.major}.${parsed.minor}.${parsed.patch + 1}`;199}200 201function getNextBaseVersion(versions) {202 const stableVersions = versions.filter(203 (version) => parseVersion(version)?.stage === 'stable',204 );205 const stableBaseline = sortDescending([206 ...stableVersions,207 getCurrentPackageBaseVersion(),208 ])[0];209 const latestPrereleaseBase = sortDescending(210 [211 getLatestPreviewBaseVersion(versions),212 getLatestNightlyBaseVersion(versions),213 ].filter(Boolean),214 )[0];215 216 if (217 latestPrereleaseBase &&218 compareVersions(latestPrereleaseBase, stableBaseline) >= 0219 ) {220 return latestPrereleaseBase;221 }222 223 // On first release (no stable versions on PyPI), use the pyproject.toml224 // version directly instead of incrementing it.225 if (stableVersions.length === 0) {226 return stableBaseline;227 }228 229 return incrementPatchVersion(stableBaseline);230}231 232function getUtcTimestamp() {233 const now = new Date();234 const pad = (value) => String(value).padStart(2, '0');235 return [236 now.getUTCFullYear(),237 pad(now.getUTCMonth() + 1),238 pad(now.getUTCDate()),239 pad(now.getUTCHours()),240 pad(now.getUTCMinutes()),241 pad(now.getUTCSeconds()),242 ].join('');243}244 245function getGitShortHash() {246 try {247 return execSync('git rev-parse --short HEAD', {248 timeout: LOCAL_COMMAND_TIMEOUT_MS,249 })250 .toString()251 .trim();252 } catch (error) {253 if (isTimeoutError(error)) {254 throw new Error(255 `git rev-parse timed out after ${LOCAL_COMMAND_TIMEOUT_MS / 1000}s — local git may be unresponsive`,256 );257 }258 throw error;259 }260}261 262function isTimeoutError(error) {263 // Node.js execSync timeout: `code` is 'ETIMEDOUT' on POSIX; on some264 // versions/platforms `killed` is true with signal 'SIGTERM' or null.265 // Match the pattern used in packages/core/src/utils/pdf.ts.266 return (267 error.code === 'ETIMEDOUT' ||268 (error.killed === true &&269 (error.signal === 'SIGTERM' ||270 error.signal === undefined ||271 error.signal === null))272 );273}274 275async function getReleaseState(276 { packageVersion, releaseVersion },277 allVersions,278) {279 const state = {280 packageVersionExistsOnPyPI: allVersions.includes(packageVersion),281 gitTagExists: false,282 githubReleaseExists: false,283 };284 const fullTag = `${TAG_PREFIX}${releaseVersion}`;285 try {286 const tagOutput = execSync(`git tag -l '${fullTag}'`, {287 timeout: LOCAL_COMMAND_TIMEOUT_MS,288 })289 .toString()290 .trim();291 if (tagOutput === fullTag) {292 state.gitTagExists = true;293 }294 } catch (error) {295 if (isTimeoutError(error)) {296 throw new Error(297 `git tag -l timed out after ${LOCAL_COMMAND_TIMEOUT_MS / 1000}s — local git may be unresponsive`,298 );299 }300 throw new Error(`Failed to check git tags for conflicts: ${error.message}`);301 }302 303 try {304 const output = execSync(305 `gh release view "${fullTag}" --json tagName --jq .tagName`,306 { timeout: NETWORK_COMMAND_TIMEOUT_MS },307 )308 .toString()309 .trim();310 if (output === fullTag) {311 state.githubReleaseExists = true;312 }313 } catch (error) {314 // Timeout check must precede isExpectedMissingGitHubRelease — a timed-out315 // process may emit partial stderr matching "release not found".316 if (isTimeoutError(error)) {317 throw new Error(318 `gh release view timed out after ${NETWORK_COMMAND_TIMEOUT_MS / 1000}s checking "${fullTag}" — GitHub API may be unavailable`,319 );320 }321 if (!isExpectedMissingGitHubRelease(error)) {322 throw new Error(323 `Failed to check GitHub releases for conflicts: ${error.message}`,324 );325 }326 }327 328 return state;329}330 331function getNightlyVersion(versions) {332 const baseVersion = getNextBaseVersion(versions);333 const timestamp = getUtcTimestamp();334 const gitShortHash = getGitShortHash();335 336 return {337 releaseVersion: `${baseVersion}-nightly.${timestamp}.${gitShortHash}`,338 packageVersion: `${baseVersion}.dev${timestamp}`,339 publishChannel: 'nightly',340 };341}342 343function getPreviewVersion(args, versions) {344 if (args.preview_version_override) {345 const overrideVersion = args.preview_version_override.replace(/^v/, '');346 validateVersion(347 overrideVersion,348 'X.Y.Z-preview.N',349 'preview_version_override',350 );351 const match = overrideVersion.match(/^(\d+\.\d+\.\d+)-preview\.(\d+)$/);352 if (!match) {353 throw new Error(`Invalid preview override: ${overrideVersion}`);354 }355 return {356 releaseVersion: overrideVersion,357 packageVersion: `${match[1]}rc${match[2]}`,358 publishChannel: 'preview',359 };360 }361 362 const baseVersion = getNextBaseVersion(versions);363 return {364 releaseVersion: `${baseVersion}-preview.0`,365 packageVersion: `${baseVersion}rc0`,366 publishChannel: 'preview',367 };368}369 370function getStableVersion(args, versions) {371 if (args.stable_version_override) {372 const overrideVersion = args.stable_version_override.replace(/^v/, '');373 validateVersion(overrideVersion, 'X.Y.Z', 'stable_version_override');374 const latestStable = getLatestStableVersion(versions);375 if (latestStable && compareVersions(overrideVersion, latestStable) < 0) {376 throw new Error(377 `stable_version_override ${overrideVersion} is older than latest stable ${latestStable}. ` +378 `Publishing an older stable version is unusual — provide a newer version or contact a maintainer.`,379 );380 }381 return {382 releaseVersion: overrideVersion,383 packageVersion: overrideVersion,384 publishChannel: 'latest',385 };386 }387 388 const latestPrerelease = [389 { baseVersion: getLatestPreviewBaseVersion(versions), source: 'preview' },390 { baseVersion: getLatestNightlyBaseVersion(versions), source: 'nightly' },391 ]392 .filter(({ baseVersion }) => Boolean(baseVersion))393 .sort((a, b) => compareVersions(b.baseVersion, a.baseVersion))[0];394 const latestStable = getLatestStableVersion(versions);395 396 if (latestPrerelease) {397 if (latestPrerelease.source !== 'preview') {398 console.error(399 `::warning::Stable release ${latestPrerelease.baseVersion} derived from ${latestPrerelease.source} (no preview release found with this base version).`,400 );401 }402 if (403 latestStable &&404 compareVersions(latestPrerelease.baseVersion, latestStable) < 0405 ) {406 throw new Error(407 `Latest ${latestPrerelease.source} base ${latestPrerelease.baseVersion} is not newer than latest stable ${latestStable}. Provide stable_version_override to continue.`,408 );409 }410 return {411 releaseVersion: latestPrerelease.baseVersion,412 packageVersion: latestPrerelease.baseVersion,413 publishChannel: 'latest',414 source: latestPrerelease.source,415 };416 }417 418 const releaseVersion = getCurrentPackageBaseVersion();419 return {420 releaseVersion,421 packageVersion: releaseVersion,422 publishChannel: 'latest',423 source: 'current',424 };425}426 427function getConflictSources(releaseState) {428 const sources = [];429 if (releaseState.packageVersionExistsOnPyPI) {430 sources.push('PyPI');431 }432 if (releaseState.githubReleaseExists) {433 sources.push('GitHub releases');434 }435 if (releaseState.gitTagExists) {436 sources.push('git tags');437 }438 return sources.length > 0 ? sources.join(', ') : 'unknown release state';439}440 441function bumpVersion(versionData) {442 const match = versionData.releaseVersion.match(443 /^(\d+\.\d+\.\d+)-preview\.(\d+)$/,444 );445 if (!match) {446 throw new Error(447 `Cannot bump preview version: ${versionData.releaseVersion}`,448 );449 }450 const nextNumber = Number(match[2]) + 1;451 return {452 ...versionData,453 releaseVersion: `${match[1]}-preview.${nextNumber}`,454 packageVersion: `${match[1]}rc${nextNumber}`,455 };456}457 458async function getVersion(options = {}) {459 const args = { ...getArgs(), ...options };460 const type = args.type || 'nightly';461 const { versions, allVersions } = await getAllVersionsFromPyPI();462 const hasManualOverride =463 (type === 'preview' && Boolean(args.preview_version_override)) ||464 (type === 'stable' && Boolean(args.stable_version_override));465 466 let versionData;467 let resumeExistingRelease = false;468 switch (type) {469 case 'nightly':470 versionData = getNightlyVersion(versions);471 break;472 case 'preview':473 versionData = getPreviewVersion(args, versions);474 break;475 case 'stable':476 versionData = getStableVersion(args, versions);477 break;478 default:479 throw new Error(`Unknown release type: ${type}`);480 }481 482 while (true) {483 const releaseState = await getReleaseState(484 {485 packageVersion: versionData.packageVersion,486 releaseVersion: versionData.releaseVersion,487 },488 allVersions,489 );490 491 const versionExists =492 releaseState.packageVersionExistsOnPyPI ||493 releaseState.gitTagExists ||494 releaseState.githubReleaseExists;495 if (!versionExists) {496 break;497 }498 499 if (500 !hasManualOverride &&501 releaseState.packageVersionExistsOnPyPI &&502 !releaseState.githubReleaseExists503 ) {504 console.error(505 `PyPI version ${versionData.packageVersion} already exists without a matching GitHub release. Reusing the same release version.`,506 );507 resumeExistingRelease = true;508 break;509 }510 511 if (512 !hasManualOverride &&513 type === 'stable' &&514 releaseState.packageVersionExistsOnPyPI &&515 releaseState.githubReleaseExists516 ) {517 console.error(518 `Stable release ${versionData.releaseVersion} already has a matching GitHub release. Reusing the same release version for post-release recovery.`,519 );520 resumeExistingRelease = true;521 break;522 }523 524 if (hasManualOverride) {525 throw new Error(526 `Requested ${type} release ${versionData.releaseVersion} already exists on ${getConflictSources(releaseState)}.`,527 );528 }529 530 if (releaseState.githubReleaseExists) {531 console.error(532 `GitHub release ${TAG_PREFIX}${versionData.releaseVersion} already exists.`,533 );534 } else if (releaseState.gitTagExists) {535 console.error(536 `::warning::Orphan git tag ${TAG_PREFIX}${versionData.releaseVersion} exists without a PyPI version or GitHub release. Skipping to next version slot.`,537 );538 } else if (releaseState.packageVersionExistsOnPyPI) {539 console.error(540 `PyPI version ${versionData.packageVersion} already exists.`,541 );542 }543 544 if (type === 'stable') {545 if (546 versionData.source === 'preview' ||547 versionData.source === 'nightly'548 ) {549 throw new Error(550 `Stable release ${versionData.releaseVersion} derived from the latest ${versionData.source} already exists.`,551 );552 }553 554 throw new Error(555 `Stable release ${versionData.releaseVersion} already exists. Provide stable_version_override to release a different stable version.`,556 );557 }558 559 if (type === 'nightly') {560 throw new Error(561 `Nightly version conflict for ${versionData.packageVersion}`,562 );563 }564 565 versionData = bumpVersion(versionData);566 }567 568 const previousVersion =569 type === 'stable'570 ? getLatestStableVersion(571 versions.filter((v) => v !== versionData.releaseVersion),572 )573 : '';574 575 return {576 releaseTag: `v${versionData.releaseVersion}`,577 releaseVersion: versionData.releaseVersion,578 packageVersion: versionData.packageVersion,579 previousReleaseTag: previousVersion ? `v${previousVersion}` : '',580 publishChannel: versionData.publishChannel,581 resumeExistingRelease,582 };583}584 585if (process.argv[1] === fileURLToPath(import.meta.url)) {586 const result = await getVersion(getArgs());587 console.log(JSON.stringify(result, null, 2));588}589 590export { getVersion };591 