mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(release): verify beta floor for core npm packages (#161968)
Share filesystem core package discovery with npm bundle preparation. Include every selected core package in beta-floor diagnostics and block postpublish on any core floor failure, while preserving reused and superseded core selectors.
This commit is contained in:
parent
a3e3b0b9df
commit
dc49c824d3
6 changed files with 163 additions and 28 deletions
32
scripts/lib/npm-core-release-packages.mjs
Normal file
32
scripts/lib/npm-core-release-packages.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { readBoundedRegularFile } from "./actions-artifact-archive.mjs";
|
||||
import CORE_PACKAGE_POLICY from "./npm-core-release-packages.json" with { type: "json" };
|
||||
|
||||
export { CORE_PACKAGE_POLICY };
|
||||
|
||||
function readManifest(directory) {
|
||||
const path = join(directory, "package.json");
|
||||
return JSON.parse(readBoundedRegularFile(path, { label: path, maxBytes: 1024 * 1024 }));
|
||||
}
|
||||
|
||||
export function collectPublishableCorePackages(rootDir, root = readManifest(rootDir)) {
|
||||
return CORE_PACKAGE_POLICY.filter((policy) => {
|
||||
const directory = join(rootDir, policy.path);
|
||||
if (
|
||||
policy.dependency
|
||||
? typeof root.dependencies?.[policy.dependency] !== "string"
|
||||
: !existsSync(join(directory, "package.json"))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const manifest = readManifest(directory);
|
||||
if (!policy.dependency && manifest.openclaw?.release?.publishToNpm !== true) {
|
||||
return false;
|
||||
}
|
||||
if (manifest.name !== policy.name || manifest.version !== root.version) {
|
||||
throw new Error(`${policy.path}/package.json must publish ${policy.name}@${root.version}.`);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import { isRecord as isJsonRecord } from "../../packages/normalization-core/src/
|
|||
import { normalizeOptionalString } from "../../packages/normalization-core/src/string-coerce.ts";
|
||||
import { readPublicationArtifactArchive, sha256Digest } from "./actions-artifact-archive.mjs";
|
||||
import { readBoundedResponseText } from "./bounded-response.mjs";
|
||||
import { collectPublishableCorePackages } from "./npm-core-release-packages.mjs";
|
||||
import { resolveNpmJsonEntries } from "./npm-json-output.mts";
|
||||
import { npmRegistryReadbackDeadline } from "./npm-publish-plan.mjs";
|
||||
import { collectClawHubPublishablePluginPackages } from "./plugin-clawhub-release.ts";
|
||||
|
|
@ -1952,16 +1953,23 @@ export async function verifyBetaRelease(
|
|||
diagnostic.start("coreNpm");
|
||||
const openclawNpm = await verifyNpmPackage("openclaw", args.version, args.distTag);
|
||||
diagnostic.observeNpmPublication({ stage: "coreNpm" });
|
||||
const coreBetaFloorError = await readNpmBetaFloorError("openclaw", args.version);
|
||||
if (coreBetaFloorError !== undefined) {
|
||||
betaFloorErrors.push({ scope: { stage: "coreNpm" }, message: coreBetaFloorError });
|
||||
diagnostic.fail(createNpmBetaFloorError([coreBetaFloorError]));
|
||||
} else {
|
||||
const corePackages = collectPublishableCorePackages(rootDir);
|
||||
// Core versions can be reused without retagging; only enforce their beta floor.
|
||||
for (const name of ["openclaw", ...corePackages.map((pkg) => pkg.name)]) {
|
||||
const error = await readNpmBetaFloorError(name, args.version);
|
||||
if (error !== undefined) {
|
||||
betaFloorErrors.push({ scope: { stage: "coreNpm" }, message: error });
|
||||
diagnostic.fail(createNpmBetaFloorError([error]));
|
||||
}
|
||||
}
|
||||
const coreBetaFloorFailed = betaFloorErrors.length > 0;
|
||||
if (!coreBetaFloorFailed) {
|
||||
diagnostic.success("coreNpm");
|
||||
lines.push(`openclaw npm OK: ${args.version} (${args.distTag})`);
|
||||
lines.push(`core npm beta floors OK: ${corePackages.length}`);
|
||||
}
|
||||
|
||||
if (!args.skipPostpublish && coreBetaFloorError === undefined) {
|
||||
if (!args.skipPostpublish && !coreBetaFloorFailed) {
|
||||
diagnostic.start("postpublish");
|
||||
const postpublishVerifier = resolveOpenClawNpmPostpublishVerifier(
|
||||
rootDir,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ import {
|
|||
inspectActionsArtifactZipWithPolicy,
|
||||
readBoundedRegularFile,
|
||||
} from "./lib/actions-artifact-archive.mjs";
|
||||
import {
|
||||
collectPublishableCorePackages,
|
||||
CORE_PACKAGE_POLICY,
|
||||
} from "./lib/npm-core-release-packages.mjs";
|
||||
import { assertNpmShrinkwrapDependencies } from "./lib/npm-shrinkwrap-dependencies.mjs";
|
||||
import { isRecord } from "./lib/record-shared.mjs";
|
||||
import { resolveReleaseTagPackageIdentity } from "./lib/release-version.mjs";
|
||||
|
|
@ -47,9 +51,6 @@ const CALLER_WORKFLOWS = new Set([
|
|||
".github/workflows/full-release-candidate.yml",
|
||||
".github/workflows/full-release-artifacts.yml",
|
||||
]);
|
||||
const CORE_PACKAGE_POLICY = JSON.parse(
|
||||
readFileSync(new URL("./lib/npm-core-release-packages.json", import.meta.url), "utf8"),
|
||||
);
|
||||
const CORE_PACKAGES = CORE_PACKAGE_POLICY.map((entry) => entry.name);
|
||||
const MAX_TARBALL_BYTES = 192 * 1024 * 1024;
|
||||
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
||||
|
|
@ -859,24 +860,9 @@ export function prepareNpmPackageBundle({
|
|||
),
|
||||
};
|
||||
};
|
||||
const corePackageTarballs = CORE_PACKAGE_POLICY.flatMap((policy) => {
|
||||
const packageName = policy.name;
|
||||
const directory = join(sourceDir, policy.path);
|
||||
if (policy.dependency) {
|
||||
if (typeof root.dependencies?.[policy.dependency] !== "string") {
|
||||
return [];
|
||||
}
|
||||
} else if (
|
||||
!existsSync(join(directory, "package.json")) ||
|
||||
readJson(join(directory, "package.json")).openclaw?.release?.publishToNpm !== true
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
if (readJson(join(directory, "package.json")).version !== root.version) {
|
||||
throw new Error(`Core package version mismatch: ${packageName}.`);
|
||||
}
|
||||
return [pack(directory, packageName)];
|
||||
});
|
||||
const corePackageTarballs = collectPublishableCorePackages(sourceDir, root).map((policy) =>
|
||||
pack(join(sourceDir, policy.path), policy.name),
|
||||
);
|
||||
const aiPackage = corePackageTarballs.find(({ packageName }) => packageName === "@openclaw/ai");
|
||||
const hasRootShrinkwrap = existsSync(join(sourceDir, "npm-shrinkwrap.json"));
|
||||
if (aiPackage && hasRootShrinkwrap) {
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ const toolingPaths = [
|
|||
"scripts/lib/release-publish-inputs.mjs",
|
||||
"scripts/npm-preflight-tooling-identity.mjs",
|
||||
"scripts/npm-prepared-bundle.mjs",
|
||||
"scripts/lib/npm-core-release-packages.mjs",
|
||||
"scripts/plugin-sdk-api-release-evidence.mjs",
|
||||
"scripts/lib/plain-gh.mjs",
|
||||
"scripts/lib/release-context.mjs",
|
||||
|
|
|
|||
|
|
@ -4341,6 +4341,7 @@ function writePreflightConsumerTooling(toolingDir: string) {
|
|||
"openclaw-npm-extended-stable-release.mjs",
|
||||
"release-tooling-identity.mjs",
|
||||
"lib/actions-artifact-archive.mjs",
|
||||
"lib/npm-core-release-packages.mjs",
|
||||
"lib/npm-core-release-packages.json",
|
||||
"lib/npm-shrinkwrap-dependencies.mjs",
|
||||
"lib/record-shared.mjs",
|
||||
|
|
|
|||
|
|
@ -189,6 +189,9 @@ describe("verifyBetaRelease workflow outcomes", () => {
|
|||
tags: Record<string, Record<string, string>>;
|
||||
transientlyMissing?: string;
|
||||
npm12?: boolean;
|
||||
corePackages?: string[];
|
||||
dependencies?: Record<string, string>;
|
||||
emptyDistTags?: string;
|
||||
} = {
|
||||
version,
|
||||
distTag: "beta",
|
||||
|
|
@ -199,9 +202,27 @@ describe("verifyBetaRelease workflow outcomes", () => {
|
|||
const binDir = join(rootDir, "bin");
|
||||
mkdirSync(binDir);
|
||||
mkdirSync(join(rootDir, "extensions"));
|
||||
writeFileSync(join(rootDir, "package.json"), JSON.stringify({ version: npm.version }));
|
||||
writeFileSync(
|
||||
join(rootDir, "package.json"),
|
||||
JSON.stringify({ version: npm.version, dependencies: npm.dependencies }),
|
||||
);
|
||||
writeFileSync(join(binDir, "npm.json"), JSON.stringify(npm));
|
||||
for (const name of npm.corePackages ?? []) {
|
||||
const directory = join(rootDir, "packages", name.slice("@openclaw/".length));
|
||||
mkdirSync(directory, { recursive: true });
|
||||
writeFileSync(
|
||||
join(directory, "package.json"),
|
||||
JSON.stringify({
|
||||
name,
|
||||
version: npm.version,
|
||||
openclaw: { release: { publishToNpm: true } },
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (const name of Object.keys(npm.tags).filter((packageName) => packageName !== "openclaw")) {
|
||||
if (npm.corePackages?.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
writePublishablePluginFixture(rootDir, {
|
||||
extensionId: name.slice("@openclaw/".length),
|
||||
packageName: name,
|
||||
|
|
@ -242,6 +263,7 @@ if (path.basename(process.argv[1]) === "npm" && args[0] === "view") {
|
|||
const name = Object.keys(npm.tags).find((name) => args[1] === name || args[1] === name + "@" + npm.version);
|
||||
if (!name) throw new Error("Unexpected npm package: " + args[1]);
|
||||
if (args[2] === "dist-tags") {
|
||||
if (npm.emptyDistTags === name) process.exit(0);
|
||||
const visible = path.join(path.dirname(process.argv[1]), "npm-visible");
|
||||
if (npm.transientlyMissing === name && !fs.existsSync(visible)) {
|
||||
fs.writeFileSync(visible, "ready");
|
||||
|
|
@ -709,8 +731,10 @@ syncBuiltinESMExports();
|
|||
const fixture = workflowFixture({}, true, undefined, {
|
||||
version: latest,
|
||||
distTag: "latest",
|
||||
corePackages: ["@openclaw/gateway-client"],
|
||||
tags: {
|
||||
openclaw: { latest, beta: "2026.9.1" },
|
||||
"@openclaw/gateway-client": { latest, beta: "2026.9.1" },
|
||||
"@openclaw/demo": { latest, beta: "2026.9.3-beta.1" },
|
||||
"@openclaw/other": { latest },
|
||||
},
|
||||
|
|
@ -719,12 +743,95 @@ syncBuiltinESMExports();
|
|||
const verification = verifyBetaRelease(fixture.args, { rootDir: fixture.rootDir });
|
||||
await expect(verification).rejects.toThrow(
|
||||
"openclaw: beta=2026.9.1, latest=2026.9.3\n" +
|
||||
"@openclaw/gateway-client: beta=2026.9.1, latest=2026.9.3\n" +
|
||||
"@openclaw/demo: beta=2026.9.3-beta.1, latest=2026.9.3\n" +
|
||||
"@openclaw/other: beta=<missing>, latest=2026.9.3",
|
||||
);
|
||||
await expect(verification).rejects.toThrow("npm dist-tag add <pkg>@<latest> beta");
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"rejects a core package with invalid beta readback (empty: %s)",
|
||||
async (empty) => {
|
||||
const latest = "2026.9.7";
|
||||
const name = "@openclaw/gateway-client";
|
||||
const fixture = workflowFixture({}, true, undefined, {
|
||||
version: latest,
|
||||
distTag: "latest",
|
||||
corePackages: [name],
|
||||
emptyDistTags: empty ? name : undefined,
|
||||
tags: {
|
||||
openclaw: { latest, beta: latest },
|
||||
[name]: { latest, beta: "2026.9.3" },
|
||||
},
|
||||
});
|
||||
fixture.args.skipPostpublish = false;
|
||||
|
||||
await expect(verifyBetaRelease(fixture.args, { rootDir: fixture.rootDir })).rejects.toThrow(
|
||||
empty
|
||||
? `npm view ${name}@${latest} dist-tags returned invalid JSON`
|
||||
: `${name}: beta=2026.9.3, latest=${latest}`,
|
||||
);
|
||||
expect(
|
||||
JSON.parse(
|
||||
readFileSync(join(fixture.rootDir, "release-postpublish-diagnostics.json"), "utf8"),
|
||||
),
|
||||
).toMatchObject({
|
||||
stages: { coreNpm: { state: "failure" }, postpublish: { state: "unattempted" } },
|
||||
});
|
||||
expect(existsSync(join(fixture.rootDir, "evidence.json"))).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ version: "2026.9.3", distTag: "beta", dependsOnAi: true },
|
||||
{ version: "2026.8.33", distTag: "extended-stable", dependsOnAi: false },
|
||||
])(
|
||||
"verifies core beta floors with superseded or absent $distTag selectors",
|
||||
async ({ version: releaseVersion, distTag, dependsOnAi }) => {
|
||||
const corePackages = [
|
||||
"@openclaw/ai",
|
||||
"@openclaw/gateway-protocol",
|
||||
"@openclaw/gateway-client",
|
||||
];
|
||||
const fixture = workflowFixture({}, true, undefined, {
|
||||
version: releaseVersion,
|
||||
distTag,
|
||||
corePackages,
|
||||
dependencies: dependsOnAi ? { "@openclaw/ai": releaseVersion } : {},
|
||||
tags: {
|
||||
openclaw: { [distTag]: releaseVersion },
|
||||
"@openclaw/ai": { latest: "2026.9.7", beta: dependsOnAi ? "2026.9.7" : "2026.9.3" },
|
||||
"@openclaw/gateway-protocol": { latest: "2026.9.7", beta: "2026.9.8-beta.1" },
|
||||
"@openclaw/gateway-client": { beta: "2026.9.8-beta.1" },
|
||||
},
|
||||
});
|
||||
|
||||
const lines = await verifyBetaRelease(fixture.args, { rootDir: fixture.rootDir });
|
||||
expect(lines).toContain(`core npm beta floors OK: ${dependsOnAi ? 3 : 2}`);
|
||||
const commands: string[][] = readFileSync(join(fixture.binDir, "commands.jsonl"), "utf8")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
expect(
|
||||
commands.filter(
|
||||
([command, , selector]) => command === "npm" && selector?.startsWith("@openclaw/"),
|
||||
),
|
||||
).toEqual(
|
||||
corePackages
|
||||
.filter((name) => dependsOnAi || name !== "@openclaw/ai")
|
||||
.map((name) => [
|
||||
"npm",
|
||||
"view",
|
||||
`${name}@${releaseVersion}`,
|
||||
"dist-tags",
|
||||
"--json",
|
||||
"--prefer-online",
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
"queries a beta-only plugin without latest (npm 12 and initial E404: %s)",
|
||||
async (transientlyMissing) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue