Files
projectbrain-Public/scripts/release-evidence-dossier.mjs
T

153 lines
5.2 KiB
JavaScript

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
const proofDir = process.env.PROJECTBRAIN_PROOF_ARCHIVE_DIR ?? "checkpoints/release-proof";
const signoffDir = process.env.PROJECTBRAIN_SIGNOFF_ARCHIVE_DIR ?? "checkpoints/release-signoff";
const deploymentProofDir = process.env.PROJECTBRAIN_DEPLOY_PROOF_ARCHIVE_DIR ?? "checkpoints/deployment-proof";
const dossierOut = process.env.PROJECTBRAIN_RELEASE_DOSSIER_OUT;
const run = (command, args) => spawnSync(command, args, {
encoding: "utf8",
shell: process.platform === "win32",
});
const getGitCommit = () => {
const result = run("git", ["rev-parse", "--short", "HEAD"]);
return result.status === 0 ? result.stdout.trim() : "unknown";
};
const parseField = (text, label) => {
const match = text.match(new RegExp(`^${label}:\\s*(.+)$`, "m"));
return match?.[1]?.trim() ?? "unknown";
};
const displayPath = (filePath) => filePath.split(path.sep).join("/");
const listMarkdownFiles = (archiveDir) => {
if (!fs.existsSync(archiveDir)) return [];
return fs.readdirSync(archiveDir)
.filter((fileName) => fileName.endsWith(".md"))
.map((fileName) => path.join(archiveDir, fileName))
.filter((filePath) => fs.statSync(filePath).isFile());
};
const sortNewestFirst = (items) => items.sort((a, b) => {
const generatedDelta = Date.parse(b.generated) - Date.parse(a.generated);
if (Number.isFinite(generatedDelta) && generatedDelta !== 0) return generatedDelta;
return b.fileName.localeCompare(a.fileName);
});
const parseArtifact = (filePath, type) => {
const text = fs.readFileSync(filePath, "utf8");
const generated = parseField(text, "Generated");
const commit = type === "deployment"
? parseField(text, "Local commit")
: parseField(text, "Commit");
return {
type,
text,
filePath,
displayPath: displayPath(filePath),
fileName: path.basename(filePath),
generated,
commit,
verdict: parseField(text, "Verdict"),
freshness: type === "deployment"
? parseField(text, "Commit freshness")
: parseField(text, "Freshness"),
};
};
const findLatestCurrentArtifact = (archiveDir, type, currentCommit) => {
const artifacts = sortNewestFirst(listMarkdownFiles(archiveDir).map((filePath) => parseArtifact(filePath, type)));
return artifacts.find((artifact) => {
if (artifact.commit !== currentCommit) return false;
if (type === "proof") return artifact.verdict === "PASS";
if (type === "signoff") return artifact.freshness === "current";
if (type === "deployment") return artifact.verdict === "PASS" && artifact.freshness === "current";
return false;
});
};
const indentArtifact = (artifact) => artifact.text
.split("\n")
.map((line) => line ? `> ${line}` : ">")
.join("\n");
const createDefaultDossierPath = (commit) => {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
return `checkpoints/release-dossier/projectbrain-release-dossier-${commit}-${stamp}.md`;
};
const currentCommit = getGitCommit();
const proof = findLatestCurrentArtifact(proofDir, "proof", currentCommit);
const signoff = findLatestCurrentArtifact(signoffDir, "signoff", currentCommit);
const deployment = findLatestCurrentArtifact(deploymentProofDir, "deployment", currentCommit);
const missing = [
proof ? null : "current passing release proof",
signoff ? null : "current release signoff",
deployment ? null : "current passing deployment proof",
].filter(Boolean);
if (missing.length) {
console.error("Cannot create release evidence dossier: missing required current artifacts.");
for (const item of missing) console.error(`- ${item}`);
console.error("Run: PROJECTBRAIN_RELEASE_DEPLOYMENT_PROOF=1 pnpm run release:bundle");
process.exit(1);
}
const dossierPath = dossierOut || createDefaultDossierPath(currentCommit);
const dossier = [
"# ProjectBrain Release Evidence Dossier",
"",
`Generated: ${new Date().toISOString()}`,
`Commit: ${currentCommit}`,
`Verdict: PASS`,
"",
"## Included Artifacts",
"",
"| Artifact | Generated | Source |",
"| --- | --- | --- |",
`| Release proof | ${proof.generated} | ${proof.displayPath} |`,
`| Operator signoff | ${signoff.generated} | ${signoff.displayPath} |`,
`| Deployment proof | ${deployment.generated} | ${deployment.displayPath} |`,
"",
"## Operator Summary",
"",
"- Release operator smoke suite passed for the current commit.",
"- Release proof freshness is current.",
"- Operator signoff file exists for the current commit.",
"- Remote deployment proof passed for the current commit.",
"- Tower health and ProjectBrain containers were proven by the deployment proof.",
"",
"## Release Proof",
"",
indentArtifact(proof),
"",
"## Operator Signoff",
"",
indentArtifact(signoff),
"",
"## Remote Deployment Proof",
"",
indentArtifact(deployment),
"",
"## Tower Commands",
"",
"```bash",
"cd /mnt/pool/appdata/brain",
"docker compose build --no-cache frontend",
"docker compose up -d",
"```",
].join("\n");
fs.mkdirSync(path.dirname(dossierPath), { recursive: true });
fs.writeFileSync(dossierPath, `${dossier}\n`, "utf8");
console.log(dossier);
console.log(`\nRelease evidence dossier written to ${dossierPath}`);