diff --git a/docs/release.md b/docs/release.md index 9131d57e..5e8a254b 100644 --- a/docs/release.md +++ b/docs/release.md @@ -100,6 +100,12 @@ The artifact packaging and readiness scripts read `publicNpmPackageVersion` from `release-policy.json`, so manual version edits in build scripts aren't needed for rc releases. +The bundled Python runtime wheel also derives its version from +`publicNpmPackageVersion`. Stable npm versions are reused as-is, and rc +versions are normalized to Python's version format. For example, +`0.1.0-rc.2` becomes `0.1.0rc2` in the `kaelio-ktx` wheel filename and wheel +metadata. + ## npm authentication The release workflow publishes through npm Trusted Publishing. It doesn't use diff --git a/packages/cli/src/cli-program.test.ts b/packages/cli/src/cli-program.test.ts index 79b0d23a..f0ac9595 100644 --- a/packages/cli/src/cli-program.test.ts +++ b/packages/cli/src/cli-program.test.ts @@ -11,7 +11,13 @@ function stubIo(): KtxCliIo { } function stubPackageInfo(): KtxCliPackageInfo { - return { name: '@ktx/cli', version: '0.0.0-test', contextPackageName: '@ktx/context' }; + return { + name: '@ktx/cli', + version: '0.0.0-test', + packageVersion: '0.0.0-private', + runtimeVersion: '0.0.0-test', + contextPackageName: '@ktx/context', + }; } describe('buildKtxProgram', () => { diff --git a/packages/cli/src/cli-runtime.ts b/packages/cli/src/cli-runtime.ts index b0fe8eb0..b8bc636d 100644 --- a/packages/cli/src/cli-runtime.ts +++ b/packages/cli/src/cli-runtime.ts @@ -10,6 +10,7 @@ import type { KtxSlArgs } from './sl.js'; import type { KtxSqlArgs } from './sql.js'; import { profileMark, profileSpan } from './startup-profile.js'; import type { KtxTextIngestArgs } from './text-ingest.js'; +import { resolveKtxRuntimeVersion } from './release-version.js'; profileMark('module:cli-runtime'); @@ -18,6 +19,8 @@ const requirePackageJson = createRequire(import.meta.url); export interface KtxCliPackageInfo { name: string; version: string; + packageVersion: string; + runtimeVersion: string; contextPackageName: '@ktx/context'; } @@ -61,9 +64,16 @@ export function packageInfoFromJson(packageJson: unknown): KtxCliPackageInfo { throw new Error('Invalid KTX CLI package metadata'); } + const runtimeVersion = resolveKtxRuntimeVersion({ + packageName: packageJson.name, + packageVersion: packageJson.version, + }); + return { name: packageJson.name, - version: packageJson.version, + version: runtimeVersion, + packageVersion: packageJson.version, + runtimeVersion, contextPackageName: '@ktx/context', }; } diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 692c7cd0..a48da8d5 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -45,7 +45,9 @@ describe('getKtxCliPackageInfo', () => { it('identifies the CLI package and its context dependency', () => { expect(getKtxCliPackageInfo()).toEqual({ name: '@ktx/cli', - version: '0.0.0-private', + version: '0.1.0-rc.1', + packageVersion: '0.0.0-private', + runtimeVersion: '0.1.0-rc.1', contextPackageName: '@ktx/context', }); }); @@ -68,6 +70,8 @@ describe('getKtxCliPackageInfo', () => { ).toEqual({ name: '@kaelio/ktx', version: '0.1.0', + packageVersion: '0.1.0', + runtimeVersion: '0.1.0', contextPackageName: '@ktx/context', }); }); @@ -114,7 +118,7 @@ describe('runKtxCli', () => { await expect(runKtxCli(['--version'], testIo.io)).resolves.toBe(0); - expect(testIo.stdout()).toBe('@ktx/cli 0.0.0-private\n'); + expect(testIo.stdout()).toBe('@ktx/cli 0.1.0-rc.1\n'); expect(testIo.stderr()).toBe(''); }); @@ -252,7 +256,7 @@ describe('runKtxCli', () => { expect(listIo.stderr()).toContain("unknown option '--query'"); }); - it('routes runtime management commands with the CLI package version', async () => { + it('routes runtime management commands with the release runtime version', async () => { const runtime = vi.fn(async () => 0); const installIo = makeIo(); const startIo = makeIo(); @@ -278,7 +282,7 @@ describe('runKtxCli', () => { 1, { command: 'install', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', feature: 'local-embeddings', force: true, }, @@ -288,7 +292,7 @@ describe('runKtxCli', () => { 2, { command: 'start', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', projectDir: expect.any(String), feature: 'local-embeddings', force: true, @@ -299,7 +303,7 @@ describe('runKtxCli', () => { 3, { command: 'stop', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', projectDir: expect.any(String), all: false, }, @@ -309,7 +313,7 @@ describe('runKtxCli', () => { 4, { command: 'stop', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', projectDir: expect.any(String), all: true, }, @@ -319,7 +323,7 @@ describe('runKtxCli', () => { 5, { command: 'status', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', json: true, }, statusIo.io, @@ -392,7 +396,7 @@ describe('runKtxCli', () => { expect.objectContaining({ command: 'query', projectDir: tempDir, - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', runtimeInstallPolicy: 'prompt', query: expect.objectContaining({ measures: ['orders.order_count'], dimensions: [] }), }), @@ -407,7 +411,7 @@ describe('runKtxCli', () => { ).resolves.toBe(0); expect(sl).toHaveBeenLastCalledWith( expect.objectContaining({ - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', runtimeInstallPolicy: 'auto', }), autoIo.io, @@ -423,7 +427,7 @@ describe('runKtxCli', () => { ).resolves.toBe(0); expect(sl).toHaveBeenLastCalledWith( expect.objectContaining({ - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', runtimeInstallPolicy: 'never', }), noInputIo.io, @@ -562,7 +566,7 @@ describe('runKtxCli', () => { skipAgents: false, inputMode: 'auto', yes: false, - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', skipLlm: false, skipEmbeddings: false, databaseSchemas: [], @@ -692,7 +696,7 @@ describe('runKtxCli', () => { inputMode: 'disabled', depth: 'fast', queryHistory: 'default', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', runtimeInstallPolicy: 'never', }, testIo.io, @@ -719,7 +723,7 @@ describe('runKtxCli', () => { inputMode: 'auto', depth: 'deep', queryHistory: 'default', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', runtimeInstallPolicy: 'prompt', }, testIo.io, @@ -796,7 +800,7 @@ describe('runKtxCli', () => { json: false, inputMode: 'disabled', queryHistory: 'default', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', runtimeInstallPolicy: 'never', }, testIo.io, @@ -1074,7 +1078,7 @@ describe('runKtxCli', () => { command: 'run', projectDir: tempDir, inputMode: 'disabled', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret anthropicModel: 'claude-sonnet-4-6', skipLlm: false, @@ -1113,7 +1117,7 @@ describe('runKtxCli', () => { command: 'run', projectDir: tempDir, inputMode: 'disabled', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', llmBackend: 'vertex', vertexProject: 'local-gcp-project', vertexLocation: 'us-east5', @@ -1150,7 +1154,7 @@ describe('runKtxCli', () => { command: 'run', projectDir: tempDir, inputMode: 'disabled', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', llmBackend: 'claude-code', llmModel: 'opus', skipLlm: false, @@ -1258,7 +1262,7 @@ describe('runKtxCli', () => { projectDir: '/tmp/project', inputMode: 'disabled', yes: true, - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', skipLlm: true, skipEmbeddings: true, databaseDrivers: ['postgres'], @@ -1576,7 +1580,7 @@ describe('runKtxCli', () => { queryFile: '/tmp/query.json', execute: false, format: 'json', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', runtimeInstallPolicy: 'auto', }, autoIo.io, @@ -1590,7 +1594,7 @@ describe('runKtxCli', () => { queryFile: '/tmp/query.json', execute: false, format: 'json', - cliVersion: '0.0.0-private', + cliVersion: '0.1.0-rc.1', runtimeInstallPolicy: 'never', }, neverIo.io, diff --git a/packages/cli/src/print-command-tree.ts b/packages/cli/src/print-command-tree.ts index 2ede889c..6c9de751 100644 --- a/packages/cli/src/print-command-tree.ts +++ b/packages/cli/src/print-command-tree.ts @@ -11,7 +11,13 @@ function silentIo(): KtxCliIo { } function stubPackageInfo(): KtxCliPackageInfo { - return { name: '@ktx/cli', version: '0.0.0-docs', contextPackageName: '@ktx/context' }; + return { + name: '@ktx/cli', + version: '0.0.0-docs', + packageVersion: '0.0.0-private', + runtimeVersion: '0.0.0-docs', + contextPackageName: '@ktx/context', + }; } export function renderKtxCommandTree(): string { diff --git a/packages/cli/src/release-version.ts b/packages/cli/src/release-version.ts new file mode 100644 index 00000000..77bcb833 --- /dev/null +++ b/packages/cli/src/release-version.ts @@ -0,0 +1,55 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join, parse } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const semverPattern = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function assertReleaseVersion(value: unknown, source: string): string { + if (typeof value !== 'string' || !semverPattern.test(value)) { + throw new Error(`Invalid KTX release version in ${source}`); + } + return value; +} + +function findReleasePolicyPath(startDir: string): string | undefined { + let current = startDir; + const root = parse(current).root; + while (true) { + const candidate = join(current, 'release-policy.json'); + if (existsSync(candidate)) { + return candidate; + } + if (current === root) { + return undefined; + } + current = dirname(current); + } +} + +function readSourceReleaseVersion(startDir = dirname(fileURLToPath(import.meta.url))): string | undefined { + const policyPath = findReleasePolicyPath(startDir); + if (!policyPath) { + return undefined; + } + const policy = JSON.parse(readFileSync(policyPath, 'utf8')) as unknown; + if (!isPlainObject(policy)) { + throw new Error(`Invalid KTX release policy: ${policyPath}`); + } + return assertReleaseVersion(policy.publicNpmPackageVersion, policyPath); +} + +export function resolveKtxRuntimeVersion(input: { + packageName: string; + packageVersion: string; + startDir?: string; +}): string { + if (input.packageName === '@kaelio/ktx') { + return assertReleaseVersion(input.packageVersion, `${input.packageName}/package.json`); + } + return readSourceReleaseVersion(input.startDir) ?? input.packageVersion; +} diff --git a/python/ktx-daemon/pyproject.toml b/python/ktx-daemon/pyproject.toml index 16437879..4f21f9de 100644 --- a/python/ktx-daemon/pyproject.toml +++ b/python/ktx-daemon/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ktx-daemon" -version = "0.1.0" +version = "0.0.0+private" description = "Portable compute package for KTX semantic-layer operations" readme = "README.md" requires-python = ">=3.13" diff --git a/python/ktx-daemon/src/ktx_daemon/__init__.py b/python/ktx-daemon/src/ktx_daemon/__init__.py index ddc5f6d9..d228ac79 100644 --- a/python/ktx-daemon/src/ktx_daemon/__init__.py +++ b/python/ktx-daemon/src/ktx_daemon/__init__.py @@ -1,6 +1,28 @@ """Portable compute package for KTX.""" -PACKAGE_NAME = "ktx-daemon" -VERSION = "0.1.0" +from collections.abc import Callable +from importlib.metadata import PackageNotFoundError, version -__all__ = ["PACKAGE_NAME", "VERSION"] +PACKAGE_NAME = "ktx-daemon" +RUNTIME_DISTRIBUTION_NAME = "kaelio-ktx" + + +def resolve_package_version( + version_loader: Callable[[str], str] = version, +) -> str: + for distribution_name in (RUNTIME_DISTRIBUTION_NAME, PACKAGE_NAME): + try: + return version_loader(distribution_name) + except PackageNotFoundError: + continue + return "0.0.0+local" + + +VERSION = resolve_package_version() + +__all__ = [ + "PACKAGE_NAME", + "RUNTIME_DISTRIBUTION_NAME", + "VERSION", + "resolve_package_version", +] diff --git a/python/ktx-daemon/src/ktx_daemon/app.py b/python/ktx-daemon/src/ktx_daemon/app.py index 0d7016dd..3208264c 100644 --- a/python/ktx-daemon/src/ktx_daemon/app.py +++ b/python/ktx-daemon/src/ktx_daemon/app.py @@ -10,6 +10,7 @@ from typing import Any from fastapi import FastAPI, HTTPException from fastapi.responses import Response +from ktx_daemon import VERSION from ktx_daemon.code_execution import ( ExecuteCodeRequest, ExecuteCodeResponse, @@ -84,7 +85,7 @@ def create_app( app = FastAPI( title="KTX Daemon", description="Stateless portable compute server for KTX.", - version="0.1.0", + version=VERSION, ) @app.get("/health") diff --git a/python/ktx-daemon/tests/test_package.py b/python/ktx-daemon/tests/test_package.py index 3812b448..943a85a4 100644 --- a/python/ktx-daemon/tests/test_package.py +++ b/python/ktx-daemon/tests/test_package.py @@ -1,6 +1,19 @@ -from ktx_daemon import PACKAGE_NAME, VERSION +from ktx_daemon import PACKAGE_NAME, VERSION, resolve_package_version def test_package_metadata() -> None: assert PACKAGE_NAME == "ktx-daemon" - assert VERSION == "0.1.0" + assert VERSION == resolve_package_version() + + +def test_package_version_prefers_bundled_runtime_distribution() -> None: + calls: list[str] = [] + + def fake_version(distribution_name: str) -> str: + calls.append(distribution_name) + if distribution_name == "kaelio-ktx": + return "0.1.0rc1" + raise AssertionError(f"unexpected distribution lookup: {distribution_name}") + + assert resolve_package_version(version_loader=fake_version) == "0.1.0rc1" + assert calls == ["kaelio-ktx"] diff --git a/python/ktx-sl/pyproject.toml b/python/ktx-sl/pyproject.toml index f3081327..51e97a83 100644 --- a/python/ktx-sl/pyproject.toml +++ b/python/ktx-sl/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ktx-sl" -version = "0.1.0" +version = "0.0.0+private" description = "Agent-first semantic layer engine with aggregate locality" readme = "README.md" requires-python = ">=3.13" diff --git a/scripts/build-python-runtime-wheel.mjs b/scripts/build-python-runtime-wheel.mjs index 9623b48a..46e30ce5 100644 --- a/scripts/build-python-runtime-wheel.mjs +++ b/scripts/build-python-runtime-wheel.mjs @@ -6,11 +6,13 @@ import { dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; +import { publicPythonRuntimePackageVersion } from './public-npm-release-metadata.mjs'; + const execFileAsync = promisify(execFile); export const RUNTIME_WHEEL_DISTRIBUTION_NAME = 'kaelio-ktx'; export const RUNTIME_WHEEL_NORMALIZED_NAME = 'kaelio_ktx'; -export const RUNTIME_WHEEL_PACKAGE_VERSION = '0.1.0'; +export const RUNTIME_WHEEL_PACKAGE_VERSION = publicPythonRuntimePackageVersion(); function scriptRootDir() { return resolve(dirname(fileURLToPath(import.meta.url)), '..'); diff --git a/scripts/build-python-runtime-wheel.test.mjs b/scripts/build-python-runtime-wheel.test.mjs index 2fbb3fbc..7fc3923b 100644 --- a/scripts/build-python-runtime-wheel.test.mjs +++ b/scripts/build-python-runtime-wheel.test.mjs @@ -48,11 +48,11 @@ describe('runtimeWheelLayout', () => { }); describe('runtimeWheelPyproject', () => { - it('describes one kaelio-ktx wheel with lazy local embeddings', () => { + it('describes one kaelio-ktx wheel with the release-derived Python version and lazy local embeddings', () => { const pyproject = runtimeWheelPyproject(); assert.match(pyproject, /name = "kaelio-ktx"/); - assert.match(pyproject, /version = "0\.1\.0"/); + assert.match(pyproject, /version = "0\.1\.0rc1"/); assert.match(pyproject, /ktx-daemon = "ktx_daemon\.__main__:main"/); assert.match(pyproject, /packages = \["semantic_layer", "ktx_daemon"\]/); assert.match(pyproject, /\[project\.optional-dependencies\]/); @@ -110,6 +110,6 @@ describe('runtimeWheelBuildCommand', () => { cwd: '/repo/ktx', }); assert.equal(RUNTIME_WHEEL_DISTRIBUTION_NAME, 'kaelio-ktx'); - assert.equal(RUNTIME_WHEEL_PACKAGE_VERSION, '0.1.0'); + assert.equal(RUNTIME_WHEEL_PACKAGE_VERSION, '0.1.0rc1'); }); }); diff --git a/scripts/package-artifacts.test.mjs b/scripts/package-artifacts.test.mjs index 95eb6c0c..bbffe06c 100644 --- a/scripts/package-artifacts.test.mjs +++ b/scripts/package-artifacts.test.mjs @@ -82,7 +82,7 @@ async function writeUploadableArtifactFixtures(layout) { `${packageInfo.name}-tarball`, ]), [ - join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'), + join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel', ], ]); @@ -139,7 +139,7 @@ describe('packageReleaseMetadata', () => { ecosystem: 'python', packageName: 'kaelio-ktx', packageRoot: 'python/runtime-wheel', - packageVersion: '0.1.0', + packageVersion: '0.1.0rc1', private: false, releaseMode: 'ci-artifact-only', }, @@ -154,10 +154,10 @@ describe('findPythonArtifacts', () => { it('finds the bundled runtime wheel only', async () => { const root = await mkdtemp(join(tmpdir(), 'ktx-artifacts-test-')); try { - await writeFile(join(root, 'kaelio_ktx-0.1.0-py3-none-any.whl'), ''); + await writeFile(join(root, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), ''); assert.deepEqual(await findPythonArtifacts(root), { - runtimeWheel: join(root, 'kaelio_ktx-0.1.0-py3-none-any.whl'), + runtimeWheel: join(root, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), }); } finally { await rm(root, { recursive: true, force: true }); @@ -210,7 +210,7 @@ describe('artifact manifest', () => { ecosystem: 'python', packageName: 'kaelio-ktx', packageRoot: 'python/runtime-wheel', - packageVersion: '0.1.0', + packageVersion: '0.1.0rc1', private: false, releaseMode: 'ci-artifact-only', }, @@ -252,8 +252,8 @@ describe('artifact manifest', () => { artifactKind: 'wheel', ecosystem: 'python', packageName: 'kaelio-ktx', - packageVersion: '0.1.0', - path: 'python/kaelio_ktx-0.1.0-py3-none-any.whl', + packageVersion: '0.1.0rc1', + path: 'python/kaelio_ktx-0.1.0rc1-py3-none-any.whl', }, ], ); @@ -362,17 +362,17 @@ describe('copyRuntimeWheelAssets', () => { try { await mkdir(layout.pythonDir, { recursive: true }); await writeFile( - join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'), + join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel', ); const assets = await copyRuntimeWheelAssets(layout, { - runtimeWheel: join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'), + runtimeWheel: join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), }); assert.equal( assets.wheelPath, - join(root, 'packages', 'cli', 'assets', 'python', 'kaelio_ktx-0.1.0-py3-none-any.whl'), + join(root, 'packages', 'cli', 'assets', 'python', 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), ); assert.equal( assets.manifestPath, @@ -385,7 +385,7 @@ describe('copyRuntimeWheelAssets', () => { normalizedName: RUNTIME_WHEEL_NORMALIZED_NAME, version: RUNTIME_WHEEL_PACKAGE_VERSION, wheel: { - file: 'kaelio_ktx-0.1.0-py3-none-any.whl', + file: 'kaelio_ktx-0.1.0rc1-py3-none-any.whl', sha256: createHash('sha256') .update('kaelio-ktx-runtime-wheel') .digest('hex'), diff --git a/scripts/public-npm-release-metadata.mjs b/scripts/public-npm-release-metadata.mjs index a8d4fa43..acc77c7e 100644 --- a/scripts/public-npm-release-metadata.mjs +++ b/scripts/public-npm-release-metadata.mjs @@ -9,6 +9,8 @@ export const PUBLIC_NPM_RELEASE_TAGS = new Set(['latest', 'next']); const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; +const SEMVER_PARTS_PATTERN = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; function scriptRootDir() { return resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -29,6 +31,30 @@ export function assertPublicNpmPackageVersion(version) { return version; } +export function publicNpmPackageVersionToPythonVersion(version) { + const safeVersion = assertPublicNpmPackageVersion(version); + const match = SEMVER_PARTS_PATTERN.exec(safeVersion); + if (!match) { + throw new Error(`Invalid public npm package version: ${version}`); + } + + const [, major, minor, patch, prerelease, buildMetadata] = match; + if (buildMetadata) { + throw new Error(`Unsupported public npm build metadata for Python runtime version: ${safeVersion}`); + } + + const baseVersion = `${major}.${minor}.${patch}`; + if (!prerelease) { + return baseVersion; + } + + const rcMatch = /^rc\.([1-9]\d*|0)$/.exec(prerelease); + if (!rcMatch) { + throw new Error(`Unsupported public npm prerelease for Python runtime version: ${safeVersion}`); + } + return `${baseVersion}rc${rcMatch[1]}`; +} + export function assertPublicNpmReleaseTag(tag) { if (!PUBLIC_NPM_RELEASE_TAGS.has(tag)) { throw new Error(`Invalid public npm release tag: ${tag}`); @@ -51,3 +77,7 @@ export function readPublicNpmReleaseMetadata(rootDir = scriptRootDir()) { export function publicNpmPackageVersion(rootDir = scriptRootDir()) { return readPublicNpmReleaseMetadata(rootDir).version; } + +export function publicPythonRuntimePackageVersion(rootDir = scriptRootDir()) { + return publicNpmPackageVersionToPythonVersion(publicNpmPackageVersion(rootDir)); +} diff --git a/scripts/public-npm-release-metadata.test.mjs b/scripts/public-npm-release-metadata.test.mjs new file mode 100644 index 00000000..3f102ff1 --- /dev/null +++ b/scripts/public-npm-release-metadata.test.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { publicNpmPackageVersionToPythonVersion } from './public-npm-release-metadata.mjs'; + +describe('publicNpmPackageVersionToPythonVersion', () => { + it('keeps stable public npm versions unchanged for Python wheels', () => { + assert.equal(publicNpmPackageVersionToPythonVersion('1.2.3'), '1.2.3'); + }); + + it('converts semantic-release rc versions to PEP 440 rc versions', () => { + assert.equal(publicNpmPackageVersionToPythonVersion('0.1.0-rc.1'), '0.1.0rc1'); + assert.equal(publicNpmPackageVersionToPythonVersion('2.0.0-rc.12'), '2.0.0rc12'); + }); + + it('rejects unsupported prerelease and build metadata forms', () => { + assert.throws( + () => publicNpmPackageVersionToPythonVersion('1.2.3-beta.1'), + /Unsupported public npm prerelease for Python runtime version/, + ); + assert.throws( + () => publicNpmPackageVersionToPythonVersion('1.2.3+build.1'), + /Unsupported public npm build metadata for Python runtime version/, + ); + }); +}); diff --git a/scripts/release-readiness.test.mjs b/scripts/release-readiness.test.mjs index 38fcfe20..84170761 100644 --- a/scripts/release-readiness.test.mjs +++ b/scripts/release-readiness.test.mjs @@ -37,7 +37,7 @@ async function writeUploadableArtifactFixtures(layout) { layout.npmTarballs[packageInfo.name], `${packageInfo.name}-tarball`, ]), - [join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel'], + [join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel'], ]); for (const [path, contents] of fileContents) { diff --git a/uv.lock b/uv.lock index f3ffc767..2dc8b355 100644 --- a/uv.lock +++ b/uv.lock @@ -440,7 +440,7 @@ wheels = [ [[package]] name = "ktx-daemon" -version = "0.1.0" +version = "0.0.0+private" source = { editable = "python/ktx-daemon" } dependencies = [ { name = "fastapi" }, @@ -495,7 +495,7 @@ dev = [ [[package]] name = "ktx-sl" -version = "0.1.0" +version = "0.0.0+private" source = { editable = "python/ktx-sl" } dependencies = [ { name = "pydantic" },