153 lines
5.2 KiB
JavaScript
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}`);
|