Public release release-6d1a121b0a8a
Source commit: 6d1a121b0a8a636aac95998fe5d15c24c78eb7d0 Public tree identity: sha256:2bc22807f5b838286678b65d3f550b8ae4714ae673622041d4c2516a7c5b7926
This commit is contained in:
commit
21f097d9a8
210 changed files with 78649 additions and 0 deletions
294
scripts/public-release-preflight.js
Executable file
294
scripts/public-release-preflight.js
Executable file
|
|
@ -0,0 +1,294 @@
|
|||
#!/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 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 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,
|
||||
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));
|
||||
Loading…
Add table
Add a link
Reference in a new issue