openclaw/scripts/plugin-sdk-api-release-evidence.mjs
RoboClaw dd30346232
fix(release): keep complete SDK reports within artifact limits (#150273)
Preserve shared beta/latest comparisons in a versioned pooled representation.
Keep full facts, logical digests, historical receipt support and admission limits.

Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com>
2026-09-16 13:25:39 -07:00

336 lines
12 KiB
JavaScript

#!/usr/bin/env node
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
const PLUGIN_SDK_API_RELEASE_EVIDENCE_SCHEMA = "openclaw.plugin-sdk-api-release-evidence/v1";
const PLUGIN_SDK_API_RELEASE_EVIDENCE_SET_SCHEMA =
"openclaw.plugin-sdk-api-release-evidence-set/v1";
const PLUGIN_SDK_API_RELEASE_POOLED_EVIDENCE_SET_SCHEMA =
"openclaw.plugin-sdk-api-release-evidence-set/v2";
const PLUGIN_SDK_API_DIFF_SET_SCHEMA = "openclaw.plugin-sdk-api-diff-set/v1";
const SHA_PATTERN = /^[a-f0-9]{40}$/u;
const DIGEST_PATTERN = /^[a-f0-9]{64}$/u;
// Release workflows fetch this file directly from their trusted workflow SHA,
// so it must stay runnable without a workspace install.
function isReleaseEvidenceObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function assertSha(value, label) {
if (typeof value !== "string" || !SHA_PATTERN.test(value)) {
throw new Error(`${label} must be a full lowercase commit SHA`);
}
}
function diffPayload(diff) {
if (
!isReleaseEvidenceObject(diff) ||
!Array.isArray(diff.entrypointsAdded) ||
!Array.isArray(diff.entrypointsRemoved) ||
!Array.isArray(diff.exports) ||
typeof diff.digest !== "string" ||
!DIGEST_PATTERN.test(diff.digest)
) {
throw new Error("Plugin SDK API release evidence contains an invalid diff");
}
return {
entrypointsAdded: diff.entrypointsAdded,
entrypointsRemoved: diff.entrypointsRemoved,
exports: diff.exports,
};
}
function hasChanges(payload) {
return (
payload.entrypointsAdded.length > 0 ||
payload.entrypointsRemoved.length > 0 ||
payload.exports.length > 0
);
}
export function createPluginSdkApiReleaseEvidence({
baseRef,
baseSha,
diff,
headSha,
workflowSha,
}) {
assertSha(baseSha, "Plugin SDK API evidence base SHA");
assertSha(headSha, "Plugin SDK API evidence head SHA");
assertSha(workflowSha, "Plugin SDK API evidence workflow SHA");
if (typeof baseRef !== "string" || baseRef.length === 0) {
throw new Error("Plugin SDK API evidence base ref is required");
}
const payload = diffPayload(diff);
const digest = createHash("sha256").update(JSON.stringify(payload), "utf8").digest("hex");
if (diff.digest !== digest) {
throw new Error("Plugin SDK API diff digest does not match its payload");
}
return {
schema: PLUGIN_SDK_API_RELEASE_EVIDENCE_SCHEMA,
status: "checked",
baseRef,
baseSha,
headSha,
hasChanges: hasChanges(payload),
digest,
diff,
workflowSha,
};
}
function requireSelectors(selectors) {
if (
!isReleaseEvidenceObject(selectors) ||
Object.keys(selectors).length !== 2 ||
!Object.hasOwn(selectors, "beta") ||
!Object.hasOwn(selectors, "latest")
) {
throw new Error("Plugin SDK API selector evidence must bind beta and latest");
}
}
// The renderer already reuses comparisons by predecessor commit. Preserve that
// sharing on disk without changing the v1 logical diff or acknowledgement digest.
export function createPluginSdkApiDiffSet(selectors) {
requireSelectors(selectors);
/** @type {Record<string, ReturnType<typeof diffPayload> & { digest: string }>} */
const diffs = {};
/** @type {Record<string, string>} */
const references = {};
for (const [selector, diff] of Object.entries(selectors)) {
const payload = diffPayload(diff);
const digest = createHash("sha256").update(JSON.stringify(payload), "utf8").digest("hex");
if (diff.digest !== digest) {
throw new Error("Plugin SDK API diff digest does not match its payload");
}
if (Object.hasOwn(diffs, digest) && JSON.stringify(diffs[digest]) !== JSON.stringify(diff)) {
throw new Error("Plugin SDK API pooled diffs disagree for the same digest");
}
diffs[digest] = diff;
references[selector] = digest;
}
return { schema: PLUGIN_SDK_API_DIFF_SET_SCHEMA, selectors: references, diffs };
}
export function expandPluginSdkApiDiffSet(value) {
if (
!isReleaseEvidenceObject(value) ||
value.schema !== PLUGIN_SDK_API_DIFF_SET_SCHEMA ||
Object.keys(value).length !== 3 ||
!isReleaseEvidenceObject(value.diffs)
) {
throw new Error("Plugin SDK API pooled diff set is invalid");
}
requireSelectors(value.selectors);
const references = Object.values(value.selectors);
if (
references.some((ref) => typeof ref !== "string" || !DIGEST_PATTERN.test(ref)) ||
Object.keys(value.diffs).length !== new Set(references).size ||
references.some((ref) => !Object.hasOwn(value.diffs, ref) || value.diffs[ref]?.digest !== ref)
) {
throw new Error("Plugin SDK API pooled diff references are invalid");
}
const selectors = Object.fromEntries(
Object.entries(value.selectors).map(([selector, ref]) => [selector, value.diffs[ref]]),
);
// Hash the complete payload, including the unselected channel; a pool reference
// never replaces payload verification or the selected channel's acknowledgement.
createPluginSdkApiDiffSet(selectors);
return selectors;
}
export function selectPluginSdkApiReleaseEvidence({ evidence, npmDistTag }) {
const pooled = evidence?.schema === PLUGIN_SDK_API_RELEASE_POOLED_EVIDENCE_SET_SCHEMA;
if (!pooled && evidence?.schema !== PLUGIN_SDK_API_RELEASE_EVIDENCE_SET_SCHEMA) {
return evidence;
}
const selectors = evidence.selectors;
if (
Object.keys(evidence).length !== (pooled ? 3 : 2) ||
!isReleaseEvidenceObject(selectors) ||
Object.keys(selectors).length !== 2 ||
["beta", "latest"].some(
(selector) => selectors[selector]?.schema !== PLUGIN_SDK_API_RELEASE_EVIDENCE_SCHEMA,
) ||
selectors.beta.headSha !== selectors.latest.headSha ||
selectors.beta.workflowSha !== selectors.latest.workflowSha
) {
throw new Error(
"Plugin SDK API selector evidence must bind beta and latest to one release and tooling SHA",
);
}
if (npmDistTag !== "beta" && npmDistTag !== "latest") {
throw new Error("Plugin SDK API selector evidence requires the beta or latest npm dist-tag");
}
if (!pooled) {
return selectors[npmDistTag];
}
const diffs = expandPluginSdkApiDiffSet({
schema: PLUGIN_SDK_API_DIFF_SET_SCHEMA,
selectors: Object.fromEntries(
Object.entries(selectors).map(([selector, receipt]) => [selector, receipt.diff]),
),
diffs: evidence.diffs,
});
return { ...selectors[npmDistTag], diff: diffs[npmDistTag] };
}
export function createPluginSdkApiReleaseEvidenceSet(selectors) {
// Check the existing identity contract before compacting either channel.
selectPluginSdkApiReleaseEvidence({
evidence: { schema: PLUGIN_SDK_API_RELEASE_EVIDENCE_SET_SCHEMA, selectors },
npmDistTag: "beta",
});
const pool = createPluginSdkApiDiffSet(
Object.fromEntries(
Object.entries(selectors).map(([selector, receipt]) => [selector, receipt.diff]),
),
);
return {
schema: PLUGIN_SDK_API_RELEASE_POOLED_EVIDENCE_SET_SCHEMA,
selectors: Object.fromEntries(
Object.entries(selectors).map(([selector, receipt]) => [
selector,
{ ...receipt, diff: pool.selectors[selector] },
]),
),
diffs: pool.diffs,
};
}
export function validatePluginSdkApiReleaseEvidence({
acknowledgement,
currentSelectorRef = "",
currentSelectorSha = "",
evidence: inputEvidence,
expectedHeadSha,
expectedWorkflowSha,
npmDistTag = "",
targetRef = "",
}) {
const evidence = selectPluginSdkApiReleaseEvidence({ evidence: inputEvidence, npmDistTag });
assertSha(expectedHeadSha, "Expected Plugin SDK API evidence head SHA");
if (
!isReleaseEvidenceObject(evidence) ||
evidence.schema !== PLUGIN_SDK_API_RELEASE_EVIDENCE_SCHEMA
) {
throw new Error("Plugin SDK API release evidence is missing or invalid");
}
assertSha(evidence.workflowSha, "Plugin SDK API evidence workflow SHA");
assertSha(expectedWorkflowSha, "Expected Plugin SDK API evidence workflow SHA");
if (evidence.workflowSha !== expectedWorkflowSha) {
throw new Error("Plugin SDK API evidence workflow SHA does not match trusted tooling");
}
if (evidence.status !== "checked") {
throw new Error("Plugin SDK API release evidence has an invalid status");
}
assertSha(evidence.baseSha, "Plugin SDK API evidence base SHA");
assertSha(evidence.headSha, "Plugin SDK API evidence head SHA");
if (evidence.headSha !== expectedHeadSha) {
throw new Error("Plugin SDK API evidence head SHA does not match the release");
}
if (typeof evidence.baseRef !== "string" || evidence.baseRef.length === 0) {
throw new Error("Plugin SDK API evidence base ref is missing");
}
const selectorValues = [currentSelectorRef, currentSelectorSha, targetRef];
const selectorValueCount = selectorValues.filter((value) => value !== "").length;
if (selectorValueCount !== 0 && selectorValueCount !== selectorValues.length) {
throw new Error("Current Plugin SDK API selector validation is incomplete");
}
if (selectorValueCount > 0) {
if (
typeof currentSelectorRef !== "string" ||
currentSelectorRef.length === 0 ||
typeof targetRef !== "string" ||
targetRef.length === 0
) {
throw new Error("Current Plugin SDK API selector refs are invalid");
}
assertSha(currentSelectorSha, "Current Plugin SDK API selector SHA");
if (currentSelectorRef === targetRef && currentSelectorSha !== expectedHeadSha) {
throw new Error("Current npm dist-tag target does not match the release SHA");
}
if (
currentSelectorRef !== targetRef &&
(evidence.baseRef !== currentSelectorRef || evidence.baseSha !== currentSelectorSha)
) {
throw new Error("Plugin SDK API evidence predecessor no longer matches the npm dist-tag");
}
}
const payload = diffPayload(evidence.diff);
const digest = createHash("sha256").update(JSON.stringify(payload), "utf8").digest("hex");
if (evidence.digest !== digest || evidence.diff.digest !== digest) {
throw new Error("Plugin SDK API release evidence digest does not match its diff");
}
const changed = hasChanges(payload);
if (evidence.hasChanges !== changed) {
throw new Error("Plugin SDK API release evidence change state does not match its diff");
}
const expectedAcknowledgement = digest.slice(0, 8);
if (changed && acknowledgement !== expectedAcknowledgement) {
throw new Error(
`Plugin SDK API changes require acknowledgement digest ${expectedAcknowledgement}`,
);
}
return {
acknowledgement: changed ? expectedAcknowledgement : null,
digest,
hasChanges: changed,
status: "checked",
};
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function readArgs(argv) {
const values = new Map();
for (let index = 0; index < argv.length; index += 2) {
const flag = argv[index];
const value = argv[index + 1];
if (!flag?.startsWith("--") || value === undefined) {
throw new Error(
"Usage: plugin-sdk-api-release-evidence --manifest <path> --head <sha> --workflow-sha <sha> [--npm-dist-tag <tag>] [--acknowledge <digest>] [--current-selector-ref <tag> --current-selector-sha <sha> --target-ref <tag>]",
);
}
values.set(flag, value);
}
for (const flag of ["--manifest", "--head", "--workflow-sha"]) {
if (!values.has(flag)) {
throw new Error(`${flag} is required`);
}
}
return values;
}
function main() {
const args = readArgs(process.argv.slice(2));
const manifest = readJson(args.get("--manifest"));
const result = validatePluginSdkApiReleaseEvidence({
acknowledgement: args.get("--acknowledge") ?? "",
currentSelectorRef: args.get("--current-selector-ref"),
currentSelectorSha: args.get("--current-selector-sha"),
evidence: manifest.pluginSdkApi,
expectedHeadSha: args.get("--head"),
expectedWorkflowSha: args.get("--workflow-sha"),
npmDistTag: args.get("--npm-dist-tag"),
targetRef: args.get("--target-ref"),
});
process.stdout.write(`${JSON.stringify(result)}\n`);
}
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}