Files
tech-log-frontend/scripts/lib/stage-verified-promotion-cli.ts
T

95 lines
3.4 KiB
TypeScript

import { appendFile } from "node:fs/promises";
import {
cleanupFinalizedPromotion,
finalizeVerifiedPromotion,
} from "./promotion-stager.ts";
export async function runStageVerifiedPromotionCli(
environment: NodeJS.ProcessEnv,
dependencies: Readonly<{
cwd?: () => string;
finalize?: typeof finalizeVerifiedPromotion;
cleanup?: typeof cleanupFinalizedPromotion;
appendOutput?: (path: string, content: string) => Promise<void>;
writeStdout?: (content: string) => void;
}> = {},
): Promise<void> {
const required = (name: string): string => {
const value = environment[name];
if (!value) throw new TypeError(`promotion staging environment is missing ${name}`);
return value;
};
const attempt = Number(
environment.GITEA_RUN_ATTEMPT ??
environment.GITHUB_RUN_ATTEMPT ??
required("CI_RUN_ATTEMPT"),
);
if (!Number.isInteger(attempt) || attempt < 1 || attempt > 1_000) {
throw new TypeError("promotion staging run attempt is invalid");
}
const runnerTempRoot = required("RUNNER_TEMP");
const staged = await (dependencies.finalize ?? finalizeVerifiedPromotion)({
repositoryRoot: (dependencies.cwd ?? process.cwd)(),
archivePath: required("CANDIDATE_ARCHIVE_PATH"),
expectedArchiveSha256: required("CANDIDATE_ARCHIVE_SHA256"),
vulnerabilityReportPath: required("VULNERABILITY_REPORT_PATH"),
provenanceAttestationPath: required("PROVENANCE_ATTESTATION_PATH"),
vulnerabilityPublicKeyPath: required("VULNERABILITY_PUBLIC_KEY_PATH"),
vulnerabilityKeyId: required("VULNERABILITY_KEY_ID"),
provenancePublicKeyPath: required("PROVENANCE_PUBLIC_KEY_PATH"),
provenanceKeyId: required("PROVENANCE_KEY_ID"),
expectedRun: {
id:
environment.GITEA_RUN_ID ??
environment.GITHUB_RUN_ID ??
required("CI_RUN_ID"),
attempt,
sourceRevision:
environment.EXPECTED_SOURCE_REVISION ?? required("VITE_COMMIT_SHA"),
},
vulnerabilityInvocationNonce: required("VULNERABILITY_INVOCATION_NONCE"),
provenanceInvocationNonce: required("PROVENANCE_INVOCATION_NONCE"),
runnerTempRoot,
});
try {
const output = required("GITHUB_OUTPUT");
const content = [
`staging_root=${staged.stagingRoot}`,
`cleanup_token=${staged.cleanupToken}`,
`runner_temp_dev=${staged.runnerTempIdentity.dev}`,
`runner_temp_ino=${staged.runnerTempIdentity.ino}`,
`staging_dev=${staged.stagingIdentity.dev}`,
`staging_ino=${staged.stagingIdentity.ino}`,
"",
].join("\n");
await (dependencies.appendOutput ?? defaultAppendOutput)(output, content);
} catch (error) {
try {
await (dependencies.cleanup ?? cleanupFinalizedPromotion)({
runnerTempRoot,
stagingRoot: staged.stagingRoot,
cleanupToken: staged.cleanupToken,
runnerTempIdentity: staged.runnerTempIdentity,
stagingIdentity: staged.stagingIdentity,
});
} catch (cleanupError) {
throw new AggregateError(
[error, cleanupError],
"promotion output publication and direct staging cleanup both failed",
{ cause: cleanupError },
);
}
throw error;
}
(dependencies.writeStdout ?? process.stdout.write.bind(process.stdout))(
`Promotion staging: ${staged.files
.map(({ name, sha256 }) => `${name}=${sha256}`)
.join(", ")} PASS\n`,
);
}
async function defaultAppendOutput(path: string, content: string): Promise<void> {
await appendFile(path, content, { encoding: "utf8" });
}