mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
release: add npm public release policy
This commit is contained in:
parent
34f5a544e3
commit
81674c3017
3 changed files with 220 additions and 18 deletions
|
|
@ -1,9 +1,11 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"releaseMode": "ci-artifact-only",
|
||||
"releaseMode": "npm-public-release-ready",
|
||||
"npm": {
|
||||
"publish": false,
|
||||
"publish": true,
|
||||
"registry": null,
|
||||
"access": "public",
|
||||
"tag": "latest",
|
||||
"packages": ["@kaelio/ktx"]
|
||||
},
|
||||
"python": {
|
||||
|
|
@ -13,12 +15,8 @@
|
|||
},
|
||||
"publishedPackageSmoke": {
|
||||
"packageName": "@kaelio/ktx",
|
||||
"version": "latest",
|
||||
"version": "0.1.0",
|
||||
"registry": null
|
||||
},
|
||||
"requiredBeforePublishing": [
|
||||
"Choose public release version.",
|
||||
"Configure registry credentials outside source control.",
|
||||
"Choose release tag and provenance policy."
|
||||
]
|
||||
"requiredBeforePublishing": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { dirname, join, resolve } from 'node:path';
|
|||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
import { packageArtifactLayout, packageReleaseMetadata, verifyArtifactManifest } from './package-artifacts.mjs';
|
||||
import { PUBLIC_NPM_PACKAGE_VERSION } from './build-public-npm-package.mjs';
|
||||
import { readPublishedPackageSmokeConfig } from './published-package-smoke-config.mjs';
|
||||
|
||||
function scriptRootDir() {
|
||||
|
|
@ -21,9 +22,11 @@ async function readJson(path) {
|
|||
|
||||
const CI_ARTIFACT_ONLY_RELEASE_MODE = 'ci-artifact-only';
|
||||
const PUBLISHED_PACKAGE_SMOKE_REQUIRED_RELEASE_MODE = 'published-package-smoke-required';
|
||||
const NPM_PUBLIC_RELEASE_READY_MODE = 'npm-public-release-ready';
|
||||
const SUPPORTED_RELEASE_MODES = new Set([
|
||||
CI_ARTIFACT_ONLY_RELEASE_MODE,
|
||||
PUBLISHED_PACKAGE_SMOKE_REQUIRED_RELEASE_MODE,
|
||||
NPM_PUBLIC_RELEASE_READY_MODE,
|
||||
]);
|
||||
|
||||
export async function readReleasePolicy(rootDir = scriptRootDir()) {
|
||||
|
|
@ -64,6 +67,19 @@ function assertStringArray(value, label) {
|
|||
}
|
||||
}
|
||||
|
||||
function assertNpmAccess(value) {
|
||||
if (value !== 'public') {
|
||||
throw new Error('Release policy npm.access must be public');
|
||||
}
|
||||
}
|
||||
|
||||
function assertNpmTag(value) {
|
||||
assertString(value, 'Release policy npm.tag');
|
||||
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value)) {
|
||||
throw new Error(`Invalid Release policy npm.tag: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertSupportedReleaseMode(releaseMode) {
|
||||
assertString(releaseMode, 'Release policy releaseMode');
|
||||
if (!SUPPORTED_RELEASE_MODES.has(releaseMode)) {
|
||||
|
|
@ -79,10 +95,11 @@ function assertRequiredBeforePublishing(policy) {
|
|||
}
|
||||
|
||||
if (
|
||||
policy.releaseMode === PUBLISHED_PACKAGE_SMOKE_REQUIRED_RELEASE_MODE &&
|
||||
(policy.releaseMode === PUBLISHED_PACKAGE_SMOKE_REQUIRED_RELEASE_MODE ||
|
||||
policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE) &&
|
||||
policy.requiredBeforePublishing.length > 0
|
||||
) {
|
||||
throw new Error('published-package-smoke-required release mode requires requiredBeforePublishing to be empty');
|
||||
throw new Error(`${policy.releaseMode} release mode requires requiredBeforePublishing to be empty`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,6 +124,8 @@ export function validateReleasePolicy(policy) {
|
|||
|
||||
assertBoolean(policy.npm.publish, 'Release policy npm.publish');
|
||||
assertNullableString(policy.npm.registry, 'Release policy npm.registry');
|
||||
assertNpmAccess(policy.npm.access);
|
||||
assertNpmTag(policy.npm.tag);
|
||||
assertStringArray(policy.npm.packages, 'Release policy npm.packages');
|
||||
|
||||
assertBoolean(policy.python.publish, 'Release policy python.publish');
|
||||
|
|
@ -128,10 +147,12 @@ function metadataNames(metadata, ecosystem) {
|
|||
function publishedPackageSmokeGate(policy) {
|
||||
const config = readPublishedPackageSmokeConfig({}, [], policy.publishedPackageSmoke);
|
||||
|
||||
if (policy.releaseMode === PUBLISHED_PACKAGE_SMOKE_REQUIRED_RELEASE_MODE && !config.enabled) {
|
||||
throw new Error(
|
||||
'published-package-smoke-required release mode requires release-policy.json publishedPackageSmoke.packageName',
|
||||
);
|
||||
if (
|
||||
(policy.releaseMode === PUBLISHED_PACKAGE_SMOKE_REQUIRED_RELEASE_MODE ||
|
||||
policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE) &&
|
||||
!config.enabled
|
||||
) {
|
||||
throw new Error(`${policy.releaseMode} release mode requires release-policy.json publishedPackageSmoke.packageName`);
|
||||
}
|
||||
|
||||
const base =
|
||||
|
|
@ -140,6 +161,11 @@ function publishedPackageSmokeGate(policy) {
|
|||
status: 'not_required',
|
||||
reason: 'Published package smoke remains pending until release-policy.json enables npm registry publishing.',
|
||||
}
|
||||
: policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE
|
||||
? {
|
||||
status: 'required',
|
||||
reason: 'Run the published package smoke after the npm package is published.',
|
||||
}
|
||||
: {
|
||||
status: 'required',
|
||||
reason: 'Run the published package smoke before accepting the hybrid-search release.',
|
||||
|
|
@ -185,23 +211,63 @@ function assertNonPublishingArtifactPolicy(policy, metadata) {
|
|||
if (entry.private !== false) {
|
||||
throw new Error(`${policyLabel} npm package @kaelio/ktx must be publishable when npm.publish is false`);
|
||||
}
|
||||
if (entry.packageVersion !== PUBLIC_NPM_PACKAGE_VERSION) {
|
||||
throw new Error(`${policyLabel} npm package @kaelio/ktx must use public version ${PUBLIC_NPM_PACKAGE_VERSION}`);
|
||||
}
|
||||
} else if (entry.private !== true) {
|
||||
throw new Error(`${policyLabel} npm package ${entry.packageName} must remain private`);
|
||||
}
|
||||
if (!entry.packageVersion.endsWith('-private')) {
|
||||
} else if (!entry.packageVersion.endsWith('-private')) {
|
||||
throw new Error(`${policyLabel} npm package ${entry.packageName} must use a private version suffix`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertNpmPublicReleaseReadyPolicy(policy, metadata) {
|
||||
if (policy.npm.publish !== true) {
|
||||
throw new Error('npm-public-release-ready policy requires npm.publish true');
|
||||
}
|
||||
if (policy.python.publish !== false) {
|
||||
throw new Error('npm-public-release-ready policy keeps python.publish false');
|
||||
}
|
||||
if (policy.python.repository !== null) {
|
||||
throw new Error('npm-public-release-ready policy keeps python.repository null');
|
||||
}
|
||||
|
||||
assertSameMembers(policy.npm.packages, ['@kaelio/ktx'], 'Release policy npm.packages');
|
||||
assertSameMembers(policy.python.packages, metadataNames(metadata, 'python'), 'Release policy python.packages');
|
||||
|
||||
const npmMetadata = metadata.find((entry) => entry.ecosystem === 'npm' && entry.packageName === '@kaelio/ktx');
|
||||
if (!npmMetadata) {
|
||||
throw new Error('npm-public-release-ready policy requires @kaelio/ktx artifact metadata');
|
||||
}
|
||||
if (npmMetadata.private !== false) {
|
||||
throw new Error('npm-public-release-ready policy requires @kaelio/ktx to be publishable');
|
||||
}
|
||||
if (npmMetadata.packageVersion !== PUBLIC_NPM_PACKAGE_VERSION) {
|
||||
throw new Error(
|
||||
`npm-public-release-ready policy expected @kaelio/ktx ${PUBLIC_NPM_PACKAGE_VERSION}, got ${npmMetadata.packageVersion}`,
|
||||
);
|
||||
}
|
||||
if (policy.publishedPackageSmoke.packageName !== '@kaelio/ktx') {
|
||||
throw new Error('npm-public-release-ready policy requires publishedPackageSmoke.packageName @kaelio/ktx');
|
||||
}
|
||||
if (policy.publishedPackageSmoke.version !== PUBLIC_NPM_PACKAGE_VERSION) {
|
||||
throw new Error(`npm-public-release-ready policy requires publishedPackageSmoke.version ${PUBLIC_NPM_PACKAGE_VERSION}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function releaseReadinessReport(rootDir = scriptRootDir()) {
|
||||
const policy = validateReleasePolicy(await readReleasePolicy(rootDir));
|
||||
const layout = packageArtifactLayout(rootDir);
|
||||
const manifest = await verifyArtifactManifest(layout);
|
||||
const metadata = await packageReleaseMetadata(rootDir);
|
||||
|
||||
assertNonPublishingArtifactPolicy(policy, metadata);
|
||||
if (policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE) {
|
||||
assertNpmPublicReleaseReadyPolicy(policy, metadata);
|
||||
} else {
|
||||
assertNonPublishingArtifactPolicy(policy, metadata);
|
||||
}
|
||||
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
|
|
@ -211,6 +277,16 @@ export async function releaseReadinessReport(rootDir = scriptRootDir()) {
|
|||
pythonPublishEnabled: policy.python.publish,
|
||||
packageNames: metadata.map((entry) => entry.packageName),
|
||||
publishedPackageSmokeGate: publishedPackageSmokeGate(policy),
|
||||
npmPublish:
|
||||
policy.releaseMode === NPM_PUBLIC_RELEASE_READY_MODE
|
||||
? {
|
||||
packageName: '@kaelio/ktx',
|
||||
version: PUBLIC_NPM_PACKAGE_VERSION,
|
||||
access: policy.npm.access,
|
||||
tag: policy.npm.tag,
|
||||
registry: policy.npm.registry,
|
||||
}
|
||||
: null,
|
||||
blockedPublishingDecisions: policy.requiredBeforePublishing,
|
||||
};
|
||||
}
|
||||
|
|
@ -234,7 +310,13 @@ async function main() {
|
|||
process.stdout.write(
|
||||
`Published package smoke registry: ${report.publishedPackageSmokeGate.registry ?? 'default npm registry'}\n`,
|
||||
);
|
||||
process.stdout.write('Registry publishing remains disabled by release-policy.json.\n');
|
||||
if (report.npmPublish) {
|
||||
process.stdout.write(
|
||||
`NPM publish target: ${report.npmPublish.packageName}@${report.npmPublish.version} (${report.npmPublish.tag})\n`,
|
||||
);
|
||||
} else {
|
||||
process.stdout.write('Registry publishing remains disabled by release-policy.json.\n');
|
||||
}
|
||||
process.stdout.write('Required decisions before publishing:\n');
|
||||
for (const decision of report.blockedPublishingDecisions) {
|
||||
process.stdout.write(`- ${decision}\n`);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
packageArtifactLayout,
|
||||
writeArtifactManifest,
|
||||
} from './package-artifacts.mjs';
|
||||
import { PUBLIC_NPM_PACKAGE_VERSION } from './build-public-npm-package.mjs';
|
||||
import { readReleasePolicy, releasePolicyPath, releaseReadinessReport } from './release-readiness.mjs';
|
||||
|
||||
async function writeJson(path, value) {
|
||||
|
|
@ -69,6 +70,8 @@ function releasePolicy(overrides = {}) {
|
|||
npm: {
|
||||
publish: false,
|
||||
registry: null,
|
||||
access: 'public',
|
||||
tag: 'latest',
|
||||
packages: ['@kaelio/ktx'],
|
||||
...npmOverrides,
|
||||
},
|
||||
|
|
@ -144,6 +147,7 @@ describe('release readiness policy', () => {
|
|||
version: 'latest',
|
||||
registry: null,
|
||||
},
|
||||
npmPublish: null,
|
||||
blockedPublishingDecisions: [
|
||||
'Choose public release version.',
|
||||
'Configure registry credentials outside source control.',
|
||||
|
|
@ -217,6 +221,7 @@ describe('release readiness policy', () => {
|
|||
version: '2026.5.8',
|
||||
registry: 'https://registry.npmjs.org/',
|
||||
},
|
||||
npmPublish: null,
|
||||
blockedPublishingDecisions: [],
|
||||
});
|
||||
} finally {
|
||||
|
|
@ -224,6 +229,123 @@ describe('release readiness policy', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('accepts the npm public release ready policy', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-npm-public-ready-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,
|
||||
},
|
||||
requiredBeforePublishing: [],
|
||||
}),
|
||||
});
|
||||
|
||||
const report = await releaseReadinessReport(root);
|
||||
|
||||
assert.deepEqual(report, {
|
||||
schemaVersion: 1,
|
||||
releaseMode: 'npm-public-release-ready',
|
||||
sourceRevision: 'abc123',
|
||||
npmPublishEnabled: true,
|
||||
pythonPublishEnabled: false,
|
||||
packageNames: ['@kaelio/ktx', 'ktx-sl', 'ktx-daemon', 'kaelio-ktx'],
|
||||
publishedPackageSmokeGate: {
|
||||
status: 'required',
|
||||
script: 'pnpm run release:published-smoke',
|
||||
reason: 'Run the published package smoke after the npm package is published.',
|
||||
configSource: 'release-policy',
|
||||
packageName: '@kaelio/ktx',
|
||||
version: PUBLIC_NPM_PACKAGE_VERSION,
|
||||
registry: null,
|
||||
},
|
||||
npmPublish: {
|
||||
packageName: '@kaelio/ktx',
|
||||
version: PUBLIC_NPM_PACKAGE_VERSION,
|
||||
access: 'public',
|
||||
tag: 'latest',
|
||||
registry: null,
|
||||
},
|
||||
blockedPublishingDecisions: [],
|
||||
});
|
||||
} 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 {
|
||||
await writeReadyFixture(root, {
|
||||
policy: releasePolicy({
|
||||
releaseMode: 'npm-public-release-ready',
|
||||
npm: {
|
||||
publish: false,
|
||||
registry: null,
|
||||
access: 'public',
|
||||
tag: 'latest',
|
||||
},
|
||||
publishedPackageSmoke: {
|
||||
packageName: '@kaelio/ktx',
|
||||
version: PUBLIC_NPM_PACKAGE_VERSION,
|
||||
registry: null,
|
||||
},
|
||||
requiredBeforePublishing: [],
|
||||
}),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => releaseReadinessReport(root),
|
||||
/npm-public-release-ready policy requires npm.publish true/,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects npm public release ready mode when Python publishing is enabled', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-npm-public-ready-python-test-'));
|
||||
try {
|
||||
await writeReadyFixture(root, {
|
||||
policy: releasePolicy({
|
||||
releaseMode: 'npm-public-release-ready',
|
||||
npm: {
|
||||
publish: true,
|
||||
registry: null,
|
||||
access: 'public',
|
||||
tag: 'latest',
|
||||
},
|
||||
python: {
|
||||
publish: true,
|
||||
repository: 'pypi',
|
||||
},
|
||||
publishedPackageSmoke: {
|
||||
packageName: '@kaelio/ktx',
|
||||
version: PUBLIC_NPM_PACKAGE_VERSION,
|
||||
registry: null,
|
||||
},
|
||||
requiredBeforePublishing: [],
|
||||
}),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => releaseReadinessReport(root),
|
||||
/npm-public-release-ready policy keeps python.publish false/,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects required published smoke mode without a package name', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-release-smoke-required-missing-config-test-'));
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue