mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-22 15:06:57 +00:00
* feat(release): qualify frozen candidates with their own harness Keep candidate qualification code, required coverage, and baselines frozen while independently trusted tooling authenticates admission and publication. Preserve historical recovery and verify exact qualified npm and Docker artifacts without candidate-to-publisher ancestry. Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> * fix(release): restore authenticated harnesses before execution Bootstrap only from the immutable caller commit, then restore and verify the authenticated callee revision before any repository code runs. Preserve canonical origin and path guards, with real Git regression coverage. Remove duplicate coercion declarations without importing executable normalization code before bootstrap admission. Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> * test(release): align qualification fixtures with frozen harnesses Provide explicit ordinary-lane workflow inputs and the complete committed bootstrap closure. Assert immutable caller seeds rather than a moving main checkout, preserving authenticated harness restoration. Keep the internal evidence script constant private. Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> * fix(ci): register frozen qualification dynamic entrypoints Model the verified Node child runner and generated admission-transport fixture as exact Knip entrypoints. Keep the fixture out of production roots and preserve the hard-zero unused-file and export checks without ignores or fake imports. Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> * test(release): include captured baselines in plugin fixtures Model the independent phase qualification-baseline step with empty outputs and assert both trusted baseline output bindings. Preserve the substituted candidate SHA and the scanner checks against the separately admitted source identity. Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> * fix(release): remove circular manifest policy dependency Remove the retired empty-advisory helper and emit the unchanged empty wire fields at their producers. Keep strict advisory rejection and the single manifest builder. Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> * test(release): cover frozen catalog checkout closure Preserve exact sparse-path, byte-integrity, and candidate-isolation assertions for both catalog dependencies. Keep them excluded from preflight materialization. Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> * refactor(release): remove unused evidence facade export Keep manifest child evidence owned by the extracted manifest module; policy and state consumers use that owner directly. Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> * fix(release): revalidate admission before publication effects Carry authenticated admission authority through Docker and npm publication, rechecking it after preparation and immediately before each external registry mutation. Preserve original evidence and publisher guards; deny revoked authority in recovered and multi-package publication. Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> * test: align release qualification coverage with main * test: format release checklist coverage * test: restore release fixture closure * test: run compact admission from frozen tooling * test: cover oldest supported upgrade baseline * test: reconcile package acceptance coverage * test(release): align candidate-owned dispatch docs --------- Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com> Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com> Co-authored-by: Dallin Romney <dallinromney@gmail.com>
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { resolve } from "node:path";
|
|
import {
|
|
verifyQualificationAdmission,
|
|
type QualificationInputs,
|
|
} from "../release-qualification-admission.mjs";
|
|
import {
|
|
resolveReleaseToolingIdentity,
|
|
verifyReleaseToolingIdentity,
|
|
} from "../release-tooling-identity.mjs";
|
|
|
|
export type ReleaseInventorySource = {
|
|
repoRoot?: string;
|
|
candidateSha: string;
|
|
toolingSha: string;
|
|
toolingFullRef: string;
|
|
qualificationAdmission?: unknown;
|
|
qualificationInputs?: QualificationInputs;
|
|
runGh?: (args: string[]) => string;
|
|
downloadArchive?: (args: string[]) => Uint8Array;
|
|
};
|
|
const REPOSITORY = "openclaw/openclaw";
|
|
const git = (repoRoot: string, args: string[]) =>
|
|
execFileSync("git", args, {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
}).trim();
|
|
|
|
export function resolveCommit(repoRoot: string, revision: string, label: string): string {
|
|
let resolved: string;
|
|
try {
|
|
resolved = git(repoRoot, ["rev-parse", "--verify", `${revision}^{commit}`]);
|
|
} catch {
|
|
throw new Error(`${label} does not resolve to a commit: ${revision}`);
|
|
}
|
|
if (!/^[a-f0-9]{40}$/u.test(resolved)) {
|
|
throw new Error(`${label} did not resolve to an exact lowercase commit SHA`);
|
|
}
|
|
return resolved;
|
|
}
|
|
function requireExactSha(value: string, label: string): string {
|
|
if (!/^[a-f0-9]{40}$/u.test(value)) {
|
|
throw new Error(`${label} must be an exact lowercase 40-character commit SHA`);
|
|
}
|
|
return value;
|
|
}
|
|
function requireQualifiedRef(value: string, label: string): string {
|
|
if (!/^refs\/(?:heads|tags)\/[A-Za-z0-9._/-]+$/u.test(value)) {
|
|
throw new Error(`${label} must be a qualified branch or tag ref`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function resolveSource(params: ReleaseInventorySource, inventoryOnly = false) {
|
|
const repoRoot = resolve(params.repoRoot ?? ".");
|
|
const candidateSha = requireExactSha(params.candidateSha, "candidate SHA");
|
|
const toolingSha = requireExactSha(params.toolingSha, "tooling SHA");
|
|
const toolingFullRef = requireQualifiedRef(params.toolingFullRef, "tooling full ref");
|
|
if (resolveCommit(repoRoot, candidateSha, "candidate SHA") !== candidateSha) {
|
|
throw new Error("candidate SHA does not resolve to itself");
|
|
}
|
|
const toolingRef = toolingFullRef.replace(/^refs\/(?:heads|tags)\//u, "");
|
|
if (inventoryOnly) {
|
|
resolveReleaseToolingIdentity({
|
|
qualificationAdmission: params.qualificationAdmission,
|
|
candidateSha,
|
|
workflowContract: "2",
|
|
requestedIdentityJson: JSON.stringify({
|
|
ref: toolingRef,
|
|
fullRef: toolingFullRef,
|
|
sha: toolingSha,
|
|
}),
|
|
workflowFullRef: toolingFullRef,
|
|
workflowRef: toolingRef,
|
|
workflowSha: toolingSha,
|
|
});
|
|
}
|
|
if (params.qualificationAdmission !== undefined) {
|
|
if (!inventoryOnly || params.qualificationInputs === undefined) {
|
|
throw new Error("Candidate inventory requires its complete qualification inputs");
|
|
}
|
|
verifyQualificationAdmission({
|
|
descriptor: params.qualificationAdmission,
|
|
repository: REPOSITORY,
|
|
candidateSha,
|
|
qualificationSha: toolingSha,
|
|
workflowRef: toolingRef,
|
|
inputs: params.qualificationInputs,
|
|
runGh: params.runGh,
|
|
downloadArchive: params.downloadArchive,
|
|
});
|
|
return {
|
|
candidateSha,
|
|
repoRoot,
|
|
toolingFullRef,
|
|
toolingSha,
|
|
verifiedTooling: {
|
|
ref: toolingRef,
|
|
fullRef: toolingFullRef,
|
|
sha: toolingSha,
|
|
route: "candidate" as const,
|
|
},
|
|
};
|
|
}
|
|
const verifiedTooling = verifyReleaseToolingIdentity({
|
|
allowPrevalidatedRef: inventoryOnly,
|
|
repository: REPOSITORY,
|
|
workflowFullRef: toolingFullRef,
|
|
workflowRef: toolingRef,
|
|
workflowSha: toolingSha,
|
|
...(params.runGh ? { runGh: params.runGh } : {}),
|
|
});
|
|
return { candidateSha, repoRoot, toolingFullRef, toolingSha, verifiedTooling };
|
|
}
|