From a5c23dd36a8bb5b371a76197e460c9910712c202 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:20:16 +0800 Subject: [PATCH] fix: prerelease version --- apps/cli/tests/built-bin.e2e.ts | 5 ++- .../scaffold/create-sdk/tests/create.spec.ts | 6 ++- python/sdk/tests/test_release_version.py | 24 ++++++++++-- scripts/build-python-release.py | 38 +++++++++++++++---- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index 015a729d5a..81acefadc5 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -8,6 +8,9 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' /** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)) +// The release version, including a prerelease such as 0.0.1-rc.1: `--version` +// prints what this manifest carries, so no test may pin it to a literal. +const cliVersion = (JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }).version const dshBin = join(repoRoot, 'apps/cli/lib/bin.js') const invalidProvider = fileURLToPath(new URL('./fixtures/invalid-provider.cordis.yml', import.meta.url)) @@ -387,7 +390,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', writeFileSync(join(project, '.env'), 'PATH=/project-only-path\n') try { const result = await runBuiltBin(['--version'], {}, project) - expect(result).toEqual({ code: 0, stdout: '0.0.1', stderr: '' }) + expect(result).toEqual({ code: 0, stdout: cliVersion, stderr: '' }) } finally { rmSync(project, { recursive: true, force: true }) } diff --git a/packages/scaffold/create-sdk/tests/create.spec.ts b/packages/scaffold/create-sdk/tests/create.spec.ts index 6c2995ec7d..060f76b11d 100644 --- a/packages/scaffold/create-sdk/tests/create.spec.ts +++ b/packages/scaffold/create-sdk/tests/create.spec.ts @@ -461,7 +461,11 @@ describe('CreateWizard and scaffolder', () => { }) it('reads the release batch from the initializer package', async () => { - await expect(readCreateSdkVersion()).resolves.toBe('0.0.1') + // The version tracks the release, including a prerelease such as 0.0.1-rc.1, + // so the expectation comes from the manifest rather than a literal. + const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')) as { version: string } + + await expect(readCreateSdkVersion()).resolves.toBe(manifest.version) }) }) diff --git a/python/sdk/tests/test_release_version.py b/python/sdk/tests/test_release_version.py index cf5a6cf57b..c66de01a3e 100644 --- a/python/sdk/tests/test_release_version.py +++ b/python/sdk/tests/test_release_version.py @@ -32,13 +32,31 @@ def test_release_tag_must_match_repository_version() -> None: build_python_release.validate_release_tag("python-v1.2.4", "1.2.3") -def test_repository_version_rejects_non_stable_versions(tmp_path: Path) -> None: - (tmp_path / "package.json").write_text('{"version":"1.2.3-dev"}\n') +def test_repository_version_accepts_a_prerelease(tmp_path: Path) -> None: + (tmp_path / "package.json").write_text('{"version":"1.2.3-rc.1"}\n') - with pytest.raises(ValueError, match="must be stable X.Y.Z"): + assert build_python_release.repository_version(tmp_path) == "1.2.3-rc.1" + + +def test_repository_version_rejects_malformed_versions(tmp_path: Path) -> None: + (tmp_path / "package.json").write_text('{"version":"v1.2"}\n') + + with pytest.raises(ValueError, match="must be X.Y.Z"): build_python_release.repository_version(tmp_path) +def test_pep440_version_spells_a_prerelease_the_python_way() -> None: + # Build backends normalize to this spelling, so the wheel filename and + # metadata checks compare against it rather than the repository version. + assert build_python_release.pep440_version("1.2.3") == "1.2.3" + assert build_python_release.pep440_version("1.2.3-rc.1") == "1.2.3rc1" + assert build_python_release.pep440_version("1.2.3-alpha.2") == "1.2.3a2" + assert build_python_release.pep440_version("1.2.3-beta.10") == "1.2.3b10" + + with pytest.raises(ValueError, match="no PEP 440 spelling"): + build_python_release.pep440_version("1.2.3-nightly") + + def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path: Path) -> None: destination = tmp_path / "staging" diff --git a/scripts/build-python-release.py b/scripts/build-python-release.py index c9ee5e31c0..d326222efa 100644 --- a/scripts/build-python-release.py +++ b/scripts/build-python-release.py @@ -43,6 +43,8 @@ def main() -> None: args = parser.parse_args() version = repository_version() validate_release_tag(args.tag, version) + # Wheels carry the PEP 440 spelling; the tag keeps the repository spelling. + wheel_version = pep440_version(version) if args.package == "runtime" and (args.platform is None or args.runtime_exe is None): parser.error("runtime builds require --platform and --runtime-exe") if args.package == "sdk" and (args.platform is not None or args.runtime_exe is not None): @@ -53,19 +55,19 @@ def main() -> None: with tempfile.TemporaryDirectory(prefix="dsh-python-release-") as temporary: staging = Path(temporary) / args.package if args.package == "sdk": - stage_sdk(staging, version) + stage_sdk(staging, wheel_version) environment = None - expected = output_dir / f"deepseek_harness_sdk-{version}-py3-none-any.whl" + expected = output_dir / f"deepseek_harness_sdk-{wheel_version}-py3-none-any.whl" else: platform_tag, executable_name = PLATFORMS[args.platform] - stage_runtime(staging, version, args.runtime_exe.resolve(), executable_name) + stage_runtime(staging, wheel_version, args.runtime_exe.resolve(), executable_name) environment = {"DSH_RUNTIME_PLATFORM_TAG": platform_tag} - expected = output_dir / f"deepseek_harness_runtime_bin-{version}-py3-none-{platform_tag}.whl" + expected = output_dir / f"deepseek_harness_runtime_bin-{wheel_version}-py3-none-{platform_tag}.whl" command = ["uv", "build", "--wheel", "--out-dir", str(output_dir), str(staging)] subprocess.run(command, cwd=ROOT, env=None if environment is None else {**os.environ, **environment}, check=True) if not expected.is_file(): raise RuntimeError(f"build did not produce expected wheel: {expected}") - verify_wheel(expected, args.package, version, None if args.platform is None else PLATFORMS[args.platform]) + verify_wheel(expected, args.package, wheel_version, None if args.platform is None else PLATFORMS[args.platform]) print(expected) @@ -76,13 +78,35 @@ def repository_version(root: Path = ROOT) -> str: except (OSError, json.JSONDecodeError) as error: raise ValueError(f"could not read repository version from {package_json}") from error version = payload.get("version") if isinstance(payload, dict) else None - if not isinstance(version, str) or re.fullmatch(r"\d+\.\d+\.\d+", version) is None: + if not isinstance(version, str) or re.fullmatch(r"\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?", version) is None: raise ValueError( - f"{package_json} version must be stable X.Y.Z, got {version!r}" + f"{package_json} version must be X.Y.Z with an optional prerelease segment, got {version!r}" ) return version +def pep440_version(version: str) -> str: + """The Python spelling of a repository version. + + A release candidate is `0.0.1-rc.1` in the repository and `0.0.1rc1` under + PEP 440. Build backends normalize to the latter, so the wheel filename and + metadata carry it: comparing them against the repository spelling would + reject every prerelease build. + """ + stable, separator, prerelease = version.partition("-") + if not separator: + return stable + match = re.fullmatch(r"(a|b|c|rc|alpha|beta|pre|preview)\.?(\d+)", prerelease) + if match is None: + raise ValueError( + f"prerelease segment {prerelease!r} has no PEP 440 spelling; use rc.N, alpha.N, or beta.N" + ) + identifier = {"alpha": "a", "beta": "b", "c": "rc", "pre": "rc", "preview": "rc"}.get( + match.group(1), match.group(1) + ) + return f"{stable}{identifier}{match.group(2)}" + + def validate_release_tag(tag: str | None, version: str) -> None: if tag is None: return