184 lines
5.9 KiB
JavaScript
184 lines
5.9 KiB
JavaScript
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const baseUrl = process.env.PROJECTBRAIN_SMOKE_URL ?? "http://127.0.0.1:5173";
|
|
const projectId = process.env.PROJECTBRAIN_SMOKE_PROJECT ?? "widefrog";
|
|
const shouldRunSmokes = process.argv.includes("--run-smokes") || process.env.PROJECTBRAIN_PROOF_RUN_SMOKES === "1";
|
|
const hasExplicitBaseUrl = Boolean(process.env.PROJECTBRAIN_SMOKE_URL);
|
|
const shouldWriteReport = process.argv.includes("--write") || process.env.PROJECTBRAIN_PROOF_WRITE === "1";
|
|
const explicitOutputArg = process.argv.find((arg) => arg.startsWith("--out="));
|
|
const explicitOutputPath = explicitOutputArg?.slice("--out=".length) || process.env.PROJECTBRAIN_PROOF_OUT;
|
|
|
|
const evidenceRows = [
|
|
{
|
|
id: "RC63",
|
|
label: "Release evidence lane click-through",
|
|
script: "smoke:rc63-release-evidence-lane",
|
|
proves: "Release evidence routes are reachable from the operator lane.",
|
|
},
|
|
{
|
|
id: "RC60",
|
|
label: "Review decision receipt recovery",
|
|
script: "smoke:rc60-review-decision-loop",
|
|
proves: "Review decisions can be recovered and inspected from history.",
|
|
},
|
|
{
|
|
id: "RC73",
|
|
label: "Release evidence capture assist",
|
|
script: "smoke:rc73-release-evidence-capture-assist",
|
|
proves: "Runtime evidence capture assists the operator instead of leaving proof manual-only.",
|
|
},
|
|
{
|
|
id: "RC88",
|
|
label: "Release Center cockpit",
|
|
script: "smoke:rc88-release-center",
|
|
proves: "Release Center renders the cockpit, archives, quality and operator actions.",
|
|
},
|
|
{
|
|
id: "RC99",
|
|
label: "Release checklist click-through",
|
|
script: "smoke:rc99-release-checklist-clickthrough",
|
|
proves: "Checklist links land on exact target sections and expose return paths.",
|
|
},
|
|
];
|
|
|
|
const run = (command, args, options = {}) => spawnSync(command, args, {
|
|
encoding: "utf8",
|
|
shell: process.platform === "win32",
|
|
...options,
|
|
});
|
|
|
|
const readPackage = () => {
|
|
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
|
return packageJson.scripts ?? {};
|
|
};
|
|
|
|
const getGitCommit = () => {
|
|
const result = run("git", ["rev-parse", "--short", "HEAD"]);
|
|
return result.status === 0 ? result.stdout.trim() : "unknown";
|
|
};
|
|
|
|
const createDefaultReportPath = (commit) => {
|
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
return `checkpoints/release-proof/projectbrain-release-proof-${commit}-${stamp}.md`;
|
|
};
|
|
|
|
const checkLocalConfiguration = () => {
|
|
const scripts = readPackage();
|
|
const missing = [];
|
|
|
|
if (!scripts["smoke:release-operator"]) missing.push("smoke:release-operator");
|
|
if (!scripts["release:proof-report"]) missing.push("release:proof-report");
|
|
for (const row of evidenceRows) {
|
|
if (!scripts[row.script]) missing.push(row.script);
|
|
}
|
|
|
|
return missing;
|
|
};
|
|
|
|
const checkLiveHealth = async () => {
|
|
try {
|
|
const response = await fetch(`${baseUrl}/api/health`);
|
|
const body = await response.text();
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
body: body.slice(0, 220),
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
status: "unreachable",
|
|
body: error instanceof Error ? error.message : String(error),
|
|
};
|
|
}
|
|
};
|
|
|
|
const runOperatorSmoke = () => {
|
|
const result = run("pnpm", ["run", "smoke:release-operator"], {
|
|
stdio: "inherit",
|
|
env: process.env,
|
|
});
|
|
|
|
return result.status === 0;
|
|
};
|
|
|
|
const formatTable = (rows) => {
|
|
const header = "| Gate | Smoke | Proof |\n| --- | --- | --- |";
|
|
const body = rows.map((row) => `| ${row.id} | \`${row.script}\` | ${row.proves} |`).join("\n");
|
|
return `${header}\n${body}`;
|
|
};
|
|
|
|
const localMissing = checkLocalConfiguration();
|
|
let smokePassed = null;
|
|
|
|
if (shouldRunSmokes) {
|
|
console.log("Running release operator smoke suite before generating proof report...\n");
|
|
smokePassed = runOperatorSmoke();
|
|
}
|
|
|
|
const liveHealth = await checkLiveHealth();
|
|
const gitCommit = getGitCommit();
|
|
const localConfigOk = localMissing.length === 0;
|
|
const healthGateOk = liveHealth.ok || !hasExplicitBaseUrl;
|
|
const verdict = localConfigOk && healthGateOk && (smokePassed !== false)
|
|
? (smokePassed === true ? "PASS" : "CONFIGURED - SMOKE NOT RUN")
|
|
: "ATTENTION REQUIRED";
|
|
const reportPath = explicitOutputPath || createDefaultReportPath(gitCommit);
|
|
|
|
const report = [
|
|
"# ProjectBrain Release Operator Proof Report",
|
|
"",
|
|
`Generated: ${new Date().toISOString()}`,
|
|
`Commit: ${gitCommit}`,
|
|
`Base URL: ${baseUrl}`,
|
|
`Project: ${projectId}`,
|
|
`Verdict: ${verdict}`,
|
|
"",
|
|
"## Live Health",
|
|
"",
|
|
`- Status: ${liveHealth.status}`,
|
|
`- Result: ${liveHealth.ok ? "reachable" : hasExplicitBaseUrl ? "not reachable" : "not reachable in local default mode"}`,
|
|
`- Body: ${liveHealth.body}`,
|
|
"",
|
|
"## Proof Coverage",
|
|
"",
|
|
formatTable(evidenceRows),
|
|
"",
|
|
"## Local Configuration",
|
|
"",
|
|
localConfigOk
|
|
? "- Required release smoke scripts are registered."
|
|
: `- Missing scripts: ${localMissing.map((name) => `\`${name}\``).join(", ")}`,
|
|
`- Smoke execution: ${smokePassed === true ? "passed" : smokePassed === false ? "failed" : "not run"}`,
|
|
"",
|
|
"## Operator Commands",
|
|
"",
|
|
"```bash",
|
|
"pnpm run release:proof-report",
|
|
"pnpm run release:proof-report -- --write",
|
|
"PROJECTBRAIN_PROOF_RUN_SMOKES=1 pnpm run release:proof-report",
|
|
"PROJECTBRAIN_PROOF_RUN_SMOKES=1 PROJECTBRAIN_PROOF_WRITE=1 pnpm run release:proof-report",
|
|
"PROJECTBRAIN_SMOKE_URL=http://127.0.0.1:1209 pnpm run smoke:release-operator",
|
|
"```",
|
|
"",
|
|
"## Remaining Manual Gates",
|
|
"",
|
|
"- Confirm the intended release commit/tag.",
|
|
"- Confirm any environment-specific deployment notes.",
|
|
"- Confirm operator signoff after the smoke suite is green.",
|
|
].join("\n");
|
|
|
|
console.log(report);
|
|
|
|
if (shouldWriteReport) {
|
|
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
fs.writeFileSync(reportPath, `${report}\n`, "utf8");
|
|
console.log(`\nRelease proof report written to ${reportPath}`);
|
|
}
|
|
|
|
if (!localConfigOk || !healthGateOk || smokePassed === false) {
|
|
process.exitCode = 1;
|
|
}
|