163 lines
6.4 KiB
JavaScript
Executable file
163 lines
6.4 KiB
JavaScript
Executable file
#!/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) {
|
|
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);
|
|
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.strictEqual(result.strict_requirement_ledger.length, 25);
|
|
assert(
|
|
result.strict_requirement_ledger.every((requirement) => requirement.passed === true),
|
|
"final validation contains a failed launch requirement"
|
|
);
|
|
|
|
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 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);
|
|
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(resultPath, path.join(destinationRoot, "final-result.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 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();
|