Public release release-e47f9c27bbeb
Source commit: e47f9c27bbebe6759f6b6fe7b12fd00bb20d116d Public tree identity: sha256:4c1af1dfd67d3f2b5088531ea8727b11124b013d2251a4d5088f51a6424c0196
This commit is contained in:
parent
3a4d4fa7ae
commit
9223c54939
30 changed files with 4195 additions and 434 deletions
322
scripts/release-quality-gates.js
Executable file
322
scripts/release-quality-gates.js
Executable file
|
|
@ -0,0 +1,322 @@
|
|||
#!/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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue