mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(release): validate npm preflight workflow trust
This commit is contained in:
@@ -162,6 +162,19 @@ export function requireRunIdFromDispatchOutput(output: unknown, workflowFile: un
|
||||
export function buildPublishCommand(options: unknown): string;
|
||||
export function validatePreflightManifest(manifest: unknown, params: unknown): void;
|
||||
export function validateFullManifest(manifest: unknown, params: unknown): void;
|
||||
export function validateNpmPreflightRunSource({
|
||||
workflowRun,
|
||||
workflowRef,
|
||||
isTrustedWorkflowAncestor,
|
||||
}: {
|
||||
workflowRun: { headSha: string };
|
||||
workflowRef: string;
|
||||
isTrustedWorkflowAncestor?: ((ancestor: string, target: string) => boolean) | undefined;
|
||||
}): {
|
||||
status: string;
|
||||
headSha: string;
|
||||
workflowRef: string;
|
||||
};
|
||||
export function candidateParallelsArgs(
|
||||
tarballPath: unknown,
|
||||
dependencyTarballPaths?: unknown[],
|
||||
|
||||
@@ -587,6 +587,24 @@ function gitIsAncestor(ancestor, target) {
|
||||
);
|
||||
}
|
||||
|
||||
export function validateNpmPreflightRunSource({
|
||||
workflowRun,
|
||||
workflowRef,
|
||||
isTrustedWorkflowAncestor = gitIsAncestor,
|
||||
}) {
|
||||
const trustedRef = `refs/remotes/origin/${workflowRef}`;
|
||||
if (!isTrustedWorkflowAncestor(workflowRun.headSha, trustedRef)) {
|
||||
throw new Error(
|
||||
`npm preflight workflow SHA ${workflowRun.headSha} is not reachable from trusted ${workflowRef}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
status: "passed",
|
||||
headSha: workflowRun.headSha,
|
||||
workflowRef,
|
||||
};
|
||||
}
|
||||
|
||||
function candidateContributionRecordPullRequests(
|
||||
section,
|
||||
label,
|
||||
@@ -1332,9 +1350,10 @@ async function main() {
|
||||
workflowName: "OpenClaw NPM Release",
|
||||
workflowRef: options.workflowRef,
|
||||
});
|
||||
if (npmRun.headSha !== targetSha) {
|
||||
throw new Error(`run SHA mismatch: tag=${targetSha} npm=${npmRun.headSha}`);
|
||||
}
|
||||
const npmPreflightSource = validateNpmPreflightRunSource({
|
||||
workflowRun: npmRun,
|
||||
workflowRef: options.workflowRef,
|
||||
});
|
||||
|
||||
const npmDir = join(options.outputDir, "npm-preflight");
|
||||
const fullDir = join(options.outputDir, "full-release-validation");
|
||||
@@ -1435,6 +1454,7 @@ async function main() {
|
||||
fullReleaseValidationUrl: fullRun.url,
|
||||
fullReleaseValidationControls: fullManifest.controls,
|
||||
npmPreflightUrl: npmRun.url,
|
||||
npmPreflightSource,
|
||||
artifacts: {
|
||||
npmPreflight: npmArtifactName,
|
||||
fullReleaseValidation: fullArtifactName,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
validateCandidateCheckout,
|
||||
validateCandidateReleaseNotes,
|
||||
validateFullManifest,
|
||||
validateNpmPreflightRunSource,
|
||||
validatePreflightManifest,
|
||||
validateWindowsSourceRelease,
|
||||
} from "../../scripts/release-candidate-checklist.mjs";
|
||||
@@ -520,6 +521,34 @@ describe("release candidate checklist", () => {
|
||||
).toThrow("invalid dependency tarball metadata");
|
||||
});
|
||||
|
||||
it("trusts the npm workflow SHA while binding the candidate through its manifest", () => {
|
||||
const workflowSha = "a".repeat(40);
|
||||
const isTrustedWorkflowAncestor = vi.fn(() => true);
|
||||
|
||||
expect(
|
||||
validateNpmPreflightRunSource({
|
||||
workflowRun: { headSha: workflowSha },
|
||||
workflowRef: "main",
|
||||
isTrustedWorkflowAncestor,
|
||||
}),
|
||||
).toEqual({
|
||||
status: "passed",
|
||||
headSha: workflowSha,
|
||||
workflowRef: "main",
|
||||
});
|
||||
expect(isTrustedWorkflowAncestor).toHaveBeenCalledWith(workflowSha, "refs/remotes/origin/main");
|
||||
});
|
||||
|
||||
it("rejects npm preflight workflow code outside the trusted ref", () => {
|
||||
expect(() =>
|
||||
validateNpmPreflightRunSource({
|
||||
workflowRun: { headSha: "a".repeat(40) },
|
||||
workflowRef: "main",
|
||||
isTrustedWorkflowAncestor: () => false,
|
||||
}),
|
||||
).toThrow("is not reachable from trusted main");
|
||||
});
|
||||
|
||||
it("requires run ids when dispatch is disabled", () => {
|
||||
expect(() => parseArgs(["--tag", "v2026.5.14-beta.3", "--skip-dispatch"])).toThrow(
|
||||
"--skip-dispatch requires --full-release-run and --npm-preflight-run",
|
||||
|
||||
Reference in New Issue
Block a user