import fs from "fs"; import path from "path"; import YAML from "yaml"; import { loadSimulationCallerNetwork, type SimulationCallerNetworkBinding, } from "./simulationCallers"; export interface ReleaseFinalizeInput { root: string; deploymentId: string; targetNetworks: string[]; now?: () => string; } export interface ReleaseFinalizeResult { releasePath: string; statusPath: string; handoffPath: string; reportPath: string; } interface ManifestStep { id: string; kind: string; } interface SimulationEvidence { executionId: string; resultPath: string; callerNetworkHash: string; } interface ResultEnvelope { executionId: string; steps: Record[]; } interface PendingWrite { filePath: string; contents: string; } interface StagedWrite extends PendingWrite { temporaryPath: string; backupPath: string; originalMoved: boolean; replacementMoved: boolean; } const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/; const SAFE_NETWORK = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/; const HASH = /^[0-9a-f]{64}$/; const ADDRESS = /^0x[0-9a-fA-F]{40}$/; const ZERO_ADDRESS = /^0x0{40}$/i; const RESULT_NAMES = ["execution", "view", "verification"] as const; const object = (value: unknown, label: string): Record => { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`${label} must be an object`); } return value as Record; }; const optionalObject = (value: unknown): Record => ( value && typeof value === "object" && !Array.isArray(value) ? value as Record : {} ); const requiredString = ( source: Record, key: string, label: string, ): string => { const value = source[key]; if (typeof value !== "string" || !value.trim()) { throw new Error(`${label}.${key} must be a non-empty string`); } return value.trim(); }; const safeId = (value: string, label: string): string => { if (!SAFE_ID.test(value)) throw new Error(`${label} is invalid`); return value; }; const safeNetwork = (value: string, label: string): string => { if (!SAFE_NETWORK.test(value) || ["__proto__", "prototype", "constructor"].includes(value)) { throw new Error(`${label} is invalid`); } return value; }; const normalizedAddress = (value: unknown, label: string): string => { if (typeof value !== "string") { throw new Error(`${label} must be a valid non-zero EVM address`); } const address = value.trim(); if (!ADDRESS.test(address) || ZERO_ADDRESS.test(address)) { throw new Error(`${label} must be a valid non-zero EVM address`); } return address; }; const normalizedTargets = (values: string[], label: string): string[] => { if (!Array.isArray(values)) throw new Error(`${label} must be a unique non-empty list`); const targets = values.map((value) => ( safeNetwork(typeof value === "string" ? value.trim() : "", label) )); if (targets.length === 0 || new Set(targets).size !== targets.length) { throw new Error(`${label} must be a unique non-empty list`); } return targets.sort(); }; const stringArray = (value: unknown, label: string): string[] => { if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) { throw new Error(`${label} must be an array of strings`); } return normalizedTargets(value as string[], label); }; const orderedStringArray = (value: unknown, label: string): string[] => { if (!Array.isArray(value) || value.some((item) => ( typeof item !== "string" || !item.trim() || item !== item.trim() ))) { throw new Error(`${label} must be an ordered array of non-empty strings`); } return value as string[]; }; const sameStrings = (left: string[], right: string[]): boolean => ( JSON.stringify(left) === JSON.stringify(right) ); const readJsonObject = (filePath: string, label: string): Record => { if (!fs.existsSync(filePath)) throw new Error(`${label} is missing`); try { return object(JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown, label); } catch (error) { if (error instanceof SyntaxError) { throw new Error(`${label} must contain valid JSON: ${error.message}`); } throw error; } }; const validateHash = (value: unknown, label: string): string => { if (typeof value !== "string" || !HASH.test(value)) { throw new Error(`${label} must be a lowercase sha256 hash`); } return value; }; const parseManifestSteps = (manifestText: string, deploymentId: string): ManifestStep[] => { let parsed: unknown; try { parsed = YAML.parse(manifestText) as unknown; } catch (error) { throw new Error( `manifest.yaml must contain valid YAML: ${error instanceof Error ? error.message : String(error)}`, ); } const manifest = object(parsed, "manifest.yaml"); if (manifest.id !== deploymentId) { throw new Error("Manifest deployment identity does not match the release task"); } if (!Array.isArray(manifest.steps) || manifest.steps.length === 0) { throw new Error("Manifest must contain a non-empty steps array"); } const seen = new Set(); return manifest.steps.map((value, index) => { const step = object(value, `Manifest step ${index}`); const id = requiredString(step, "id", `Manifest step ${index}`); const kind = requiredString(step, "kind", `Manifest step ${id}`); if (step.id !== id || step.kind !== kind) { throw new Error(`Manifest step ${id} fields must not contain surrounding whitespace`); } if (seen.has(id)) throw new Error(`Duplicate Manifest step ${id}`); seen.add(id); return { id, kind }; }); }; const confinedRegularFile = (input: { taskDir: string; filePath: string; label: string; }): string => { if (!fs.existsSync(input.filePath)) throw new Error(`${input.label} is missing`); const stat = fs.lstatSync(input.filePath); if (stat.isSymbolicLink() || !stat.isFile()) { throw new Error(`${input.label} must be a regular non-symlink file`); } const taskRoot = fs.realpathSync(input.taskDir); const realFilePath = fs.realpathSync(input.filePath); if (!realFilePath.startsWith(`${taskRoot}${path.sep}`)) { throw new Error(`${input.label} escapes the deployment task directory`); } return realFilePath; }; const resolveExecutionResult = (input: { taskDir: string; network: string; resultPath: string; }): { absolutePath: string; normalizedPath: string; pathExecutionId: string } => { const normalizedPath = input.resultPath.replaceAll("\\", "/"); const parts = normalizedPath.split("/"); if ( path.isAbsolute(input.resultPath) || parts.length !== 5 || parts[0] !== "results" || parts[1] !== input.network || parts[3] !== "simulation" || parts[4] !== "execution.json" || parts.some((part) => !part || part === "." || part === "..") ) { throw new Error(`Simulation result path is invalid for ${input.network}`); } const pathExecutionId = safeId(parts[2], `${input.network} simulation executionId`); const filePath = path.resolve(input.taskDir, ...parts); return { absolutePath: confinedRegularFile({ taskDir: input.taskDir, filePath, label: `${input.network} simulation execution result`, }), normalizedPath, pathExecutionId, }; }; const readResultEnvelope = (input: { taskDir: string; deploymentId: string; network: string; filePath: string; resultName: typeof RESULT_NAMES[number]; expectedExecutionId?: string; }): ResultEnvelope => { const label = `${input.network} simulation ${input.resultName} result`; const filePath = confinedRegularFile({ taskDir: input.taskDir, filePath: input.filePath, label, }); const result = readJsonObject(filePath, label); const executionId = safeId(requiredString(result, "executionId", label), "executionId"); if ( result.taskId !== input.deploymentId || result.target !== input.network || result.mode !== "simulate" || (input.expectedExecutionId !== undefined && executionId !== input.expectedExecutionId) || !Array.isArray(result.steps) ) { throw new Error(`Canonical simulation ${input.resultName} evidence is invalid for ${input.network}`); } return { executionId, steps: result.steps.map((step, index) => object(step, `${label}.steps[${index}]`)), }; }; const validateTerminalCallerEvidence = (input: { network: string; terminal: Record; binding: SimulationCallerNetworkBinding; manifestSteps: ManifestStep[]; }): void => { const selectedStepIds = orderedStringArray( input.terminal.selectedStepIds, `${input.network} simulation selectedStepIds`, ); const manifestStepIds = input.manifestSteps.map((step) => step.id); const callers = input.terminal.simulationCallers; if ( input.terminal.status !== "succeeded" || input.terminal.signerAddress !== undefined || !sameStrings(selectedStepIds, manifestStepIds) || input.terminal.simulationCallerNetworkHash !== input.binding.networkHash || !Array.isArray(callers) || callers.length !== input.binding.resolved.length ) { throw new Error(`Canonical simulation caller completion evidence is invalid for ${input.network}`); } for (const [index, expected] of input.binding.resolved.entries()) { const actual = object(callers[index], `${input.network} simulation caller ${index}`); const callerAddress = normalizedAddress( actual.callerAddress, `${input.network} simulation caller ${expected.stepId}`, ); if ( actual.stepId !== expected.stepId || callerAddress.toLowerCase() !== expected.callerAddress.toLowerCase() ) { throw new Error(`Ordered simulation caller evidence is invalid for ${input.network}`); } } }; const validateStepCallerAnnotations = (input: { network: string; binding: SimulationCallerNetworkBinding; manifestSteps: ManifestStep[]; results: Record; }): void => { const expectedByStep = new Map(input.binding.resolved.map((caller) => [ caller.stepId, caller.callerAddress, ])); const kindByStep = new Map(input.manifestSteps.map((step) => [step.id, step.kind])); const seen = new Set(); for (const resultName of RESULT_NAMES) { for (const record of input.results[resultName].steps) { if (record.kind === "workflow-simulation-completed") { if (resultName !== "execution") { throw new Error(`Simulation completion evidence is misplaced for ${input.network}`); } continue; } const stepId = requiredString( record, "stepId", `${input.network} simulation ${resultName} annotation`, ); const expectedCaller = expectedByStep.get(stepId); if (!expectedCaller || record.kind !== kindByStep.get(stepId)) { throw new Error(`Simulation caller annotation references an unknown Manifest step ${stepId}`); } const actualCaller = normalizedAddress( record.simulationCallerAddress, `${input.network} simulation caller annotation for ${stepId}`, ); if (actualCaller.toLowerCase() !== expectedCaller.toLowerCase()) { throw new Error(`Simulation caller annotation changed for ${input.network} step ${stepId}`); } seen.add(stepId); } } const missing = input.manifestSteps.find((step) => !seen.has(step.id)); if (missing) { throw new Error(`Missing simulation caller annotation for ${input.network} step ${missing.id}`); } }; const validateSimulationEvidence = (input: { taskDir: string; deploymentId: string; network: string; resultPath: string; manifestSteps: ManifestStep[]; binding: SimulationCallerNetworkBinding; }): SimulationEvidence => { const executionResult = resolveExecutionResult({ taskDir: input.taskDir, network: input.network, resultPath: input.resultPath, }); const resultDirectory = path.dirname(executionResult.absolutePath); const execution = readResultEnvelope({ taskDir: input.taskDir, deploymentId: input.deploymentId, network: input.network, filePath: executionResult.absolutePath, resultName: "execution", }); if (execution.executionId !== executionResult.pathExecutionId) { throw new Error(`Simulation executionId path does not match evidence for ${input.network}`); } const results: Record = { execution, view: readResultEnvelope({ taskDir: input.taskDir, deploymentId: input.deploymentId, network: input.network, filePath: path.join(resultDirectory, "view.json"), resultName: "view", expectedExecutionId: execution.executionId, }), verification: readResultEnvelope({ taskDir: input.taskDir, deploymentId: input.deploymentId, network: input.network, filePath: path.join(resultDirectory, "verification.json"), resultName: "verification", expectedExecutionId: execution.executionId, }), }; const terminalRecords = execution.steps.filter((step) => ( step.kind === "workflow-simulation-completed" )); const terminal = execution.steps.at(-1); if ( terminalRecords.length !== 1 || terminal !== terminalRecords[0] || terminal?.kind !== "workflow-simulation-completed" ) { throw new Error(`Canonical simulation completion must be terminal for ${input.network}`); } validateTerminalCallerEvidence({ network: input.network, terminal, binding: input.binding, manifestSteps: input.manifestSteps, }); validateStepCallerAnnotations({ network: input.network, binding: input.binding, manifestSteps: input.manifestSteps, results, }); return { executionId: execution.executionId, resultPath: executionResult.normalizedPath, callerNetworkHash: input.binding.networkHash, }; }; const writeFilesAtomically = (writes: PendingWrite[]): void => { const suffix = `${process.pid}.${Date.now()}`; const staged: StagedWrite[] = writes.map((write, index) => ({ ...write, temporaryPath: `${write.filePath}.${suffix}.${index}.tmp`, backupPath: `${write.filePath}.${suffix}.${index}.bak`, originalMoved: false, replacementMoved: false, })); try { for (const item of staged) { fs.mkdirSync(path.dirname(item.filePath), { recursive: true }); fs.writeFileSync(item.temporaryPath, item.contents, { flag: "wx" }); } for (const item of staged) { if (fs.existsSync(item.filePath)) { fs.renameSync(item.filePath, item.backupPath); item.originalMoved = true; } fs.renameSync(item.temporaryPath, item.filePath); item.replacementMoved = true; } } catch (error) { for (const item of [...staged].reverse()) { try { if (item.replacementMoved && fs.existsSync(item.filePath)) fs.unlinkSync(item.filePath); if (item.originalMoved && fs.existsSync(item.backupPath)) { fs.renameSync(item.backupPath, item.filePath); } } catch { // Preserve the original transaction error after best-effort rollback. } } for (const item of staged) { try { if (fs.existsSync(item.temporaryPath)) fs.unlinkSync(item.temporaryPath); if (fs.existsSync(item.backupPath)) fs.unlinkSync(item.backupPath); } catch { // Preserve the original transaction error after best-effort cleanup. } } throw error; } for (const item of staged) { try { if (fs.existsSync(item.backupPath)) fs.unlinkSync(item.backupPath); } catch { // The committed formal files are authoritative; a backup cleanup failure is non-fatal. } } }; const jsonContents = (value: unknown): string => `${JSON.stringify(value, null, 2)}\n`; export const finalizeReleaseVerification = ( input: ReleaseFinalizeInput, ): ReleaseFinalizeResult => { const deploymentId = safeId(input.deploymentId, "deploymentId"); const targets = normalizedTargets(input.targetNetworks, "target network"); const updatedAt = (input.now || (() => new Date().toISOString()))(); if (!Number.isFinite(Date.parse(updatedAt))) throw new Error("Finalize timestamp is invalid"); const taskBase = `scripts/tasks/${deploymentId}`; const releasePath = `${taskBase}/release.json`; const statusPath = `${taskBase}/status.json`; const handoffPath = `${taskBase}/handoff.json`; const reportPath = `${taskBase}/docs/release-verification.md`; const absolute = (relative: string): string => path.join(input.root, ...relative.split("/")); const taskDir = absolute(taskBase); const task = object( YAML.parse(fs.readFileSync(path.join(taskDir, "task.yaml"), "utf8")) as unknown, "task.yaml", ); const sourceSuperTaskId = safeId( requiredString(task, "sourceSuperTaskId", "task.yaml"), "sourceSuperTaskId", ); const developmentNetwork = safeNetwork( requiredString(task, "developmentNetwork", "task.yaml"), "developmentNetwork", ); if ( task.id !== deploymentId || targets.includes(developmentNetwork) || !sameStrings(stringArray(task.releaseTargetNetworks, "task releaseTargetNetworks"), targets) ) { throw new Error("Release task identity or target networks are invalid"); } const manifestText = fs.readFileSync(path.join(taskDir, "manifest.yaml"), "utf8"); const manifestSteps = parseManifestSteps(manifestText, deploymentId); const release = readJsonObject(absolute(releasePath), "release.json"); const releaseNetworks = object(release.networks, "release.json.networks"); if ( release.version !== 1 || release.sourceSuperTaskId !== sourceSuperTaskId || release.developmentNetwork !== developmentNetwork || !sameStrings(Object.keys(releaseNetworks).sort(), targets) ) { throw new Error("Release identity or target networks are invalid"); } const releaseManifestHash = validateHash(release.manifestHash, "release.json.manifestHash"); validateHash(release.artifactHash, "release.json.artifactHash"); const evidenceByNetwork: Record = {}; const nextReleaseNetworks: Record = {}; for (const network of targets) { const binding = loadSimulationCallerNetwork({ taskDir, targetNetworks: targets, network, manifestText, manifestSteps, }); if (binding.manifestHash !== releaseManifestHash) { throw new Error(`Manifest hash changed after release simulation for ${network}`); } const entry = object(releaseNetworks[network], `release.json.networks.${network}`); const resultPath = requiredString(entry, "simulationResult", `release.json.networks.${network}`); const simulatedAt = requiredString(entry, "simulatedAt", `release.json.networks.${network}`); if (!Number.isFinite(Date.parse(simulatedAt))) { throw new Error(`release.json.networks.${network}.simulatedAt is invalid`); } for (const key of ["manifestHash", "artifactHash", "parametersHash", "configHash"]) { validateHash(entry[key], `release.json.networks.${network}.${key}`); } if (entry.manifestHash !== binding.manifestHash) { throw new Error(`Manifest fingerprint changed after release simulation for ${network}`); } const evidence = validateSimulationEvidence({ taskDir, deploymentId, network, resultPath, manifestSteps, binding, }); evidenceByNetwork[network] = evidence; nextReleaseNetworks[network] = { ...entry, status: "ready", simulationCallerNetworkHash: evidence.callerNetworkHash, }; } const status = readJsonObject(absolute(statusPath), "status.json"); if ( status.sourceSuperTaskId !== sourceSuperTaskId || status.developmentNetwork !== developmentNetwork ) { throw new Error("Release status identity is invalid"); } const phases = optionalObject(status.phases); const networks = optionalObject(status.networks); const simulationResults = Object.fromEntries( targets.map((network) => [network, evidenceByNetwork[network].resultPath]), ); const simulationCallerNetworkHashes = Object.fromEntries( targets.map((network) => [network, evidenceByNetwork[network].callerNetworkHash]), ); const nextNetworks = { ...networks }; for (const network of targets) { nextNetworks[network] = { ...optionalObject(networks[network]), parameterStatus: "complete", simulationStatus: "succeeded", releaseStatus: "ready", deployStatus: "pending", simulationCallerNetworkHash: evidenceByNetwork[network].callerNetworkHash, lastExecutionId: evidenceByNetwork[network].executionId, lastExecutionMode: "simulate", updatedAt, }; } const nextStatus = { ...status, status: "release_ready", releaseTargetNetworks: targets, phases: { ...phases, releaseSimulation: { status: "succeeded", networks: targets, simulationCallerNetworkHashes, simulationResults, updatedAt, }, releaseVerification: { status: "succeeded", networks: targets, simulationCallerNetworkHashes, simulationResults, report: reportPath, updatedAt, }, deploy: { status: "pending", networks: targets }, }, networks: nextNetworks, updatedAt, }; const evidence = targets.map((network) => evidenceByNetwork[network].resultPath); const nextHandoff = { version: 1, stageKind: "release_verify", status: "release_ready", sourceSuperTaskId, deploymentId, developmentNetwork, releaseTargetNetworks: targets, readyReleaseNetworks: targets, simulationCallerNetworkHashes, evidence, evidenceDetails: { statusPath, taskPath: `${taskBase}/task.yaml`, releasePath, releaseVerificationReport: reportPath, simulationResults, simulationCallerNetworkHashes, }, updatedAt, }; const report = [ "# Release Verification Report", "", "stageKind: release_verify", "status: release_ready", `deploymentId: ${deploymentId}`, `sourceSuperTaskId: ${sourceSuperTaskId}`, `developmentNetwork: ${developmentNetwork}`, `generatedAt: ${updatedAt}`, "", "## Ready Networks", ...targets.flatMap((network) => [ `- network: ${network}`, ` simulationResult: ${evidenceByNetwork[network].resultPath}`, ` executionId: ${evidenceByNetwork[network].executionId}`, ` simulationCallerNetworkHash: ${evidenceByNetwork[network].callerNetworkHash}`, ]), "", ].join("\n"); writeFilesAtomically([ { filePath: absolute(releasePath), contents: jsonContents({ ...release, networks: nextReleaseNetworks }), }, { filePath: absolute(statusPath), contents: jsonContents(nextStatus) }, { filePath: absolute(handoffPath), contents: jsonContents(nextHandoff) }, { filePath: absolute(reportPath), contents: report }, ]); return { releasePath, statusPath, handoffPath, reportPath }; }; export const runReleaseFinalizeCli = ( argv: string[], defaultRoot: string = process.cwd(), ): ReleaseFinalizeResult => { const values = new Map(); for (let index = 0; index < argv.length; index += 1) { const item = argv[index]; if (!item.startsWith("--")) continue; const equalAt = item.indexOf("="); const key = item.slice(2, equalAt > 2 ? equalAt : undefined); const next = argv[index + 1]; const value = equalAt > 2 ? item.slice(equalAt + 1) : next; if (!value || value.startsWith("--")) throw new Error(`--${key} requires a value`); if (equalAt < 0) index += 1; values.set(key, [...(values.get(key) || []), value.trim()]); } for (const key of values.keys()) { if (!new Set(["root", "task", "network"]).has(key)) { throw new Error(`Unsupported release finalizer argument --${key}`); } } const one = (key: string, required = false): string | undefined => { const items = values.get(key) || []; if (items.length > 1) throw new Error(`--${key} may be provided only once`); if (required && !items[0]) throw new Error(`--${key} is required`); return items[0] || undefined; }; return finalizeReleaseVerification({ root: path.resolve(one("root") || defaultRoot), deploymentId: one("task", true)!, targetNetworks: (values.get("network") || []) .flatMap((item) => item.split(",")) .map((item) => item.trim()) .filter(Boolean), }); };