fix: prerelease version

This commit is contained in:
imccyu
2026-08-11 03:20:16 +08:00
parent 41360d98bc
commit a5c23dd36a
4 changed files with 61 additions and 12 deletions
+4 -1
View File
@@ -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 })
}
@@ -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)
})
})
+21 -3
View File
@@ -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"
+31 -7
View File
@@ -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