mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(release): verify legacy published chunk ownership (#163019)
This commit is contained in:
parent
48f475d653
commit
a01081aac3
5 changed files with 98 additions and 34 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue