CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
get-release-version.js591 linesDownload Raw Back to scripts
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 
basant307/AI_Governance_Project · CoolFace