From 296b3b79788e29a15444fbc3381687ded30f8c85 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Mon, 11 May 2026 13:25:43 +0200 Subject: [PATCH] chore: encode uv runtime prerequisite policy --- release-policy.json | 5 ++ scripts/release-readiness.mjs | 26 +++++++++ scripts/release-readiness.test.mjs | 86 ++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) diff --git a/release-policy.json b/release-policy.json index a6111c5f..6f275094 100644 --- a/release-policy.json +++ b/release-policy.json @@ -18,5 +18,10 @@ "version": "0.1.0", "registry": null }, + "runtimeInstaller": { + "uvStrategy": "path-prerequisite", + "bootstrapUv": false, + "missingUvBehavior": "focused-error" + }, "requiredBeforePublishing": [] } diff --git a/scripts/release-readiness.mjs b/scripts/release-readiness.mjs index 047ed43f..6b15e83e 100644 --- a/scripts/release-readiness.mjs +++ b/scripts/release-readiness.mjs @@ -103,6 +103,26 @@ function assertRequiredBeforePublishing(policy) { } } +function assertRuntimeInstallerPolicy(policy) { + assertPlainObject(policy.runtimeInstaller, 'Release policy runtimeInstaller'); + assertString(policy.runtimeInstaller.uvStrategy, 'Release policy runtimeInstaller.uvStrategy'); + assertBoolean(policy.runtimeInstaller.bootstrapUv, 'Release policy runtimeInstaller.bootstrapUv'); + assertString( + policy.runtimeInstaller.missingUvBehavior, + 'Release policy runtimeInstaller.missingUvBehavior', + ); + + if (policy.runtimeInstaller.uvStrategy !== 'path-prerequisite') { + throw new Error('Release policy runtimeInstaller.uvStrategy must be path-prerequisite'); + } + if (policy.runtimeInstaller.bootstrapUv !== false) { + throw new Error('Release policy runtimeInstaller.bootstrapUv must be false'); + } + if (policy.runtimeInstaller.missingUvBehavior !== 'focused-error') { + throw new Error('Release policy runtimeInstaller.missingUvBehavior must be focused-error'); + } +} + function assertSameMembers(actual, expected, label) { const sortedActual = [...actual].sort(); const sortedExpected = [...expected].sort(); @@ -136,6 +156,7 @@ export function validateReleasePolicy(policy) { assertNullableString(policy.publishedPackageSmoke.registry, 'Release policy publishedPackageSmoke.registry'); readPublishedPackageSmokeConfig({}, [], policy.publishedPackageSmoke); assertRequiredBeforePublishing(policy); + assertRuntimeInstallerPolicy(policy); return policy; } @@ -277,6 +298,7 @@ export async function releaseReadinessReport(rootDir = scriptRootDir()) { pythonPublishEnabled: policy.python.publish, packageNames: metadata.map((entry) => entry.packageName), publishedPackageSmokeGate: publishedPackageSmokeGate(policy), + runtimeInstaller: policy.runtimeInstaller, npmPublish: policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE ? { @@ -310,6 +332,10 @@ async function main() { process.stdout.write( `Published package smoke registry: ${report.publishedPackageSmokeGate.registry ?? 'default npm registry'}\n`, ); + process.stdout.write(`Runtime uv strategy: ${report.runtimeInstaller.uvStrategy}\n`); + process.stdout.write( + `Runtime uv bootstrap: ${report.runtimeInstaller.bootstrapUv ? 'enabled' : 'disabled'}\n`, + ); if (report.npmPublish) { process.stdout.write( `NPM publish target: ${report.npmPublish.packageName}@${report.npmPublish.version} (${report.npmPublish.tag})\n`, diff --git a/scripts/release-readiness.test.mjs b/scripts/release-readiness.test.mjs index fba913c6..0da18c4c 100644 --- a/scripts/release-readiness.test.mjs +++ b/scripts/release-readiness.test.mjs @@ -86,6 +86,11 @@ function releasePolicy(overrides = {}) { version: 'latest', registry: null, }, + runtimeInstaller: { + uvStrategy: 'path-prerequisite', + bootstrapUv: false, + missingUvBehavior: 'focused-error', + }, requiredBeforePublishing: [ 'Choose public release version.', 'Configure registry credentials outside source control.', @@ -147,6 +152,11 @@ describe('release readiness policy', () => { version: 'latest', registry: null, }, + runtimeInstaller: { + uvStrategy: 'path-prerequisite', + bootstrapUv: false, + missingUvBehavior: 'focused-error', + }, npmPublish: null, blockedPublishingDecisions: [ 'Choose public release version.', @@ -221,6 +231,11 @@ describe('release readiness policy', () => { version: '2026.5.8', registry: 'https://registry.npmjs.org/', }, + runtimeInstaller: { + uvStrategy: 'path-prerequisite', + bootstrapUv: false, + missingUvBehavior: 'focused-error', + }, npmPublish: null, blockedPublishingDecisions: [], }); @@ -268,6 +283,11 @@ describe('release readiness policy', () => { version: PUBLIC_NPM_PACKAGE_VERSION, registry: null, }, + runtimeInstaller: { + uvStrategy: 'path-prerequisite', + bootstrapUv: false, + missingUvBehavior: 'focused-error', + }, npmPublish: { packageName: '@kaelio/ktx', version: PUBLIC_NPM_PACKAGE_VERSION, @@ -282,6 +302,72 @@ describe('release readiness policy', () => { } }); + it('rejects npm public release ready mode without a runtime installer policy', async () => { + const root = await mkdtemp(join(tmpdir(), 'ktx-runtime-policy-missing-test-')); + try { + await writeReadyFixture(root, { + policy: releasePolicy({ + releaseMode: 'npm-public-release-ready', + npm: { + publish: true, + registry: null, + access: 'public', + tag: 'latest', + }, + publishedPackageSmoke: { + packageName: '@kaelio/ktx', + version: PUBLIC_NPM_PACKAGE_VERSION, + registry: null, + }, + runtimeInstaller: undefined, + requiredBeforePublishing: [], + }), + }); + + await assert.rejects( + () => releaseReadinessReport(root), + /Release policy runtimeInstaller must be a JSON object/, + ); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it('rejects uv bootstrap download policy for the first public npm release', async () => { + const root = await mkdtemp(join(tmpdir(), 'ktx-runtime-policy-bootstrap-test-')); + try { + await writeReadyFixture(root, { + policy: releasePolicy({ + releaseMode: 'npm-public-release-ready', + npm: { + publish: true, + registry: null, + access: 'public', + tag: 'latest', + }, + publishedPackageSmoke: { + packageName: '@kaelio/ktx', + version: PUBLIC_NPM_PACKAGE_VERSION, + registry: null, + }, + runtimeInstaller: { + uvStrategy: 'bootstrap-download', + bootstrapUv: true, + missingUvBehavior: 'download', + }, + requiredBeforePublishing: [], + }), + }); + + await assert.rejects( + () => releaseReadinessReport(root), + /Release policy runtimeInstaller\.uvStrategy must be path-prerequisite/, + ); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + it('rejects npm public release ready mode when npm publish is disabled', async () => { const root = await mkdtemp(join(tmpdir(), 'ktx-npm-public-ready-disabled-test-')); try {