Publish Clusterflux release candidate 6e83783
This commit is contained in:
parent
4d75257c6e
commit
f3640643bd
16 changed files with 4667 additions and 106 deletions
163
scripts/prepare-manual-github-release.js
Executable file
163
scripts/prepare-manual-github-release.js
Executable file
|
|
@ -0,0 +1,163 @@
|
|||
#!/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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue