diff --git a/release-policy.json b/release-policy.json index f7981837..c19948f2 100644 --- a/release-policy.json +++ b/release-policy.json @@ -4,19 +4,7 @@ "npm": { "publish": false, "registry": null, - "packages": [ - "@ktx/cli", - "@ktx/connector-bigquery", - "@ktx/connector-clickhouse", - "@ktx/connector-mysql", - "@ktx/connector-postgres", - "@ktx/connector-posthog", - "@ktx/connector-snowflake", - "@ktx/connector-sqlite", - "@ktx/connector-sqlserver", - "@ktx/context", - "@ktx/llm" - ] + "packages": ["@kaelio/ktx"] }, "python": { "publish": false, @@ -24,14 +12,12 @@ "packages": ["ktx-sl", "ktx-daemon", "kaelio-ktx"] }, "publishedPackageSmoke": { - "packageName": null, + "packageName": "@kaelio/ktx", "version": "latest", "registry": null }, "requiredBeforePublishing": [ - "Choose npm registry and package visibility.", - "Choose Python package repository.", - "Choose public release versions.", + "Choose public release version.", "Configure registry credentials outside source control.", "Choose release tag and provenance policy." ] diff --git a/scripts/package-artifacts.mjs b/scripts/package-artifacts.mjs index 6ad7beac..ffac1020 100644 --- a/scripts/package-artifacts.mjs +++ b/scripts/package-artifacts.mjs @@ -12,6 +12,10 @@ import { RUNTIME_WHEEL_NORMALIZED_NAME, RUNTIME_WHEEL_PACKAGE_VERSION, } from './build-python-runtime-wheel.mjs'; +import { + PUBLIC_NPM_PACKAGE_NAME, + PUBLIC_NPM_PACKAGE_TARBALL, +} from './build-public-npm-package.mjs'; const PACKAGE_VERSION = '0.0.0-private'; const PYTHON_PACKAGE_VERSION = '0.1.0'; @@ -22,7 +26,7 @@ export { RUNTIME_WHEEL_PACKAGE_VERSION, }; -export const NPM_ARTIFACT_PACKAGES = [ +export const INTERNAL_NPM_WORKSPACE_PACKAGES = [ { name: '@ktx/context', packageRoot: 'packages/context' }, { name: '@ktx/llm', packageRoot: 'packages/llm' }, { name: '@ktx/connector-bigquery', packageRoot: 'packages/connector-bigquery' }, @@ -36,9 +40,11 @@ export const NPM_ARTIFACT_PACKAGES = [ { name: '@ktx/cli', packageRoot: 'packages/cli' }, ]; +export const NPM_ARTIFACT_PACKAGES = [{ name: PUBLIC_NPM_PACKAGE_NAME, packageRoot: 'packages/cli' }]; + export const CLI_PYTHON_ASSET_MANIFEST = 'manifest.json'; -const CONNECTOR_PACKAGE_NAMES = NPM_ARTIFACT_PACKAGES +const CONNECTOR_PACKAGE_NAMES = INTERNAL_NPM_WORKSPACE_PACKAGES .map((packageInfo) => packageInfo.name) .filter((packageName) => packageName.startsWith('@ktx/connector-')); @@ -62,6 +68,9 @@ function scriptRootDir() { } function npmPackageTarballName(packageName) { + if (packageName === PUBLIC_NPM_PACKAGE_NAME) { + return PUBLIC_NPM_PACKAGE_TARBALL; + } return `${packageName.replace('@ktx/', 'ktx-')}-${PACKAGE_VERSION}.tgz`; } @@ -83,17 +92,15 @@ export function packageArtifactLayout(rootDir = scriptRootDir()) { npmDir, pythonDir, npmTarballs, - contextTarball: npmTarballs['@ktx/context'], - cliTarball: npmTarballs['@ktx/cli'], - connectorTarballs: Object.fromEntries( - CONNECTOR_PACKAGE_NAMES.map((packageName) => [packageName, npmTarballs[packageName]]), - ), + contextTarball: npmTarballs[PUBLIC_NPM_PACKAGE_NAME], + cliTarball: npmTarballs[PUBLIC_NPM_PACKAGE_NAME], + connectorTarballs: {}, manifestPath: join(artifactDir, 'manifest.json'), }; } export function buildArtifactCommands(layout) { - const packagesByName = new Map(NPM_ARTIFACT_PACKAGES.map((packageInfo) => [packageInfo.name, packageInfo])); + const packagesByName = new Map(INTERNAL_NPM_WORKSPACE_PACKAGES.map((packageInfo) => [packageInfo.name, packageInfo])); const npmBuildCommands = NPM_ARTIFACT_BUILD_ORDER.map((packageName) => { const packageInfo = packagesByName.get(packageName); if (!packageInfo) { @@ -105,11 +112,11 @@ export function buildArtifactCommands(layout) { cwd: layout.rootDir, }; }); - const npmPackCommands = NPM_ARTIFACT_PACKAGES.map((packageInfo) => ({ - command: 'pnpm', - args: ['--filter', packageInfo.name, 'pack', '--out', layout.npmTarballs[packageInfo.name]], + const publicPackageCommand = { + command: process.execPath, + args: ['scripts/build-public-npm-package.mjs'], cwd: layout.rootDir, - })); + }; return [ ...npmBuildCommands, @@ -128,7 +135,7 @@ export function buildArtifactCommands(layout) { args: ['build', '--package', 'ktx-daemon', '--out-dir', layout.pythonDir], cwd: layout.rootDir, }, - ...npmPackCommands, + publicPackageCommand, ]; } @@ -241,17 +248,18 @@ function releaseMetadataEntry({ ecosystem, packageName, packageRoot, packageVers async function readNpmPackageMetadata(rootDir, packageInfo) { const packageJson = await readJson(join(rootDir, packageInfo.packageRoot, 'package.json')); - if (packageJson.name !== packageInfo.name) { + const expectedSourceName = packageInfo.name === PUBLIC_NPM_PACKAGE_NAME ? '@ktx/cli' : packageInfo.name; + if (packageJson.name !== expectedSourceName) { throw new Error( - `Unexpected package name in ${packageInfo.packageRoot}/package.json: expected ${packageInfo.name}, got ${packageJson.name}`, + `Unexpected package name in ${packageInfo.packageRoot}/package.json: expected ${expectedSourceName}, got ${packageJson.name}`, ); } return releaseMetadataEntry({ ecosystem: 'npm', - packageName: packageJson.name, + packageName: packageInfo.name, packageRoot: packageInfo.packageRoot, packageVersion: packageJson.version, - privatePackage: packageJson.private === true, + privatePackage: packageInfo.name === PUBLIC_NPM_PACKAGE_NAME ? false : packageJson.private === true, }); } @@ -1623,8 +1631,8 @@ async function buildArtifacts(layout) { await mkdir(layout.pythonDir, { recursive: true }); const commands = buildArtifactCommands(layout); - const npmBuildCount = NPM_ARTIFACT_PACKAGES.length; - const npmPackStart = commands.length - NPM_ARTIFACT_PACKAGES.length; + const npmBuildCount = NPM_ARTIFACT_BUILD_ORDER.length; + const npmPackStart = commands.length - 1; for (const command of commands.slice(0, npmBuildCount)) { await runCommand(command.command, command.args, { cwd: command.cwd }); diff --git a/scripts/package-artifacts.test.mjs b/scripts/package-artifacts.test.mjs index 99ab36cb..fddd08a8 100644 --- a/scripts/package-artifacts.test.mjs +++ b/scripts/package-artifacts.test.mjs @@ -7,6 +7,7 @@ import { describe, it } from 'node:test'; import { CLI_PYTHON_ASSET_MANIFEST, + INTERNAL_NPM_WORKSPACE_PACKAGES, RUNTIME_WHEEL_DISTRIBUTION_NAME, RUNTIME_WHEEL_NORMALIZED_NAME, RUNTIME_WHEEL_PACKAGE_VERSION, @@ -34,35 +35,17 @@ async function writeJson(path, value) { await writeFile(path, `${JSON.stringify(value, null, 2)}\n`); } -const CONNECTOR_PACKAGE_NAMES = [ - '@ktx/connector-bigquery', - '@ktx/connector-clickhouse', - '@ktx/connector-mysql', - '@ktx/connector-postgres', - '@ktx/connector-posthog', - '@ktx/connector-snowflake', - '@ktx/connector-sqlite', - '@ktx/connector-sqlserver', -]; - +const INTERNAL_BUILD_PACKAGE_NAMES = INTERNAL_NPM_WORKSPACE_PACKAGES.map((packageInfo) => packageInfo.name); +const CONNECTOR_PACKAGE_NAMES = INTERNAL_BUILD_PACKAGE_NAMES.filter((packageName) => + packageName.startsWith('@ktx/connector-'), +); const NPM_BUILD_PACKAGE_ORDER = ['@ktx/llm', '@ktx/context', ...CONNECTOR_PACKAGE_NAMES, '@ktx/cli']; -function packageRootForName(packageName) { - return `packages/${packageName.replace('@ktx/', '')}`; -} - -function expectedNpmArtifactPath(packageName) { - return `npm/${packageName.replace('@ktx/', 'ktx-')}-0.0.0-private.tgz`; -} - async function writeReleaseMetadataInputs(root) { - const npmPackages = ['@ktx/context', '@ktx/llm', ...CONNECTOR_PACKAGE_NAMES, '@ktx/cli']; - - for (const packageName of npmPackages) { - const packageRoot = packageName === '@ktx/context' ? 'packages/context' : packageRootForName(packageName); - await mkdir(join(root, packageRoot), { recursive: true }); - await writeJson(join(root, packageRoot, 'package.json'), { - name: packageName, + for (const packageInfo of INTERNAL_NPM_WORKSPACE_PACKAGES) { + await mkdir(join(root, packageInfo.packageRoot), { recursive: true }); + await writeJson(join(root, packageInfo.packageRoot, 'package.json'), { + name: packageInfo.name, version: '0.0.0-private', private: true, }); @@ -111,20 +94,8 @@ describe('packageArtifactLayout', () => { assert.equal(layout.artifactDir, '/repo/ktx/dist/artifacts'); assert.equal(layout.npmDir, '/repo/ktx/dist/artifacts/npm'); assert.equal(layout.pythonDir, '/repo/ktx/dist/artifacts/python'); - assert.equal(layout.contextTarball, '/repo/ktx/dist/artifacts/npm/ktx-context-0.0.0-private.tgz'); - assert.equal(layout.cliTarball, '/repo/ktx/dist/artifacts/npm/ktx-cli-0.0.0-private.tgz'); - assert.equal( - layout.connectorTarballs['@ktx/connector-sqlite'], - '/repo/ktx/dist/artifacts/npm/ktx-connector-sqlite-0.0.0-private.tgz', - ); - assert.equal( - layout.connectorTarballs['@ktx/connector-postgres'], - '/repo/ktx/dist/artifacts/npm/ktx-connector-postgres-0.0.0-private.tgz', - ); - assert.deepEqual( - Object.keys(layout.npmTarballs), - NPM_ARTIFACT_PACKAGES.map((packageInfo) => packageInfo.name), - ); + assert.equal(layout.cliTarball, '/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.0.0-private.tgz'); + assert.deepEqual(Object.keys(layout.npmTarballs), ['@kaelio/ktx']); }); }); @@ -134,34 +105,23 @@ describe('buildArtifactCommands', () => { const commands = buildArtifactCommands(layout); assert.deepEqual( - commands.slice(0, NPM_ARTIFACT_PACKAGES.length).map((command) => [command.command, command.args]), + commands.slice(0, NPM_BUILD_PACKAGE_ORDER.length).map((command) => [command.command, command.args]), NPM_BUILD_PACKAGE_ORDER.map((packageName) => ['pnpm', ['--filter', packageName, 'run', 'build']]), ); assert.deepEqual( - commands - .slice(NPM_ARTIFACT_PACKAGES.length, NPM_ARTIFACT_PACKAGES.length + 3) - .map((command) => [command.command, command.args]), + commands.slice(NPM_BUILD_PACKAGE_ORDER.length, NPM_BUILD_PACKAGE_ORDER.length + 3).map((command) => [ + command.command, + command.args, + ]), [ - [ - process.execPath, - ['scripts/build-python-runtime-wheel.mjs'], - ], - [ - 'uv', - ['build', '--package', 'ktx-sl', '--out-dir', '/repo/ktx/dist/artifacts/python'], - ], - [ - 'uv', - ['build', '--package', 'ktx-daemon', '--out-dir', '/repo/ktx/dist/artifacts/python'], - ], + [process.execPath, ['scripts/build-python-runtime-wheel.mjs']], + ['uv', ['build', '--package', 'ktx-sl', '--out-dir', '/repo/ktx/dist/artifacts/python']], + ['uv', ['build', '--package', 'ktx-daemon', '--out-dir', '/repo/ktx/dist/artifacts/python']], ], ); assert.deepEqual( - commands.slice(NPM_ARTIFACT_PACKAGES.length + 3).map((command) => [command.command, command.args]), - NPM_ARTIFACT_PACKAGES.map((packageInfo) => [ - 'pnpm', - ['--filter', packageInfo.name, 'pack', '--out', layout.npmTarballs[packageInfo.name]], - ]), + commands.slice(NPM_BUILD_PACKAGE_ORDER.length + 3).map((command) => [command.command, command.args]), + [[process.execPath, ['scripts/build-public-npm-package.mjs']]], ); }); }); @@ -173,14 +133,14 @@ describe('packageReleaseMetadata', () => { await writeReleaseMetadataInputs(root); assert.deepEqual(await packageReleaseMetadata(root), [ - ...NPM_ARTIFACT_PACKAGES.map((packageInfo) => ({ + { ecosystem: 'npm', - packageName: packageInfo.name, - packageRoot: packageInfo.packageRoot, + packageName: '@kaelio/ktx', + packageRoot: 'packages/cli', packageVersion: '0.0.0-private', - private: true, + private: false, releaseMode: 'ci-artifact-only', - })), + }, { ecosystem: 'python', packageName: 'ktx-sl', @@ -262,14 +222,16 @@ describe('artifact manifest', () => { assert.equal(manifest.sourceRevision, 'abc123'); assert.deepEqual( manifest.packages.filter((entry) => entry.ecosystem === 'npm'), - NPM_ARTIFACT_PACKAGES.map((packageInfo) => ({ - ecosystem: 'npm', - packageName: packageInfo.name, - packageRoot: packageInfo.packageRoot, - packageVersion: '0.0.0-private', - private: true, - releaseMode: 'ci-artifact-only', - })), + [ + { + ecosystem: 'npm', + packageName: '@kaelio/ktx', + packageRoot: 'packages/cli', + packageVersion: '0.0.0-private', + private: false, + releaseMode: 'ci-artifact-only', + }, + ], ); assert.deepEqual( manifest.packages.filter((entry) => entry.ecosystem === 'python'), @@ -311,13 +273,15 @@ describe('artifact manifest', () => { path: file.path, })) .sort((left, right) => left.packageName.localeCompare(right.packageName)), - NPM_ARTIFACT_PACKAGES.map((packageInfo) => ({ - artifactKind: 'tarball', - ecosystem: 'npm', - packageName: packageInfo.name, - packageVersion: '0.0.0-private', - path: expectedNpmArtifactPath(packageInfo.name), - })).sort((left, right) => left.packageName.localeCompare(right.packageName)), + [ + { + artifactKind: 'tarball', + ecosystem: 'npm', + packageName: '@kaelio/ktx', + packageVersion: '0.0.0-private', + path: 'npm/kaelio-ktx-0.0.0-private.tgz', + }, + ], ); assert.deepEqual( manifest.files @@ -368,10 +332,10 @@ describe('artifact manifest', () => { ], ); - const sqliteEntry = manifest.files.find((file) => file.path === 'npm/ktx-connector-sqlite-0.0.0-private.tgz'); - assert.ok(sqliteEntry); - assert.equal(sqliteEntry.bytes, Buffer.byteLength('@ktx/connector-sqlite-tarball')); - assert.equal(sqliteEntry.sha256, createHash('sha256').update('@ktx/connector-sqlite-tarball').digest('hex')); + const npmEntry = manifest.files.find((file) => file.path === 'npm/kaelio-ktx-0.0.0-private.tgz'); + assert.ok(npmEntry); + assert.equal(npmEntry.bytes, Buffer.byteLength('@kaelio/ktx-tarball')); + assert.equal(npmEntry.sha256, createHash('sha256').update('@kaelio/ktx-tarball').digest('hex')); const writtenManifest = JSON.parse(await readFile(artifactManifestPath(layout), 'utf-8')); assert.deepEqual(writtenManifest, manifest); diff --git a/scripts/release-readiness.mjs b/scripts/release-readiness.mjs index 48a57e6c..39814aae 100644 --- a/scripts/release-readiness.mjs +++ b/scripts/release-readiness.mjs @@ -180,7 +180,12 @@ function assertNonPublishingArtifactPolicy(policy, metadata) { throw new Error(`Package ${entry.packageName} releaseMode must remain ci-artifact-only`); } if (entry.ecosystem === 'npm') { - if (entry.private !== true) { + const isPublicKtxPackage = entry.packageName === '@kaelio/ktx'; + if (isPublicKtxPackage) { + if (entry.private !== false) { + throw new Error(`${policyLabel} npm package @kaelio/ktx must be publishable when npm.publish is false`); + } + } else if (entry.private !== true) { throw new Error(`${policyLabel} npm package ${entry.packageName} must remain private`); } if (!entry.packageVersion.endsWith('-private')) { diff --git a/scripts/release-readiness.test.mjs b/scripts/release-readiness.test.mjs index 1c49bb67..08a5c2a7 100644 --- a/scripts/release-readiness.test.mjs +++ b/scripts/release-readiness.test.mjs @@ -4,25 +4,25 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, it } from 'node:test'; -import { NPM_ARTIFACT_PACKAGES, packageArtifactLayout, writeArtifactManifest } from './package-artifacts.mjs'; +import { + INTERNAL_NPM_WORKSPACE_PACKAGES, + NPM_ARTIFACT_PACKAGES, + packageArtifactLayout, + writeArtifactManifest, +} from './package-artifacts.mjs'; import { readReleasePolicy, releasePolicyPath, releaseReadinessReport } from './release-readiness.mjs'; async function writeJson(path, value) { await writeFile(path, `${JSON.stringify(value, null, 2)}\n`); } -async function writeReleaseMetadataInputs(root, options = {}) { - for (const packageInfo of NPM_ARTIFACT_PACKAGES) { +async function writeReleaseMetadataInputs(root) { + for (const packageInfo of INTERNAL_NPM_WORKSPACE_PACKAGES) { await mkdir(join(root, packageInfo.packageRoot), { recursive: true }); await writeJson(join(root, packageInfo.packageRoot, 'package.json'), { name: packageInfo.name, version: '0.0.0-private', - private: - packageInfo.name === '@ktx/context' - ? (options.contextPrivate ?? true) - : packageInfo.name === '@ktx/cli' - ? (options.cliPrivate ?? true) - : true, + private: true, }); } @@ -69,7 +69,7 @@ function releasePolicy(overrides = {}) { npm: { publish: false, registry: null, - packages: NPM_ARTIFACT_PACKAGES.map((packageInfo) => packageInfo.name), + packages: ['@kaelio/ktx'], ...npmOverrides, }, python: { @@ -79,14 +79,12 @@ function releasePolicy(overrides = {}) { ...pythonOverrides, }, publishedPackageSmoke: { - packageName: null, + packageName: '@kaelio/ktx', version: 'latest', registry: null, }, requiredBeforePublishing: [ - 'Choose npm registry and package visibility.', - 'Choose Python package repository.', - 'Choose public release versions.', + 'Choose public release version.', 'Configure registry credentials outside source control.', 'Choose release tag and provenance policy.', ], @@ -99,7 +97,7 @@ async function writePolicy(root, policy = releasePolicy()) { } async function writeReadyFixture(root, options = {}) { - await writeReleaseMetadataInputs(root, options); + await writeReleaseMetadataInputs(root); await writePolicy(root, options.policy ?? releasePolicy()); const layout = packageArtifactLayout(root); await writeUploadableArtifactFixtures(layout); @@ -136,25 +134,18 @@ describe('release readiness policy', () => { sourceRevision: 'abc123', npmPublishEnabled: false, pythonPublishEnabled: false, - packageNames: [ - ...NPM_ARTIFACT_PACKAGES.map((packageInfo) => packageInfo.name), - 'ktx-sl', - 'ktx-daemon', - 'kaelio-ktx', - ], + packageNames: ['@kaelio/ktx', 'ktx-sl', 'ktx-daemon', 'kaelio-ktx'], publishedPackageSmokeGate: { status: 'not_required', script: 'pnpm run release:published-smoke', reason: 'Published package smoke remains pending until release-policy.json enables npm registry publishing.', - configSource: null, - packageName: null, + configSource: 'release-policy', + packageName: '@kaelio/ktx', version: 'latest', registry: null, }, blockedPublishingDecisions: [ - 'Choose npm registry and package visibility.', - 'Choose Python package repository.', - 'Choose public release versions.', + 'Choose public release version.', 'Configure registry credentials outside source control.', 'Choose release tag and provenance policy.', ], @@ -170,7 +161,7 @@ describe('release readiness policy', () => { await writeReadyFixture(root, { policy: releasePolicy({ publishedPackageSmoke: { - packageName: '@ktx/cli-public', + packageName: '@kaelio/ktx', version: '2026.5.8', registry: 'https://registry.npmjs.org/', }, @@ -184,7 +175,7 @@ describe('release readiness policy', () => { script: 'pnpm run release:published-smoke', reason: 'Published package smoke remains pending until release-policy.json enables npm registry publishing.', configSource: 'release-policy', - packageName: '@ktx/cli-public', + packageName: '@kaelio/ktx', version: '2026.5.8', registry: 'https://registry.npmjs.org/', }); @@ -200,7 +191,7 @@ describe('release readiness policy', () => { policy: releasePolicy({ releaseMode: 'published-package-smoke-required', publishedPackageSmoke: { - packageName: '@ktx/cli-public', + packageName: '@kaelio/ktx', version: '2026.5.8', registry: 'https://registry.npmjs.org/', }, @@ -216,18 +207,13 @@ describe('release readiness policy', () => { sourceRevision: 'abc123', npmPublishEnabled: false, pythonPublishEnabled: false, - packageNames: [ - ...NPM_ARTIFACT_PACKAGES.map((packageInfo) => packageInfo.name), - 'ktx-sl', - 'ktx-daemon', - 'kaelio-ktx', - ], + packageNames: ['@kaelio/ktx', 'ktx-sl', 'ktx-daemon', 'kaelio-ktx'], publishedPackageSmokeGate: { status: 'required', script: 'pnpm run release:published-smoke', reason: 'Run the published package smoke before accepting the hybrid-search release.', configSource: 'release-policy', - packageName: '@ktx/cli-public', + packageName: '@kaelio/ktx', version: '2026.5.8', registry: 'https://registry.npmjs.org/', }, @@ -244,6 +230,11 @@ describe('release readiness policy', () => { await writeReadyFixture(root, { policy: releasePolicy({ releaseMode: 'published-package-smoke-required', + publishedPackageSmoke: { + packageName: null, + version: 'latest', + registry: null, + }, requiredBeforePublishing: [], }), }); @@ -264,7 +255,7 @@ describe('release readiness policy', () => { policy: releasePolicy({ releaseMode: 'published-package-smoke-required', publishedPackageSmoke: { - packageName: '@ktx/cli-public', + packageName: '@kaelio/ktx', version: 'latest', registry: null, }, @@ -356,14 +347,20 @@ describe('release readiness policy', () => { } }); - it('rejects a public npm package while releaseMode is ci-artifact-only', async () => { - const root = await mkdtemp(join(tmpdir(), 'ktx-release-public-npm-test-')); + it('rejects release policy that still lists internal npm packages', async () => { + const root = await mkdtemp(join(tmpdir(), 'ktx-release-stale-internal-npm-policy-test-')); try { - await writeReadyFixture(root, { contextPrivate: false }); + await writeReadyFixture(root, { + policy: releasePolicy({ + npm: { + packages: ['@kaelio/ktx', '@ktx/context'], + }, + }), + }); await assert.rejects( () => releaseReadinessReport(root), - /ci-artifact-only policy npm package @ktx\/context must remain private/, + /Release policy npm\.packages mismatch/, ); } finally { await rm(root, { recursive: true, force: true });