Source commit: e47f9c27bbebe6759f6b6fe7b12fd00bb20d116d Public tree identity: sha256:4c1af1dfd67d3f2b5088531ea8727b11124b013d2251a4d5088f51a6424c0196
322 lines
8.8 KiB
JavaScript
Executable file
322 lines
8.8 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
const assert = require("assert");
|
|
const cp = require("child_process");
|
|
const crypto = require("crypto");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
|
|
const repo = path.resolve(__dirname, "..");
|
|
const manifestPath = path.resolve(
|
|
process.env.CLUSTERFLUX_PUBLIC_RELEASE_MANIFEST ||
|
|
path.join(
|
|
process.env.CLUSTERFLUX_PUBLIC_RELEASE_DIR ||
|
|
path.join(repo, "target/release-candidate"),
|
|
"public-release-manifest.json"
|
|
)
|
|
);
|
|
const evidencePath = path.resolve(
|
|
process.env.CLUSTERFLUX_QUALITY_GATE_EVIDENCE_PATH ||
|
|
path.join(repo, "target/acceptance/quality-gates.json")
|
|
);
|
|
const transcriptPath = path.resolve(
|
|
process.env.CLUSTERFLUX_QUALITY_GATE_TRANSCRIPT_PATH ||
|
|
path.join(repo, "target/acceptance/quality-gates-transcript.txt")
|
|
);
|
|
|
|
function readJson(file) {
|
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
}
|
|
|
|
function sha256(file) {
|
|
return crypto
|
|
.createHash("sha256")
|
|
.update(fs.readFileSync(file))
|
|
.digest("hex");
|
|
}
|
|
|
|
function commandText(command, args) {
|
|
return [command, ...args]
|
|
.map((value) =>
|
|
/^[A-Za-z0-9_./:=+-]+$/.test(value)
|
|
? value
|
|
: JSON.stringify(value)
|
|
)
|
|
.join(" ");
|
|
}
|
|
|
|
function main() {
|
|
assert(fs.existsSync(manifestPath), `missing release manifest ${manifestPath}`);
|
|
const manifest = readJson(manifestPath);
|
|
fs.mkdirSync(path.dirname(evidencePath), { recursive: true });
|
|
fs.mkdirSync(path.dirname(transcriptPath), { recursive: true });
|
|
const transcript = fs.openSync(transcriptPath, "w");
|
|
const evidence = {
|
|
kind: "clusterflux-release-quality-gates",
|
|
source_commit: manifest.source_commit,
|
|
source_tree_digest: manifest.source_tree_digest,
|
|
public_tree_identity: manifest.public_tree_identity,
|
|
checks: {},
|
|
};
|
|
|
|
const runGroup = (id, commands, extra = {}) => {
|
|
const startedAt = Date.now();
|
|
const commandEvidence = [];
|
|
let status = "passed";
|
|
for (const command of commands) {
|
|
const args = command.args || [];
|
|
const cwd = command.cwd || repo;
|
|
const text = commandText(command.command, args);
|
|
fs.writeSync(transcript, `\n[${id}] ${text}\n`);
|
|
const commandStartedAt = Date.now();
|
|
const result = cp.spawnSync(command.command, args, {
|
|
cwd,
|
|
env: { ...process.env, ...(command.env || {}) },
|
|
stdio: ["ignore", transcript, transcript],
|
|
});
|
|
const durationMs = Math.max(1, Date.now() - commandStartedAt);
|
|
commandEvidence.push({
|
|
command: text,
|
|
cwd,
|
|
exit_status: result.status,
|
|
duration_ms: durationMs,
|
|
});
|
|
if (result.error || result.status !== 0) {
|
|
status = "failed";
|
|
evidence.checks[id] = {
|
|
status,
|
|
duration_ms: Math.max(1, Date.now() - startedAt),
|
|
commands: commandEvidence,
|
|
error: result.error?.message || `command exited ${result.status}`,
|
|
...extra,
|
|
};
|
|
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
throw new Error(`${id} failed: ${text}`);
|
|
}
|
|
}
|
|
evidence.checks[id] = {
|
|
status,
|
|
duration_ms: Math.max(1, Date.now() - startedAt),
|
|
commands: commandEvidence,
|
|
...extra,
|
|
};
|
|
};
|
|
|
|
try {
|
|
runGroup("repository_structure", [
|
|
{ command: path.join(repo, "scripts/check-old-name.sh") },
|
|
{ command: "node", args: [path.join(repo, "scripts/check-docs.js")] },
|
|
{ command: path.join(repo, "scripts/check-code-size.sh") },
|
|
]);
|
|
runGroup("formatting", [
|
|
{ command: "cargo", args: ["fmt", "--all", "--check"] },
|
|
{
|
|
command: "cargo",
|
|
args: [
|
|
"fmt",
|
|
"--all",
|
|
"--manifest-path",
|
|
"private/hosted-policy/Cargo.toml",
|
|
"--check",
|
|
],
|
|
},
|
|
]);
|
|
runGroup("clippy_warnings_denied", [
|
|
{
|
|
command: "cargo",
|
|
args: ["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"],
|
|
},
|
|
{
|
|
command: "cargo",
|
|
args: [
|
|
"clippy",
|
|
"--manifest-path",
|
|
"private/hosted-policy/Cargo.toml",
|
|
"--all-targets",
|
|
"--",
|
|
"-D",
|
|
"warnings",
|
|
],
|
|
},
|
|
]);
|
|
runGroup("public_workspace_tests", [
|
|
{
|
|
command: "cargo",
|
|
args: ["test", "--workspace", "--all-targets"],
|
|
},
|
|
{
|
|
command: "cargo",
|
|
args: ["build", "--workspace", "--all-targets"],
|
|
},
|
|
]);
|
|
runGroup("private_hosted_policy_locked_tests", [
|
|
{
|
|
command: "cargo",
|
|
args: [
|
|
"test",
|
|
"--locked",
|
|
"--manifest-path",
|
|
"private/hosted-policy/Cargo.toml",
|
|
"--all-targets",
|
|
],
|
|
},
|
|
]);
|
|
runGroup("process_lifecycle_regressions", [
|
|
{
|
|
command: "cargo",
|
|
args: [
|
|
"test",
|
|
"-p",
|
|
"clusterflux-coordinator",
|
|
"completed_main_waits_for_final_child_then_preserves_history_and_releases_the_slot",
|
|
],
|
|
},
|
|
{
|
|
command: "cargo",
|
|
args: [
|
|
"test",
|
|
"-p",
|
|
"clusterflux-coordinator",
|
|
"failed_main_aborts_unfinished_children_and_clears_process_debug_state",
|
|
],
|
|
},
|
|
{
|
|
command: "cargo",
|
|
args: [
|
|
"test",
|
|
"-p",
|
|
"clusterflux-coordinator",
|
|
"service_cancels_whole_process_and_blocks_new_task_launches",
|
|
],
|
|
},
|
|
]);
|
|
runGroup("wasm_example_builds", [
|
|
{
|
|
command: "cargo",
|
|
args: [
|
|
"build",
|
|
"-p",
|
|
"hello-build",
|
|
"--target",
|
|
"wasm32-unknown-unknown",
|
|
],
|
|
},
|
|
{
|
|
command: "cargo",
|
|
args: [
|
|
"build",
|
|
"-p",
|
|
"recovery-build",
|
|
"--target",
|
|
"wasm32-unknown-unknown",
|
|
],
|
|
},
|
|
{
|
|
command: "cargo",
|
|
args: [
|
|
"build",
|
|
"-p",
|
|
"runtime-conformance",
|
|
"--target",
|
|
"wasm32-unknown-unknown",
|
|
],
|
|
},
|
|
]);
|
|
runGroup("filtered_public_tree_build_and_tests", [
|
|
{ command: path.join(repo, "scripts/verify-public-split.sh") },
|
|
]);
|
|
|
|
const extensionAsset = manifest.assets.find((asset) =>
|
|
asset.name.endsWith(".vsix")
|
|
);
|
|
assert(extensionAsset, "candidate manifest omitted its VSIX");
|
|
const extensionFile = path.resolve(
|
|
path.dirname(manifestPath),
|
|
extensionAsset.file
|
|
);
|
|
assert(fs.existsSync(extensionFile), `missing candidate VSIX ${extensionFile}`);
|
|
const extensionDigest = sha256(extensionFile);
|
|
assert.strictEqual(extensionDigest, extensionAsset.sha256);
|
|
assert.strictEqual(
|
|
extensionDigest,
|
|
manifest.release_candidate.extension_sha256
|
|
);
|
|
const extractedVsix = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), "clusterflux-tested-vsix-")
|
|
);
|
|
try {
|
|
runGroup(
|
|
"vscode_extension_candidate",
|
|
[
|
|
{
|
|
command: "npm",
|
|
args: ["ci", "--ignore-scripts"],
|
|
cwd: path.join(repo, "vscode-extension"),
|
|
},
|
|
{
|
|
command: "unzip",
|
|
args: ["-q", extensionFile, "-d", extractedVsix],
|
|
},
|
|
{
|
|
command: "node",
|
|
args: [path.join(repo, "scripts/vscode-extension-smoke.js")],
|
|
env: {
|
|
CLUSTERFLUX_VSCODE_EXTENSION_ROOT: path.join(
|
|
extractedVsix,
|
|
"extension"
|
|
),
|
|
},
|
|
},
|
|
],
|
|
{
|
|
candidate_vsix: {
|
|
file: extensionFile,
|
|
sha256: extensionDigest,
|
|
},
|
|
}
|
|
);
|
|
} finally {
|
|
fs.rmSync(extractedVsix, { recursive: true, force: true });
|
|
}
|
|
|
|
const publicCheckIds = [
|
|
"repository_structure",
|
|
"formatting",
|
|
"clippy_warnings_denied",
|
|
"public_workspace_tests",
|
|
"process_lifecycle_regressions",
|
|
"wasm_example_builds",
|
|
"filtered_public_tree_build_and_tests",
|
|
"vscode_extension_candidate",
|
|
];
|
|
evidence.public = {
|
|
status: "passed",
|
|
duration_ms: publicCheckIds.reduce(
|
|
(total, id) => total + evidence.checks[id].duration_ms,
|
|
0
|
|
),
|
|
independent_filtered_tree: true,
|
|
};
|
|
evidence.private = {
|
|
status: "passed",
|
|
duration_ms:
|
|
evidence.checks.formatting.duration_ms +
|
|
evidence.checks.clippy_warnings_denied.duration_ms +
|
|
evidence.checks.private_hosted_policy_locked_tests.duration_ms,
|
|
};
|
|
evidence.status = "passed";
|
|
evidence.transcript = transcriptPath;
|
|
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
} finally {
|
|
fs.closeSync(transcript);
|
|
}
|
|
console.log(`Release quality gates passed: ${evidencePath}`);
|
|
}
|
|
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.error(error.stack || error.message);
|
|
process.exit(1);
|
|
}
|