mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the @kaelio/ktx version. publicNpmPackageVersion() now reads it directly, so artifact filenames, release-readiness checks, and the Python wheel version all derive from one field. The duplicate release-policy.json.publicNpmPackageVersion is removed. Previously the two fields could drift: tarballs were named kaelio-ktx-0.4.1.tgz while internally containing @kaelio/ktx@0.0.0-private. - update-public-release-version.mjs rewrites both Python pyproject.toml files (ktx-daemon, ktx-sl) alongside the npm package.jsons, normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2). - semantic-release-config.cjs adds the two pyproject.toml files to @semantic-release/git assets so the release commit back to main carries every version source in lockstep. - The six "?? '0.0.0-private'" fallback literals across the CLI are replaced with "?? getKtxCliPackageInfo().version", and createDefaultKtxMcpServer makes its version arg required. - docs/release.md describes the actual commit-back model: the dev tree always reflects the most recent release; no sentinel pin to maintain. Verified: pnpm run artifacts:build now produces kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with @kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and 2287 vitests + 173 script tests pass.
This commit is contained in:
parent
36805c4533
commit
99f7a31919
22 changed files with 168 additions and 54 deletions
|
|
@ -52,7 +52,10 @@ describe('runtimeWheelPyproject', () => {
|
|||
const pyproject = runtimeWheelPyproject();
|
||||
|
||||
assert.match(pyproject, /name = "kaelio-ktx"/);
|
||||
assert.match(pyproject, new RegExp(`version = "${RUNTIME_WHEEL_PACKAGE_VERSION.replace(/\./g, '\\.')}"`));
|
||||
assert.match(
|
||||
pyproject,
|
||||
new RegExp(`version = "${RUNTIME_WHEEL_PACKAGE_VERSION.replace(/[.+]/g, (char) => `\\${char}`)}"`),
|
||||
);
|
||||
assert.match(pyproject, /ktx-daemon = "ktx_daemon\.__main__:main"/);
|
||||
assert.match(pyproject, /packages = \["semantic_layer", "ktx_daemon"\]/);
|
||||
assert.match(pyproject, /\[project\.optional-dependencies\]/);
|
||||
|
|
|
|||
|
|
@ -57,11 +57,11 @@ describe('publicKtxTarballName', () => {
|
|||
});
|
||||
|
||||
describe('expectedPublicKtxVersionPattern', () => {
|
||||
it('matches the public package version and rejects the private workspace version', () => {
|
||||
it('matches the public package version and rejects other versions', () => {
|
||||
const pattern = expectedPublicKtxVersionPattern();
|
||||
|
||||
assert.match(`@kaelio/ktx ${PUBLIC_NPM_PACKAGE_VERSION}\n`, pattern);
|
||||
assert.doesNotMatch('@kaelio/ktx 0.0.0-private\n', pattern);
|
||||
assert.doesNotMatch('@kaelio/ktx 9.9.9-other\n', pattern);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ async function writeJson(path, value) {
|
|||
async function writeReleaseMetadataInputs(root) {
|
||||
await writeJson(join(root, 'release-policy.json'), {
|
||||
schemaVersion: 1,
|
||||
publicNpmPackageVersion: PUBLIC_NPM_PACKAGE_VERSION,
|
||||
releaseMode: 'ci-artifact-only',
|
||||
npm: {
|
||||
publish: false,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ export function releasePolicyPath(rootDir = scriptRootDir()) {
|
|||
return join(rootDir, 'release-policy.json');
|
||||
}
|
||||
|
||||
export function cliPackageJsonPath(rootDir = scriptRootDir()) {
|
||||
return join(rootDir, 'packages', 'cli', 'package.json');
|
||||
}
|
||||
|
||||
function readJsonSync(path) {
|
||||
return JSON.parse(readFileSync(path, 'utf8'));
|
||||
}
|
||||
|
|
@ -70,20 +74,24 @@ export function assertPublicNpmReleaseTag(tag) {
|
|||
throw new Error(`Invalid public npm release tag: ${tag}`);
|
||||
}
|
||||
|
||||
function readCliPackageVersion(rootDir = scriptRootDir()) {
|
||||
const packageJson = readJsonSync(cliPackageJsonPath(rootDir));
|
||||
return assertPublicNpmPackageVersion(packageJson.version);
|
||||
}
|
||||
|
||||
export function readPublicNpmReleaseMetadata(rootDir = scriptRootDir()) {
|
||||
const policy = readJsonSync(releasePolicyPath(rootDir));
|
||||
const version = assertPublicNpmPackageVersion(policy.publicNpmPackageVersion);
|
||||
const tag = assertPublicNpmReleaseTag(policy.npm?.tag);
|
||||
|
||||
return {
|
||||
packageName: PUBLIC_NPM_PACKAGE_NAME,
|
||||
version,
|
||||
version: readCliPackageVersion(rootDir),
|
||||
tag,
|
||||
};
|
||||
}
|
||||
|
||||
export function publicNpmPackageVersion(rootDir = scriptRootDir()) {
|
||||
return readPublicNpmReleaseMetadata(rootDir).version;
|
||||
return readCliPackageVersion(rootDir);
|
||||
}
|
||||
|
||||
export const PUBLIC_NPM_PACKAGE_VERSION = publicNpmPackageVersion();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { dirname, join, resolve } from 'node:path';
|
|||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
import { packageArtifactLayout, packageReleaseMetadata, verifyArtifactManifest } from './package-artifacts.mjs';
|
||||
import { assertPublicNpmPackageVersion, publicNpmPackageVersion } from './public-npm-release-metadata.mjs';
|
||||
import { publicNpmPackageVersion } from './public-npm-release-metadata.mjs';
|
||||
import { readPublishedPackageSmokeConfig } from './published-package-smoke-config.mjs';
|
||||
|
||||
function scriptRootDir() {
|
||||
|
|
@ -138,8 +138,6 @@ export function validateReleasePolicy(policy) {
|
|||
throw new Error(`Unsupported release policy schemaVersion: ${policy.schemaVersion}`);
|
||||
}
|
||||
assertSupportedReleaseMode(policy.releaseMode);
|
||||
assertString(policy.publicNpmPackageVersion, 'Release policy publicNpmPackageVersion');
|
||||
assertPublicNpmPackageVersion(policy.publicNpmPackageVersion);
|
||||
assertPlainObject(policy.npm, 'Release policy npm');
|
||||
assertPlainObject(policy.python, 'Release policy python');
|
||||
assertPlainObject(policy.publishedPackageSmoke, 'Release policy publishedPackageSmoke');
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ function releasePolicy(overrides = {}) {
|
|||
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
publicNpmPackageVersion: PUBLIC_NPM_PACKAGE_VERSION,
|
||||
releaseMode: 'ci-artifact-only',
|
||||
npm: {
|
||||
publish: false,
|
||||
|
|
|
|||
|
|
@ -166,7 +166,13 @@ function createReleaseConfig(env = process.env) {
|
|||
[
|
||||
'@semantic-release/git',
|
||||
{
|
||||
assets: ['package.json', 'release-policy.json', 'packages/cli/package.json'],
|
||||
assets: [
|
||||
'package.json',
|
||||
'release-policy.json',
|
||||
'packages/cli/package.json',
|
||||
'python/ktx-daemon/pyproject.toml',
|
||||
'python/ktx-sl/pyproject.toml',
|
||||
],
|
||||
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -88,7 +88,13 @@ describe('semantic-release config', () => {
|
|||
const config = createReleaseConfig({ KTX_RELEASE_KIND: kind, GITHUB_REF_NAME: 'main' });
|
||||
const git = gitPluginOptions(config);
|
||||
assert.ok(git, `${kind}: @semantic-release/git plugin must be configured`);
|
||||
assert.deepEqual(git.assets, ['package.json', 'release-policy.json', 'packages/cli/package.json']);
|
||||
assert.deepEqual(git.assets, [
|
||||
'package.json',
|
||||
'release-policy.json',
|
||||
'packages/cli/package.json',
|
||||
'python/ktx-daemon/pyproject.toml',
|
||||
'python/ktx-sl/pyproject.toml',
|
||||
]);
|
||||
assert.match(git.message, /^chore\(release\): \$\{nextRelease\.version\} \[skip ci\]/);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
PUBLIC_NPM_PACKAGE_NAME,
|
||||
assertPublicNpmPackageVersion,
|
||||
assertPublicNpmReleaseTag,
|
||||
publicNpmPackageVersionToPythonVersion,
|
||||
releasePolicyPath,
|
||||
} from './public-npm-release-metadata.mjs';
|
||||
|
||||
|
|
@ -23,6 +24,42 @@ async function writeJson(path, value) {
|
|||
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function pyprojectWithProjectVersion(source, version) {
|
||||
const lines = source.split('\n');
|
||||
let inProject = false;
|
||||
let replaced = false;
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
const sectionMatch = /^\s*\[([^\]]+)\]\s*$/.exec(line);
|
||||
if (sectionMatch) {
|
||||
inProject = sectionMatch[1] === 'project';
|
||||
continue;
|
||||
}
|
||||
if (inProject && /^\s*version\s*=\s*"[^"]*"\s*$/.test(line)) {
|
||||
lines[index] = `version = "${version}"`;
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!replaced) {
|
||||
throw new Error('No [project].version assignment found in pyproject.toml');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function rewritePyprojectVersion(path, version) {
|
||||
const source = await readFile(path, 'utf8');
|
||||
await writeFile(path, pyprojectWithProjectVersion(source, version));
|
||||
}
|
||||
|
||||
function safePythonVersionFor(version) {
|
||||
try {
|
||||
return publicNpmPackageVersionToPythonVersion(version);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePublicReleaseVersion(rootDir, version, tag) {
|
||||
const safeVersion = assertPublicNpmPackageVersion(version);
|
||||
const safeTag = assertPublicNpmReleaseTag(tag);
|
||||
|
|
@ -37,9 +74,15 @@ export async function updatePublicReleaseVersion(rootDir, version, tag) {
|
|||
cliPackageJson.version = safeVersion;
|
||||
await writeJson(cliPackageJsonPath, cliPackageJson);
|
||||
|
||||
const pythonVersion = safePythonVersionFor(safeVersion);
|
||||
if (pythonVersion !== null) {
|
||||
await rewritePyprojectVersion(join(rootDir, 'python', 'ktx-daemon', 'pyproject.toml'), pythonVersion);
|
||||
await rewritePyprojectVersion(join(rootDir, 'python', 'ktx-sl', 'pyproject.toml'), pythonVersion);
|
||||
}
|
||||
|
||||
const policyPath = releasePolicyPath(rootDir);
|
||||
const policy = await readJson(policyPath);
|
||||
policy.publicNpmPackageVersion = safeVersion;
|
||||
delete policy.publicNpmPackageVersion;
|
||||
policy.releaseMode = 'npm-public-release-ready';
|
||||
policy.requiredBeforePublishing = [];
|
||||
policy.npm = {
|
||||
|
|
@ -60,6 +103,7 @@ export async function updatePublicReleaseVersion(rootDir, version, tag) {
|
|||
return {
|
||||
version: safeVersion,
|
||||
tag: safeTag,
|
||||
pythonVersion,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,23 +11,51 @@ async function writeJson(path, value) {
|
|||
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
async function writeText(path, value) {
|
||||
await mkdir(join(path, '..'), { recursive: true });
|
||||
await writeFile(path, value);
|
||||
}
|
||||
|
||||
async function readJson(path) {
|
||||
return JSON.parse(await readFile(path, 'utf8'));
|
||||
}
|
||||
|
||||
async function readText(path) {
|
||||
return readFile(path, 'utf8');
|
||||
}
|
||||
|
||||
const DAEMON_PYPROJECT = `[project]
|
||||
name = "ktx-daemon"
|
||||
version = "0.4.0"
|
||||
description = "Portable compute package for KTX semantic-layer operations"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
`;
|
||||
|
||||
const SL_PYPROJECT = `[project]
|
||||
name = "ktx-sl"
|
||||
version = "0.4.0"
|
||||
description = "Agent-first semantic layer engine with aggregate locality"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
`;
|
||||
|
||||
async function writeReleaseFixture(root) {
|
||||
await writeJson(join(root, 'package.json'), {
|
||||
name: 'ktx-workspace',
|
||||
version: '0.0.0-private',
|
||||
version: '0.4.0',
|
||||
private: true,
|
||||
});
|
||||
await writeJson(join(root, 'packages', 'cli', 'package.json'), {
|
||||
name: '@kaelio/ktx',
|
||||
version: '0.0.0-private',
|
||||
version: '0.4.0',
|
||||
});
|
||||
await writeJson(join(root, 'release-policy.json'), {
|
||||
schemaVersion: 1,
|
||||
publicNpmPackageVersion: '0.1.0-rc.1',
|
||||
releaseMode: 'ci-artifact-only',
|
||||
npm: {
|
||||
publish: false,
|
||||
|
|
@ -43,7 +71,7 @@ async function writeReleaseFixture(root) {
|
|||
},
|
||||
publishedPackageSmoke: {
|
||||
packageName: '@kaelio/ktx',
|
||||
version: '0.1.0-rc.1',
|
||||
version: 'latest',
|
||||
registry: null,
|
||||
},
|
||||
runtimeInstaller: {
|
||||
|
|
@ -53,6 +81,8 @@ async function writeReleaseFixture(root) {
|
|||
},
|
||||
requiredBeforePublishing: ['Choose public release version.'],
|
||||
});
|
||||
await writeText(join(root, 'python', 'ktx-daemon', 'pyproject.toml'), DAEMON_PYPROJECT);
|
||||
await writeText(join(root, 'python', 'ktx-sl', 'pyproject.toml'), SL_PYPROJECT);
|
||||
}
|
||||
|
||||
describe('updatePublicReleaseVersion', () => {
|
||||
|
|
@ -65,9 +95,10 @@ describe('updatePublicReleaseVersion', () => {
|
|||
|
||||
assert.equal((await readJson(join(root, 'package.json'))).version, '0.1.0-rc.2');
|
||||
assert.equal((await readJson(join(root, 'packages', 'cli', 'package.json'))).version, '0.1.0-rc.2');
|
||||
assert.match(await readText(join(root, 'python', 'ktx-daemon', 'pyproject.toml')), /^version = "0\.1\.0rc2"$/m);
|
||||
assert.match(await readText(join(root, 'python', 'ktx-sl', 'pyproject.toml')), /^version = "0\.1\.0rc2"$/m);
|
||||
assert.deepEqual(await readJson(join(root, 'release-policy.json')), {
|
||||
schemaVersion: 1,
|
||||
publicNpmPackageVersion: '0.1.0-rc.2',
|
||||
releaseMode: 'npm-public-release-ready',
|
||||
npm: {
|
||||
publish: true,
|
||||
|
|
@ -110,8 +141,17 @@ describe('updatePublicReleaseVersion', () => {
|
|||
(await readJson(join(root, 'packages', 'cli', 'package.json'))).version,
|
||||
'0.1.0-feature-foo.0',
|
||||
);
|
||||
assert.match(
|
||||
await readText(join(root, 'python', 'ktx-daemon', 'pyproject.toml')),
|
||||
/^version = "0\.4\.0"$/m,
|
||||
);
|
||||
assert.match(
|
||||
await readText(join(root, 'python', 'ktx-sl', 'pyproject.toml')),
|
||||
/^version = "0\.4\.0"$/m,
|
||||
);
|
||||
const policy = await readJson(join(root, 'release-policy.json'));
|
||||
assert.equal(policy.publicNpmPackageVersion, '0.1.0-feature-foo.0');
|
||||
assert.equal(policy.publicNpmPackageVersion, undefined);
|
||||
assert.equal(policy.publishedPackageSmoke.version, '0.1.0-feature-foo.0');
|
||||
assert.equal(policy.npm.tag, 'branch-feature-foo');
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue