fix(release): verify legacy published chunk ownership (#163019)

This commit is contained in:
Dallin Romney 2026-10-01 15:29:41 -07:00 • committed by GitHub
parent 48f475d653
commit a01081aac3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 98 additions and 34 deletions

View file

@ -69,9 +69,31 @@ on pinned current `main` for command and validation requirements.
Docker, and finalization. Docker-only recovery may dispatch from `main` with
`publish_openclaw_npm=false` and `publish_docker_only=true`; that path does
not attach evidence or finalize the release.
8. From a clean current-`main` checkout, run
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts YYYY.M.P`.
Verify package signatures, source commits, inventories, exact versions, and selectors.
8. Keep separate clean checkouts for trusted current-main tooling and the exact
frozen release source. Install the trusted tooling checkout with
`pnpm install --frozen-lockfile`, then run its verifier with the frozen
release checkout as the working directory. Use a disposable release
worktree with no existing `node_modules`, then expose only the trusted
tooling install through the same link used by the publication workflow:
```bash
VERSION=YYYY.M.P
TOOLING_ROOT=/absolute/path/to/clean-current-main
RELEASE_ROOT=/absolute/path/to/frozen-release-source
test "$(node -e 'console.log(require(process.argv[1]).version)' "$RELEASE_ROOT/package.json")" = "$VERSION"
test ! -e "$RELEASE_ROOT/node_modules"
ln -s "$TOOLING_ROOT/node_modules" "$RELEASE_ROOT/node_modules"
(
cd "$RELEASE_ROOT"
node --import tsx "$TOOLING_ROOT/scripts/openclaw-npm-postpublish-verify.ts" "$VERSION"
)
```
The script, external dependencies, and TypeScript loader come from trusted
current main. The frozen `cwd` supplies its immutable source manifests and
workspace path aliases, matching the release workflow's `.release-harness`
layout. A different source version must fail closed. Verify package
signatures, source commits, inventories, exact versions, and selectors.
To promote an already-published core version to `extended-stable`, use
`promote_extended_stable` in the `openclaw/releases` dist-tag workflow
from that repository's `main`. Follow
@ -82,6 +104,7 @@ on pinned current `main` for command and validation requirements.
patch range. The same action can select an older extended-stable version
for rollback. Repair other selectors separately with
approved credential-isolated tooling. Never republish a version.
9. Require `Docker Release` to verify default, slim, browser, and architecture
images in GHCR and Docker Hub, including attestations and platform versions.
It must advance only

View file

@ -196,6 +196,28 @@ export function buildPublishedInstallScenarios(version: string): PublishedInstal
return scenarios;
}
export function resolvePublishedInstallSourceVerification(
sourceRoot: string,
expectedVersion: string,
): Pick<
Parameters<typeof collectInstalledPackageErrors>[0],
"additionalCompanionManifestRoots" | "allowLegacyGeneratedOwnership"
> {
const packageJsonPath = join(sourceRoot, "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as InstalledPackageJson;
if (packageJson.name !== "openclaw" || packageJson.version !== expectedVersion) {
throw new Error(
`source checkout version mismatch: expected openclaw@${expectedVersion}, found ${packageJson.name ?? "<missing>"}@${packageJson.version ?? "<missing>"}.`,
);
}
return {
additionalCompanionManifestRoots: [join(sourceRoot, "extensions")],
allowLegacyGeneratedOwnership: !existsSync(
join(sourceRoot, "scripts/lib/runtime-dependency-ownership-build-plugin.mts"),
),
};
}
type NpmRegistryKey = {
key: string;
keyid: string;
@ -1267,7 +1289,11 @@ async function verifyPublishedRegistryProvenanceOnce(version: string): Promise<v
);
}
function verifyScenario(version: string, scenario: PublishedInstallScenario): void {
function verifyScenario(
version: string,
scenario: PublishedInstallScenario,
sourceVerification: ReturnType<typeof resolvePublishedInstallSourceVerification>,
): void {
const workingDir = mkdtempSync(join(tmpdir(), `openclaw-postpublish-${scenario.name}.`));
const prefixDir = join(workingDir, "prefix");
@ -1282,6 +1308,7 @@ function verifyScenario(version: string, scenario: PublishedInstallScenario): vo
readFileSync(join(packageRoot, "package.json"), "utf8"),
) as InstalledPackageJson;
const errors = collectInstalledPackageErrors({
...sourceVerification,
expectedVersion: scenario.expectedVersion,
installedVersion: pkg.version?.trim() ?? "",
packageRoot,
@ -1320,9 +1347,10 @@ async function main(argv = process.argv.slice(2)): Promise<void> {
const { version } = args;
const scenarios = buildPublishedInstallScenarios(version);
const sourceVerification = resolvePublishedInstallSourceVerification(process.cwd(), version);
await retryNpmRegistryProvenanceRead(() => verifyPublishedRegistryProvenanceOnce(version));
for (const scenario of scenarios) {
verifyScenario(version, scenario);
verifyScenario(version, scenario, sourceVerification);
}
console.log(

View file

@ -39,6 +39,7 @@ import { resolveNpmRunner, type NpmRunnerParams } from "./npm-runner.mts";
import {
collectInstalledPackageErrors,
normalizeInstalledBinaryVersion,
resolvePublishedInstallSourceVerification,
} from "./openclaw-npm-postpublish-verify.ts";
import { assertPreparedOpenClawAiDependency } from "./openclaw-npm-prepublish-verify.ts";
import { parseNpmPackJsonOutput, type NpmPackResult } from "./openclaw-npm-release-check.ts";
@ -671,12 +672,6 @@ export function collectPackedInstalledPackageVerificationErrors(params: {
return errors;
}
export function allowsLegacyGeneratedOwnershipForSourceRoot(sourceRoot: string): boolean {
return !existsSync(
resolve(sourceRoot, "scripts/lib/runtime-dependency-ownership-build-plugin.mts"),
);
}
function verifyPackedInstalledPackage(params: {
expectedVersion: string;
packageRoot: string;
@ -690,10 +685,7 @@ function verifyPackedInstalledPackage(params: {
stdio: ["ignore", "pipe", "pipe"],
}).trim();
const errors = collectPackedInstalledPackageVerificationErrors({
// The selected source checkout is immutable release input. Its companion
// manifests are the exact inputs packed by the following plugin preflight.
additionalCompanionManifestRoots: [resolve("extensions")],
allowLegacyGeneratedOwnership: allowsLegacyGeneratedOwnershipForSourceRoot(resolve()),
...resolvePublishedInstallSourceVerification(resolve(), params.expectedVersion),
expectedVersion: params.expectedVersion,
installedBinaryVersion,
packageRoot: params.packageRoot,

View file

@ -22,6 +22,7 @@ import {
collectInstalledPackageErrors,
fetchRegistryJson,
parseOpenClawNpmPostpublishVerifyArgs,
resolvePublishedInstallSourceVerification,
resolveInstalledBinaryCommandInvocation,
retryNpmRegistryProvenanceRead,
verifyNpmProvenanceAttestation,
@ -97,6 +98,44 @@ describe("buildPublishedInstallScenarios", () => {
});
});
describe("resolvePublishedInstallSourceVerification", () => {
it("uses exact legacy source manifests for published chunk ownership", () => {
const sourceRoot = createTempDir("openclaw-postpublish-source-");
writePackageFile(sourceRoot, "package.json", {
name: "openclaw",
version: "2026.8.34",
});
writePackageFile(sourceRoot, "extensions/discord/package.json", {
name: "@openclaw/discord",
version: "2026.8.34",
dependencies: { "@discordjs/voice": "0.19.2" },
});
expect(resolvePublishedInstallSourceVerification(sourceRoot, "2026.8.34")).toEqual({
additionalCompanionManifestRoots: [join(sourceRoot, "extensions")],
allowLegacyGeneratedOwnership: true,
});
writeInstalledFile(sourceRoot, "scripts/lib/runtime-dependency-ownership-build-plugin.mts");
expect(resolvePublishedInstallSourceVerification(sourceRoot, "2026.8.34")).toEqual({
additionalCompanionManifestRoots: [join(sourceRoot, "extensions")],
allowLegacyGeneratedOwnership: false,
});
});
it("rejects source manifests from another release", () => {
const sourceRoot = createTempDir("openclaw-postpublish-source-");
writePackageFile(sourceRoot, "package.json", {
name: "openclaw",
version: "2026.9.7",
});
expect(() => resolvePublishedInstallSourceVerification(sourceRoot, "2026.8.34")).toThrow(
"source checkout version mismatch",
);
});
});
describe("npm registry provenance verification", () => {
const packageName = "openclaw";
const version = "2026.3.23";

View file

@ -3,7 +3,7 @@ import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync
import { tmpdir } from "node:os";
import { dirname, join, resolve as resolvePath, win32 } from "node:path";
import { bundledDistPluginFile } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it } from "vitest";
import { describe, expect, it } from "vitest";
import { collectBundledExtensionManifestErrors } from "../scripts/lib/bundled-extension-manifest.ts";
import { listBundledPluginPackArtifacts } from "../scripts/lib/bundled-plugin-build-entries.mjs";
import { resolveNpmJsonEntries } from "../scripts/lib/npm-json-output.mts";
@ -12,7 +12,6 @@ import { PACKAGE_DIST_INVENTORY_RELATIVE_PATH } from "../scripts/lib/package-dis
import { createWorkspaceBootstrapSmokeEnv } from "../scripts/lib/workspace-bootstrap-smoke.mts";
import { collectInstalledBundledRuntimeSidecarPaths } from "../scripts/openclaw-npm-postpublish-verify.ts";
import {
allowsLegacyGeneratedOwnershipForSourceRoot,
collectAppcastSparkleVersionErrors,
collectCriticalPluginSdkEntrypointSizeFindings,
collectForbiddenPackContentPaths,
@ -33,9 +32,6 @@ import {
import { COMPLETION_SKIP_PLUGIN_COMMANDS_ENV } from "../src/cli/completion-runtime.ts";
import { resolveNpmJsonEntries as resolveRuntimeNpmJsonEntries } from "../src/infra/npm-registry-spec.js";
import { withEnv } from "../src/test-utils/env.js";
import { useAutoCleanupTempDirTracker } from "./helpers/temp-dir.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
function makeItem(shortVersion: string, sparkleVersion: string, channel?: string): string {
const channelElement = channel ? `<sparkle:channel>${channel}</sparkle:channel>` : "";
@ -522,20 +518,6 @@ describe("collectForbiddenPackPaths", () => {
});
describe("packed install verification", () => {
it("disables legacy ownership when the historical metadata producer exists", () => {
const sourceRoot = tempDirs.make("release-check-ownership-producer-");
expect(allowsLegacyGeneratedOwnershipForSourceRoot(sourceRoot)).toBe(true);
const producerPath = join(
sourceRoot,
"scripts/lib/runtime-dependency-ownership-build-plugin.mts",
);
mkdirSync(dirname(producerPath), { recursive: true });
writeFileSync(producerPath, "export {};\n", "utf8");
expect(allowsLegacyGeneratedOwnershipForSourceRoot(sourceRoot)).toBe(false);
});
it("runs postpublish package integrity checks against the packed install before publish", () => {
const root = mkdtempSync(join(tmpdir(), "release-check-packed-install-"));
try {