qwen-code/scripts/tests/get-release-version-python-sdk.test.js
jinye 03f66bada5
feat(sdk-python): add PyPI release workflow (#3685)
* feat(sdk-python): add pypi release workflow

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): build cli before smoke test

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): tighten release conflict handling

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): harden python release workflow

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): tighten stable release guards

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): harden prerelease publish flow

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): reuse release branches on rerun

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): resume incomplete releases

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(release): tighten missing-release checks

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): resume stable release reruns

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): tighten release recovery guards

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(sdk-python): cover release version edge cases

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): address release workflow review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(sdk-python): address review feedback on release version script

- Remove unreachable `if (type === 'stable')` branch in bumpVersion();
  the stable path was dead code since getVersion() throws for all
  stable conflicts before calling bumpVersion(). Move nightly conflict
  throw to the call site for symmetry.
- Rename getNextPatchBaseVersion → getNextBaseVersion to reflect that
  the function can return a prerelease base without incrementing patch.
- Add test for preview+nightly coexistence where nightly base is higher.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(sdk-python): address remaining review feedback on release workflow

- Fix failure-issue gate to read github.event.inputs.dry_run directly
  instead of steps.vars.outputs.is_dry_run (which is empty when early
  steps fail). Add --repo flag for gh issue create when checkout failed.
- Add diagnostic state table to failure-issue body (RELEASE_TAG,
  PACKAGE_VERSION, PUBLISH_CHANNEL, RESUME_EXISTING_RELEASE, etc.)
- Fix release-notes error swallow: only silence release not found /
  Not Found / HTTP 404, emit :⚠️: for other gh release view errors.
- Improve validateVersion error messages to use human-readable format
  keys (X.Y.Z, X.Y.Z-preview.N) matching TS sibling convention.
- Filter fully-yanked versions in getAllVersionsFromPyPI.
- Add console.error log when stable is derived from nightly.
- Add bash regex guard for inputs.version to prevent shell injection.
- Use per-release-type concurrency groups (nightly/preview/stable).
- Add jq null-guard checks for all 6 field extractions.
- Remove misleading --follow-tags from git push (lightweight tags).

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(sdk-python): rename misleading test description

The test asserts that preview/nightly releases return empty
previousReleaseTag, but the name said "same-channel previous
release tags" which implied non-empty values.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(sdk-python): address unresolved review comments on release workflow

- Remove -z check in extract_field() that blocked preview/nightly releases
  (previousReleaseTag is legitimately empty for non-stable releases)
- Use static environment.url since step outputs aren't available at job startup
- Use skip-existing for resumed PyPI publish to fill in missing artifacts
- Add AbortSignal.timeout(30s) to PyPI fetch to prevent indefinite hangs
- Add downgrade guard for stable_version_override
- Use GHA :⚠️: annotation instead of console.error for visibility
- Separate yanked/non-yanked version lists so conflict detection includes
  yanked versions (PyPI still reserves those slots)
- Filter current release from previousReleaseTag to avoid self-reference on resume
- Add tests for yanked conflict detection, downgrade guard, and resume previousReleaseTag

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): address final review round on release version script

- Fix getNextBaseVersion() first-release skip: use pyproject.toml version
  directly when PyPI has no stable versions instead of unconditionally
  incrementing
- Fix getNextBaseVersion() off-by-one: change > to >= so equal prerelease
  base continues the existing line instead of incrementing patch
- Add :⚠️: annotation when preview auto-bumps due to orphan git
  tags (tag exists without PyPI version or GitHub release)
- Add set -euo pipefail to 5 workflow steps missing it: release_branch,
  persist_source, Create GitHub release, Delete prerelease branch, Create
  issue on failure
- Fix 2 existing tests affected by first-release change, add 4 new tests

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(sdk-python): use stderr for GHA warning annotations to avoid corrupting JSON stdout

console.log writes to stdout, which gets captured by VERSION_JSON=$(node ...)
in the workflow and corrupts the JSON output for jq. Switch to console.error
so :⚠️: annotations go to stderr (GHA recognizes workflow commands on
both streams). Also add set -euo pipefail to the "Get the version" step for
consistency with other workflow steps.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

---------

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-04 21:07:21 +08:00

930 lines
23 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
const fetchMock = vi.fn();
const execSyncMock = vi.fn();
const readFileSyncMock = vi.fn();
vi.mock('node:child_process', () => ({
execSync: execSyncMock,
}));
vi.mock('node:fs', () => ({
readFileSync: readFileSyncMock,
}));
global.fetch = fetchMock;
const modulePath = '../../packages/sdk-python/scripts/get-release-version.js';
async function loadGetVersion() {
const mod = await import(`${modulePath}?t=${Date.now()}-${Math.random()}`);
return mod.getVersion;
}
function makeResponse({ status = 200, json = {}, statusText = 'OK' } = {}) {
return {
status,
ok: status >= 200 && status < 300,
statusText,
json: async () => json,
};
}
function makeExecError(message, { stderr = '', stdout = '', status } = {}) {
const error = new Error(message);
if (stderr) {
error.stderr = Buffer.from(stderr);
}
if (stdout) {
error.stdout = Buffer.from(stdout);
}
if (status !== undefined) {
error.status = status;
}
return error;
}
function makeExecSyncMock({
tags = {},
tagError = null,
releases = {},
gitHash = 'abc1234',
} = {}) {
return (command) => {
if (command === 'git rev-parse --short HEAD') {
return Buffer.from(gitHash);
}
const tagMatch = command.match(/^git tag -l '(.+)'$/);
if (tagMatch) {
if (tagError) {
throw tagError;
}
return Buffer.from(tags[tagMatch[1]] ?? '');
}
const releaseMatch = command.match(
/^gh release view "(.+)" --json tagName --jq \.tagName$/,
);
if (releaseMatch) {
const releaseName = releaseMatch[1];
const outcome = releases[releaseName];
if (outcome instanceof Error) {
throw outcome;
}
if (typeof outcome === 'string') {
return Buffer.from(outcome);
}
throw makeExecError('release not found', { status: 1 });
}
throw new Error(`Unexpected execSync command: ${command}`);
};
}
describe('python sdk get-release-version', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-30T03:15:16.000Z'));
readFileSyncMock.mockReturnValue('version = "0.1.0"\n');
fetchMock.mockResolvedValue(
makeResponse({
json: { releases: {} },
}),
);
execSyncMock.mockImplementation(makeExecSyncMock());
});
it('returns empty previousReleaseTag for preview and nightly releases', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.1.1rc0': [{}],
'0.1.1.dev20260429010101': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
releaseTag: 'v0.1.1-preview.1',
previousReleaseTag: '',
});
await expect(getVersion({ type: 'nightly' })).resolves.toMatchObject({
releaseTag: 'v0.1.1-nightly.20260430031516.abc1234',
releaseVersion: '0.1.1-nightly.20260430031516.abc1234',
packageVersion: '0.1.1.dev20260430031516',
publishChannel: 'nightly',
previousReleaseTag: '',
});
});
it('fails when an explicit override conflicts with existing PyPI version', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.1rc0': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(
getVersion({
type: 'preview',
preview_version_override: 'v0.1.1-preview.0',
}),
).rejects.toThrow(
'Requested preview release 0.1.1-preview.0 already exists on PyPI, GitHub releases.',
);
});
it('fails when an explicit preview override only conflicts with PyPI state', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.1rc0': [{}],
},
},
}),
);
const getVersion = await loadGetVersion();
await expect(
getVersion({
type: 'preview',
preview_version_override: 'v0.1.1-preview.0',
}),
).rejects.toThrow(
'Requested preview release 0.1.1-preview.0 already exists on PyPI.',
);
});
it('fails when an explicit override conflicts with an existing git tag', async () => {
execSyncMock.mockImplementation(
makeExecSyncMock({
tags: {
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(
getVersion({
type: 'preview',
preview_version_override: 'v0.1.1-preview.0',
}),
).rejects.toThrow(
'Requested preview release 0.1.1-preview.0 already exists on git tags.',
);
});
it('fails when an explicit override conflicts with an existing GitHub release', async () => {
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(
getVersion({
type: 'preview',
preview_version_override: 'v0.1.1-preview.0',
}),
).rejects.toThrow(
'Requested preview release 0.1.1-preview.0 already exists on GitHub releases.',
);
});
it('fails closed when git tag conflict checks error', async () => {
execSyncMock.mockImplementation(
makeExecSyncMock({
tagError: new Error('git tag failed'),
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).rejects.toThrow(
'Failed to check git tags for conflicts: git tag failed',
);
});
it('fails if GitHub release lookup errors for reasons other than not found', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
},
},
}),
);
const authError = new Error('HTTP 403 rate limited');
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.1.1-preview.0': authError,
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).rejects.toThrow(
'Failed to check GitHub releases for conflicts: HTTP 403 rate limited',
);
});
it('fails closed when unrelated lowercase not-found errors occur', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.1.1-preview.0': makeExecError('host not found'),
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).rejects.toThrow(
'Failed to check GitHub releases for conflicts: host not found',
);
});
it('reuses a PyPI version when GitHub release finalization needs to resume', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.1.1rc0': [{}],
},
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
releaseTag: 'v0.1.1-preview.0',
packageVersion: '0.1.1rc0',
resumeExistingRelease: true,
});
});
it('reuses a stable release when only post-release recovery steps remain', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.1.0': 'sdk-python-v0.1.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
releaseTag: 'v0.1.0',
packageVersion: '0.1.0',
resumeExistingRelease: true,
});
});
it('returns a manual stable override on the happy path', async () => {
const getVersion = await loadGetVersion();
await expect(
getVersion({
type: 'stable',
stable_version_override: 'v0.2.0',
}),
).resolves.toMatchObject({
releaseVersion: '0.2.0',
packageVersion: '0.2.0',
publishChannel: 'latest',
});
});
it('fails when an explicit stable override conflicts with a completed release', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.1.0': 'sdk-python-v0.1.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(
getVersion({
type: 'stable',
stable_version_override: 'v0.1.0',
}),
).rejects.toThrow(
'Requested stable release 0.1.0 already exists on PyPI, GitHub releases.',
);
});
it('fails when the latest preview base is not newer than the latest stable', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.2.0': [{}],
'0.1.1rc1': [{}],
},
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'stable' })).rejects.toThrow(
'Latest preview base 0.1.1 is not newer than latest stable 0.2.0.',
);
});
it('uses the latest nightly base for stable releases when no preview exists', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.2.0.dev20260429010101': [{}],
},
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
releaseTag: 'v0.2.0',
releaseVersion: '0.2.0',
packageVersion: '0.2.0',
previousReleaseTag: 'v0.1.0',
publishChannel: 'latest',
});
});
it('prefers nightly base over preview when nightly is higher for stable releases', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.1.1rc0': [{}],
'0.2.0.dev20260429010101': [{}],
},
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
releaseTag: 'v0.2.0',
releaseVersion: '0.2.0',
packageVersion: '0.2.0',
previousReleaseTag: 'v0.1.0',
publishChannel: 'latest',
});
});
it('fails instead of patch-bumping a stable release derived from preview when its tag already exists', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.1.1rc0': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
tags: {
'sdk-python-v0.1.1': 'sdk-python-v0.1.1',
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'stable' })).rejects.toThrow(
'Stable release 0.1.1 derived from the latest preview already exists.',
);
});
it('fails instead of patch-bumping a stable release derived from nightly when its tag already exists', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.2.0.dev20260429010101': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
tags: {
'sdk-python-v0.2.0': 'sdk-python-v0.2.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'stable' })).rejects.toThrow(
'Stable release 0.2.0 derived from the latest nightly already exists.',
);
});
it('fails instead of patch-bumping the current stable version when its tag already exists', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.0.9': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
tags: {
'sdk-python-v0.1.0': 'sdk-python-v0.1.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'stable' })).rejects.toThrow(
'Stable release 0.1.0 already exists. Provide stable_version_override to release a different stable version.',
);
});
it('returns the previous stable tag for stable releases', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.1.1rc0': [{}],
},
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
releaseTag: 'v0.1.1',
previousReleaseTag: 'v0.1.0',
});
});
it('maps preview versions to PEP 440 package versions', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
},
},
}),
);
const getVersion = await loadGetVersion();
await expect(
getVersion({
type: 'preview',
preview_version_override: 'v0.1.1-preview.2',
}),
).resolves.toMatchObject({
releaseVersion: '0.1.1-preview.2',
packageVersion: '0.1.1rc2',
});
});
it('continues the highest prerelease base for preview releases', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.2.0rc0': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.2.0-preview.0': 'sdk-python-v0.2.0-preview.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
releaseVersion: '0.2.0-preview.1',
packageVersion: '0.2.0rc1',
});
});
it('continues the highest nightly-only prerelease base for preview releases', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.2.0.dev20260429010101': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.2.0-preview.0': 'sdk-python-v0.2.0-preview.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
releaseVersion: '0.2.0-preview.1',
packageVersion: '0.2.0rc1',
});
});
it('keeps bumping preview slots until it finds an unused iteration', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.1.1rc0': [{}],
'0.1.1rc1': [{}],
'0.1.1rc2': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
'sdk-python-v0.1.1-preview.1': 'sdk-python-v0.1.1-preview.1',
'sdk-python-v0.1.1-preview.2': 'sdk-python-v0.1.1-preview.2',
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
releaseVersion: '0.1.1-preview.3',
packageVersion: '0.1.1rc3',
resumeExistingRelease: false,
});
});
it('throws on nightly conflicts instead of silently changing the timestamped version', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.1.dev20260430031516': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.1.1-nightly.20260430031516.abc1234':
'sdk-python-v0.1.1-nightly.20260430031516.abc1234',
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'nightly' })).rejects.toThrow(
'Nightly version conflict for 0.1.1.dev20260430031516',
);
});
it('throws when PyPI metadata fetch is not ok', async () => {
fetchMock.mockResolvedValue(
makeResponse({
status: 503,
statusText: 'Service Unavailable',
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).rejects.toThrow(
'Failed to fetch PyPI metadata: 503 Service Unavailable',
);
});
it('treats a PyPI 404 as a first-release scenario', async () => {
fetchMock.mockResolvedValue(makeResponse({ status: 404 }));
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
releaseTag: 'v0.1.0',
releaseVersion: '0.1.0',
packageVersion: '0.1.0',
previousReleaseTag: '',
publishChannel: 'latest',
});
});
it('ignores fully yanked PyPI versions', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{ yanked: true }],
'0.0.9': [{}],
},
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
releaseTag: 'v0.1.0',
releaseVersion: '0.1.0',
packageVersion: '0.1.0',
previousReleaseTag: 'v0.0.9',
});
});
it('detects yanked versions as conflicts on PyPI', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{ yanked: true }],
},
},
}),
);
const getVersion = await loadGetVersion();
// 0.1.0 is yanked so base-version computation ignores it and derives 0.1.0
// from pyproject.toml, but conflict detection sees it on PyPI. Since it has
// no GitHub release, the script resumes the existing release.
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
releaseTag: 'v0.1.0',
packageVersion: '0.1.0',
resumeExistingRelease: true,
});
});
it('rejects stable_version_override that is older than latest stable', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.5.0': [{}],
},
},
}),
);
const getVersion = await loadGetVersion();
await expect(
getVersion({
type: 'stable',
stable_version_override: 'v0.1.0',
}),
).rejects.toThrow(
'stable_version_override 0.1.0 is older than latest stable 0.5.0',
);
});
it('allows stable_version_override equal to latest stable', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.5.0': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.5.0': 'sdk-python-v0.5.0',
},
}),
);
const getVersion = await loadGetVersion();
// Equal version already exists, so the override conflict check fires
await expect(
getVersion({
type: 'stable',
stable_version_override: 'v0.5.0',
}),
).rejects.toThrow(
'Requested stable release 0.5.0 already exists on PyPI, GitHub releases.',
);
});
it('uses pyproject.toml version for first preview release when PyPI has no versions', async () => {
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
releaseTag: 'v0.1.0-preview.0',
releaseVersion: '0.1.0-preview.0',
packageVersion: '0.1.0rc0',
});
});
it('uses pyproject.toml version for first nightly release when PyPI has no versions', async () => {
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'nightly' })).resolves.toMatchObject({
releaseTag: 'v0.1.0-nightly.20260430031516.abc1234',
releaseVersion: '0.1.0-nightly.20260430031516.abc1234',
packageVersion: '0.1.0.dev20260430031516',
});
});
it('continues the prerelease base when it equals the stable baseline', async () => {
readFileSyncMock.mockReturnValue('version = "0.2.0"\n');
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.2.0rc0': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.2.0-preview.0': 'sdk-python-v0.2.0-preview.0',
},
}),
);
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
releaseVersion: '0.2.0-preview.1',
packageVersion: '0.2.0rc1',
});
});
it('emits a warning when skipping a preview slot due to an orphan git tag', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
tags: {
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
},
}),
);
const consoleSpy = vi.spyOn(console, 'error');
const getVersion = await loadGetVersion();
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
releaseVersion: '0.1.1-preview.1',
packageVersion: '0.1.1rc1',
});
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('::warning::Orphan git tag'),
);
consoleSpy.mockRestore();
});
it('excludes current release from previousReleaseTag on resume', async () => {
fetchMock.mockResolvedValue(
makeResponse({
json: {
releases: {
'0.1.0': [{}],
'0.2.0': [{}],
'0.2.0rc0': [{}],
},
},
}),
);
execSyncMock.mockImplementation(
makeExecSyncMock({
releases: {
'sdk-python-v0.2.0': 'sdk-python-v0.2.0',
},
}),
);
const getVersion = await loadGetVersion();
const result = await getVersion({ type: 'stable' });
expect(result).toMatchObject({
releaseTag: 'v0.2.0',
previousReleaseTag: 'v0.1.0',
resumeExistingRelease: true,
});
});
});