#!/usr/bin/env node "use strict"; const assert = require("node:assert"); const crypto = require("node:crypto"); const fs = require("node:fs"); const path = require("node:path"); const { spawnSync } = require("node:child_process"); const repo = path.resolve(__dirname, ".."); const releaseRoot = path.resolve( process.env.CLUSTERFLUX_PUBLIC_RELEASE_DIR || path.join(repo, "target/public-release-final") ); const manifestPath = path.join(releaseRoot, "public-release-manifest.json"); const resultPath = path.resolve( process.env.CLUSTERFLUX_FINAL_RESULT || path.join(repo, "target/acceptance/cli-happy-path-live.json") ); const transcriptPath = path.resolve( process.env.CLUSTERFLUX_VALIDATION_TRANSCRIPT || path.join(repo, "target/acceptance/clusterflux-manual-launch-transcript.log") ); function sha256(file) { const hash = crypto.createHash("sha256"); hash.update(fs.readFileSync(file)); return hash.digest("hex"); } function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } function write(file, contents) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, contents); } function copyVerifiedAsset(asset, destinationRoot) { const source = path.resolve(releaseRoot, asset.file); assert(fs.existsSync(source), `missing finalized asset ${source}`); assert.strictEqual( sha256(source), asset.sha256, `finalized asset digest changed: ${asset.name}` ); const destination = path.join(destinationRoot, "assets", asset.name); fs.copyFileSync(source, destination); assert.strictEqual(sha256(destination), asset.sha256); return destination; } function extractBinaries(archive, destinationRoot, expectedDigests) { const staging = path.join(destinationRoot, ".binary-extract"); fs.mkdirSync(staging, { recursive: true }); const extracted = spawnSync("tar", ["-xzf", archive, "-C", staging], { encoding: "utf8", }); if (extracted.status !== 0) { throw new Error(`failed to extract ${archive}: ${extracted.stderr}`); } const binRoot = path.join(staging, "bin"); assert(fs.existsSync(binRoot), `binary archive omitted bin/: ${archive}`); const copied = []; for (const name of fs.readdirSync(binRoot).sort()) { const source = path.join(binRoot, name); if (!fs.statSync(source).isFile()) continue; const destination = path.join(destinationRoot, "binaries", name); fs.copyFileSync(source, destination); fs.chmodSync(destination, 0o755); assert.strictEqual( `sha256:${sha256(destination)}`, expectedDigests[name], `tested binary digest changed: ${name}` ); copied.push(destination); } fs.rmSync(staging, { recursive: true, force: true }); assert(copied.length > 0, `binary archive contained no binaries: ${archive}`); return copied; } function main() { assert(fs.existsSync(manifestPath), `missing final manifest ${manifestPath}`); assert(fs.existsSync(resultPath), `missing final result ${resultPath}`); assert(fs.existsSync(transcriptPath), `missing validation transcript ${transcriptPath}`); const manifest = readJson(manifestPath); const result = readJson(resultPath); assert.strictEqual(result.acceptance_result, "passed"); assert.strictEqual(result.source_commit, manifest.source_commit); assert.strictEqual(result.source_tree_digest, manifest.source_tree_digest); assert.deepStrictEqual(result.binary_digests, manifest.binary_digests); assert.strictEqual(result.scenario_skips.length, 0); assert(result.strict_requirement_ledger.length >= 29); assert( result.strict_requirement_ledger.every( (requirement) => requirement.passed === true && Number.isFinite(requirement.duration_ms) && requirement.duration_ms > 0 && requirement.evidence ), "final validation contains a failed launch requirement" ); assert.strictEqual( result.named_scenarios.length, result.strict_requirement_ledger.length ); const destinationRoot = path.resolve( process.env.CLUSTERFLUX_GITHUB_RELEASE_DIR || path.join(repo, "target/github-release", manifest.release_name) ); fs.rmSync(destinationRoot, { recursive: true, force: true }); fs.mkdirSync(path.join(destinationRoot, "assets"), { recursive: true }); fs.mkdirSync(path.join(destinationRoot, "binaries"), { recursive: true }); const copiedAssets = manifest.assets.map((asset) => copyVerifiedAsset(asset, destinationRoot) ); const publicationGuidance = [ JSON.stringify(manifest.notes || []), ...copiedAssets .filter((file) => file.endsWith(".md")) .map((file) => fs.readFileSync(file, "utf8")), ].join("\n"); assert.doesNotMatch( publicationGuidance, /(?:download|upload|release downloads?)[^\n]*Forgejo/i, "manual GitHub package contains Forgejo release-publication instructions" ); const binaryArchive = copiedAssets.find((file) => path.basename(file).startsWith("clusterflux-public-binaries-") ); assert(binaryArchive, "final manifest omitted the public binary archive"); const binaries = extractBinaries( binaryArchive, destinationRoot, manifest.binary_digests ); const vsix = copiedAssets.find((file) => file.endsWith(".vsix")); assert(vsix, "final manifest omitted the tested VSIX"); assert.strictEqual( sha256(vsix), manifest.release_candidate.extension_sha256, "tested VSIX digest does not match final candidate binding" ); write(path.join(destinationRoot, "SOURCE_REVISION"), `${manifest.source_commit}\n`); write( path.join(destinationRoot, "SOURCE_TREE_DIGEST"), `${manifest.source_tree_digest}\n` ); write(path.join(destinationRoot, "VSIX_SHA256"), `${sha256(vsix)} ${path.basename(vsix)}\n`); fs.copyFileSync(manifestPath, path.join(destinationRoot, "public-release-manifest.json")); fs.copyFileSync(transcriptPath, path.join(destinationRoot, "VALIDATION_TRANSCRIPT.log")); write( path.join(destinationRoot, "RELEASE_NOTES.md"), `# Clusterflux ${manifest.release_name}\n\n` + "First manual GitHub launch release of Clusterflux. The release includes the filtered public source, real Linux CLI/node/coordinator binaries, and the exact VSIX used by the final production-shaped validation batch.\n" ); write( path.join(destinationRoot, "KNOWN_LIMITATIONS.md"), "# Known limitations\n\n" + "- The launch batch validates one enrolled Linux node with rootless Podman.\n" + "- Full Windows sandbox validation is deferred.\n" + "- Providers, billing, teams, managed compute, and operator-panel UI are not part of this release.\n" ); const testedUploadAssets = [ ...copiedAssets, ...binaries, ] .sort() .map((file) => ({ file: path.relative(destinationRoot, file), sha256: `sha256:${sha256(file)}`, })); write( path.join(destinationRoot, "final-result.json"), `${JSON.stringify( { ...result, manual_release: { source_revision: manifest.source_commit, source_tree_digest: manifest.source_tree_digest, tested_upload_assets: testedUploadAssets, publication: "manual GitHub upload from this directory", }, }, null, 2 )}\n` ); const checksummed = [ ...copiedAssets, ...binaries, path.join(destinationRoot, "SOURCE_REVISION"), path.join(destinationRoot, "SOURCE_TREE_DIGEST"), path.join(destinationRoot, "VSIX_SHA256"), path.join(destinationRoot, "public-release-manifest.json"), path.join(destinationRoot, "final-result.json"), path.join(destinationRoot, "VALIDATION_TRANSCRIPT.log"), path.join(destinationRoot, "RELEASE_NOTES.md"), path.join(destinationRoot, "KNOWN_LIMITATIONS.md"), ].sort(); write( path.join(destinationRoot, "SHA256SUMS"), `${checksummed .map((file) => `${sha256(file)} ${path.relative(destinationRoot, file)}`) .join("\n")}\n` ); console.log(destinationRoot); } main();