Files
openclaw/scripts/plugin-sdk-api-release-evidence.mjs
T
Ayaan Zaidi d2afbd05ad refactor(plugin-sdk): replace API baselines with diffs (#123036)
* refactor(plugin-sdk): replace API baselines with diffs

* perf(plugin-sdk): bound API diff resources

* fix(plugin-sdk): isolate API diff dependencies

* fix(release): forward Plugin SDK acknowledgement

* fix(release): enforce SDK acknowledgement on publish

* chore: preserve generated-doc ignore policy

* fix(release): freeze SDK API evidence before publish

* fix(ci): satisfy SDK evidence guards

* fix(release): bind complete SDK evidence

* fix(release): authenticate plugin SDK evidence

* fix(plugin-sdk): abort interrupted API diffs

* test(ui): freeze page clock in background-tasks rail e2e

The rail transcript is compared byte-for-byte across the detail-panel
round-trip while it renders live relative ages; on slow CI runners the
second boundary ticks between the two reads (11s -> 12s) and fails the
equality assertion. Fix the page Date with Playwright setFixedTime while
keeping timers running so the tasks.list polling assertions still hold.

Repro: a 1.5s stall between the reads fails pre-fix with the exact CI
diff and passes post-fix.

* fix(scripts): drop unused export on dependency-evidence CLI main

Knip's workflow scan re-roots script references after an actions/checkout
step that sets path:, so the new trusted-tooling checkout in
openclaw-npm-release.yml stops marking this CLI as a workflow entry and
its exported main() surfaces as an unused export in check-dependencies.
Nothing imports main; the module invokes it through its own entry guard,
so the export keyword was dead surface either way.

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-13 03:45:36 -07:00

208 lines
7.1 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 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,
};
}
export function validatePluginSdkApiReleaseEvidence({
acknowledgement,
currentSelectorRef = "",
currentSelectorSha = "",
evidence,
expectedHeadSha,
expectedWorkflowSha,
targetRef = "",
}) {
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> [--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"),
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;
}
}