#!/usr/bin/env node const assert = require("assert"); const cp = require("child_process"); const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); const repo = path.resolve(__dirname, ".."); const releaseRoot = path.resolve( process.env.CLUSTERFLUX_PUBLIC_RELEASE_DIR || path.join(repo, "target/public-release") ); const acceptanceRoot = path.join(repo, "target/acceptance"); const manifestPath = process.env.CLUSTERFLUX_PUBLIC_RELEASE_MANIFEST || path.join(releaseRoot, "public-release-manifest.json"); const reportPath = process.env.CLUSTERFLUX_PUBLIC_RELEASE_PREFLIGHT_REPORT || path.join(acceptanceRoot, "public-release-preflight.json"); function commandOutput(command, args, options = {}) { try { return cp .execFileSync(command, args, { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...options, }) .trim(); } catch (_) { return null; } } function nonInteractiveEnv(extra = {}) { return { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: process.env.GIT_ASKPASS || "/bin/false", SSH_ASKPASS: process.env.SSH_ASKPASS || "/bin/false", GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND || "ssh -o BatchMode=yes -o NumberOfPasswordPrompts=0", ...extra, }; } function expectedSourceCommit() { const head = commandOutput("git", ["rev-parse", "HEAD"]); assert(head && /^[0-9a-f]{40}$/u.test(head), "preflight requires an exact Git HEAD"); const asserted = process.env.CLUSTERFLUX_ACCEPTANCE_COMMIT; if (asserted) { assert.strictEqual(asserted, head, "acceptance commit must match Git HEAD"); } return head; } function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } function sha256File(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); } function sha256Buffer(buffer) { return crypto.createHash("sha256").update(buffer).digest("hex"); } function readArchiveMember(archive, member) { return cp.execFileSync("tar", ["-xOzf", archive, `./${member}`], { cwd: repo, encoding: null, maxBuffer: 128 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], }); } function parseSha256Sums(file) { const sums = new Map(); for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) { if (!line.trim()) continue; const match = /^([0-9a-f]{64})\s+(.+)$/.exec(line.trim()); assert(match, `malformed SHA256SUMS line: ${line}`); sums.set(path.basename(match[2]), match[1]); } return sums; } function remoteHead(remote) { const output = commandOutput("git", ["ls-remote", remote, "HEAD", "refs/heads/main"], { env: nonInteractiveEnv(), timeout: Number(process.env.CLUSTERFLUX_PUBLIC_REPO_REMOTE_TIMEOUT_MS || 30000), }); if (!output) return null; const lines = output.split(/\r?\n/).filter(Boolean); const head = lines.find((line) => line.endsWith("\tHEAD")) || lines[0]; return head && head.split(/\s+/)[0]; } function publicTreeAlreadyPushed(manifest) { return ( (manifest.public_tree_publish && manifest.public_tree_publish.pushed === true) || process.env.CLUSTERFLUX_PUBLIC_TREE_ALREADY_PUSHED === "1" ); } function publicTreePushSource(manifest) { return manifest.public_tree_publish && manifest.public_tree_publish.pushed === true ? "manifest" : "external-env"; } function publicRepoRemoteForManifest(manifest) { return ( manifest.public_repo_url || manifest.public_repo_remote || process.env.CLUSTERFLUX_PUBLIC_REPO_REMOTE || process.env.CLUSTERFLUX_PUBLIC_REPO_URL || null ); } function publicTreeCommitForManifest(manifest) { return ( (manifest.public_tree_publish && manifest.public_tree_publish.commit) || process.env.CLUSTERFLUX_PUBLIC_TREE_COMMIT || process.env.CLUSTERFLUX_PUBLIC_RELEASE_TARGET || null ); } function envState(name) { return process.env[name] ? "set" : "unset"; } function staleEvidence(file, currentSourceCommit) { if (!fs.existsSync(file)) { return { file, status: "missing", source_commit: null, release_name: null }; } const evidence = readJson(file); if (!evidence.source_commit) { return { file, status: "unversioned", source_commit: null, release_name: evidence.release_name || null, }; } return { file, status: evidence.source_commit === currentSourceCommit ? "current" : "stale", source_commit: evidence.source_commit, release_name: evidence.release_name || null, }; } assert(fs.existsSync(manifestPath), `missing public release manifest: ${manifestPath}`); const manifest = readJson(manifestPath); const currentSourceCommit = expectedSourceCommit(); const currentTreeStatus = commandOutput("git", ["status", "--short"]) || ""; assert.strictEqual( currentTreeStatus, "", "public release preflight requires a clean source tree" ); assert.strictEqual(manifest.kind, "clusterflux-public-release"); assert.strictEqual( manifest.source_commit, currentSourceCommit, "public release manifest must be regenerated for the current acceptance commit" ); assert.strictEqual(manifest.source_tree_clean, true, "public release prep must start clean"); assert.strictEqual( publicTreeAlreadyPushed(manifest), true, "filtered public tree must be pushed to Forgejo before release publication" ); const publicRepoRemote = publicRepoRemoteForManifest(manifest); assert(publicRepoRemote, "manifest must record public repository URL or remote"); const publicTreeCommit = publicTreeCommitForManifest(manifest); const remoteMain = remoteHead(publicRepoRemote); assert(remoteMain, "Forgejo public repository main branch must be readable"); if (publicTreeCommit) { assert.strictEqual( remoteMain, publicTreeCommit, "Forgejo public repository main branch must match the prepared public tree commit" ); } assert(Array.isArray(manifest.assets) && manifest.assets.length > 0, "manifest assets missing"); const checksumAsset = manifest.assets.find((asset) => asset.name === "SHA256SUMS"); assert(checksumAsset, "manifest must include SHA256SUMS"); assert(fs.existsSync(checksumAsset.file), `missing checksum asset: ${checksumAsset.file}`); const checksums = parseSha256Sums(checksumAsset.file); const assets = manifest.assets.map((asset) => { assert(fs.existsSync(asset.file), `missing release asset: ${asset.file}`); const actual = sha256File(asset.file); const expected = checksums.get(asset.name); if (asset.name !== "SHA256SUMS") { assert.strictEqual(actual, expected, `checksum mismatch for ${asset.name}`); } return { name: asset.name, file: asset.file, bytes: fs.statSync(asset.file).size, sha256: actual, }; }); const binaryAsset = manifest.assets.find((asset) => asset.name.startsWith("clusterflux-public-binaries-") ); assert(binaryAsset, "manifest must include the exact candidate binary archive"); for (const [name, digest] of Object.entries(manifest.binary_digests)) { const binary = readArchiveMember(binaryAsset.file, `bin/${name}`); assert.strictEqual( `sha256:${sha256Buffer(binary)}`, digest, `candidate archive binary ${name} does not match the manifest` ); } assert( manifest.final_evidence && manifest.final_evidence.complete === true, "release publication requires complete final result and transcript evidence" ); const evidenceAsset = manifest.assets.find((asset) => asset.name.startsWith("clusterflux-public-evidence-") ); assert(evidenceAsset, "manifest must include the final evidence archive"); const binding = JSON.parse( readArchiveMember(evidenceAsset.file, "EVIDENCE_BINDING.json").toString("utf8") ); assert.strictEqual(binding.source_commit, currentSourceCommit); assert.strictEqual(binding.source_tree_digest, manifest.source_tree_digest); assert.strictEqual(binding.public_tree_identity, manifest.public_tree_identity); assert.deepStrictEqual(binding.binary_digests, manifest.binary_digests); assert(binding.final_evidence && binding.final_evidence.complete === true); assert(Array.isArray(binding.included_evidence)); const includedEvidence = new Map( binding.included_evidence.map((entry) => [entry.name, entry.sha256]) ); for (const required of ["FINAL_RELEASE_RESULT.json", "FINAL_RELEASE_TRANSCRIPT.txt"]) { assert(includedEvidence.has(required), `final evidence archive is missing ${required}`); const contents = readArchiveMember(evidenceAsset.file, required); assert.strictEqual( sha256Buffer(contents), includedEvidence.get(required), `final evidence digest mismatch for ${required}` ); } const finalResult = JSON.parse( readArchiveMember(evidenceAsset.file, "FINAL_RELEASE_RESULT.json").toString("utf8") ); assert.strictEqual(finalResult.source_commit, currentSourceCommit); assert.strictEqual(finalResult.source_tree_digest, manifest.source_tree_digest); assert.strictEqual(finalResult.public_tree_identity, manifest.public_tree_identity); assert.deepStrictEqual(finalResult.binary_digests, manifest.binary_digests); assert.strictEqual( manifest.release_candidate?.mode, "finalized-exact", "publication requires exact candidate finalization" ); assert.strictEqual(finalResult.release_binding?.source_commit, currentSourceCommit); assert.strictEqual( finalResult.release_binding?.source_tree_digest, manifest.source_tree_digest ); assert.strictEqual( finalResult.release_binding?.public_tree_identity, manifest.public_tree_identity ); assert.deepStrictEqual( finalResult.release_binding?.binary_digests, manifest.binary_digests ); assert.deepStrictEqual( finalResult.release_candidate, manifest.release_candidate ); assert.strictEqual(finalResult.acceptance_result, "passed"); assert.deepStrictEqual(finalResult.scenario_skips, []); assert( Array.isArray(finalResult.named_scenarios) && finalResult.named_scenarios.length === 16 && finalResult.named_scenarios.every((scenario) => scenario.status === "passed"), "all sixteen named final scenarios must be present and passed" ); assert( Array.isArray(finalResult.strict_requirement_ledger) && finalResult.strict_requirement_ledger.length > 0 && finalResult.strict_requirement_ledger.every((requirement) => requirement.passed === true), "the strict final requirement ledger must be present and passed" ); const finalTranscript = readArchiveMember( evidenceAsset.file, "FINAL_RELEASE_TRANSCRIPT.txt" ).toString("utf8"); for (const identity of [ currentSourceCommit, manifest.source_tree_digest, manifest.public_tree_identity, ...Object.values(manifest.binary_digests), ]) { assert(finalTranscript.includes(identity), `final transcript is missing identity ${identity}`); } const evidence = [ staleEvidence( path.join(acceptanceRoot, "public-release-forgejo-release.json"), currentSourceCommit ), staleEvidence( path.join(acceptanceRoot, "public-release-service.json"), currentSourceCommit ), staleEvidence( path.join(acceptanceRoot, "hosted-client-compat.json"), currentSourceCommit ), staleEvidence( path.join(acceptanceRoot, "core-coordinator-compat.json"), currentSourceCommit ), staleEvidence( path.join(acceptanceRoot, "public-release-e2e.json"), currentSourceCommit ), staleEvidence( path.join(acceptanceRoot, "public-release-final.json"), currentSourceCommit ), ]; const report = { kind: "clusterflux-public-release-preflight", source_commit: currentSourceCommit, release_name: manifest.release_name, public_tree_commit: publicTreeCommit || remoteMain, public_tree_push_source: publicTreePushSource(manifest), public_repo_url: publicRepoRemote, public_repo_remote_head: remoteMain, source_tree_clean: currentTreeStatus === "", local_assets_ready: true, assets, final_evidence: { complete: true, binding, }, evidence, external_gates: { forgejo_release_publication: { status: envState("CLUSTERFLUX_FORGEJO_TOKEN") === "set" ? "ready" : "pending", env: { CLUSTERFLUX_FORGEJO_TOKEN: envState("CLUSTERFLUX_FORGEJO_TOKEN"), }, }, live_service_smoke: { status: envState("CLUSTERFLUX_PUBLIC_RELEASE_SERVICE_ADDR") === "set" && envState("CLUSTERFLUX_PUBLIC_RELEASE_BROWSER_OPEN_COMMAND") === "set" ? "ready" : "pending", env: { CLUSTERFLUX_PUBLIC_RELEASE_SERVICE_ADDR: envState( "CLUSTERFLUX_PUBLIC_RELEASE_SERVICE_ADDR" ), CLUSTERFLUX_PUBLIC_RELEASE_BROWSER_OPEN_COMMAND: envState( "CLUSTERFLUX_PUBLIC_RELEASE_BROWSER_OPEN_COMMAND" ), }, }, public_release_e2e: { status: envState("CLUSTERFLUX_PUBLIC_RELEASE_E2E") === "set" && process.env.CLUSTERFLUX_PUBLIC_RELEASE_E2E === "1" && envState("CLUSTERFLUX_PUBLIC_RELEASE_BROWSER_OPEN_COMMAND") === "set" ? "ready" : "pending", env: { CLUSTERFLUX_PUBLIC_RELEASE_E2E: process.env.CLUSTERFLUX_PUBLIC_RELEASE_E2E || "unset", CLUSTERFLUX_PUBLIC_RELEASE_BROWSER_OPEN_COMMAND: envState( "CLUSTERFLUX_PUBLIC_RELEASE_BROWSER_OPEN_COMMAND" ), }, }, final_evidence: { status: envState("CLUSTERFLUX_PUBLIC_RELEASE_FINAL") === "set" && process.env.CLUSTERFLUX_PUBLIC_RELEASE_FINAL === "1" ? "ready" : "pending", env: { CLUSTERFLUX_PUBLIC_RELEASE_FINAL: process.env.CLUSTERFLUX_PUBLIC_RELEASE_FINAL || "unset", }, }, }, }; fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); console.log(JSON.stringify({ report: reportPath, release_name: report.release_name }, null, 2));