mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-14 22:54:01 -06:00
d2afbd05ad
* 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>
208 lines
7.1 KiB
JavaScript
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;
|
|
}
|
|
}
|