Files
projectbrain-Public/scripts/release-deployment-proof.mjs
T

152 lines
5.0 KiB
JavaScript

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { compareDeploymentCommits, isRunningContainer } from "./deployment-proof-state.mjs";
const baseUrl = process.env.PROJECTBRAIN_DEPLOY_URL ?? process.env.PROJECTBRAIN_SMOKE_URL ?? "http://127.0.0.1:1209";
const sshTarget = process.env.PROJECTBRAIN_DEPLOY_SSH_TARGET ?? "root@127.0.0.1";
const deployPath = process.env.PROJECTBRAIN_DEPLOY_PATH ?? "/mnt/pool/appdata/brain";
const sshKey = process.env.PROJECTBRAIN_DEPLOY_SSH_KEY ?? path.join(os.homedir(), ".ssh", "widefrog_unraid_deploy");
const shouldWrite = process.argv.includes("--write") || process.env.PROJECTBRAIN_DEPLOY_PROOF_WRITE === "1";
const explicitOutputArg = process.argv.find((arg) => arg.startsWith("--out="));
const explicitOutputPath = explicitOutputArg?.slice("--out=".length) || process.env.PROJECTBRAIN_DEPLOY_PROOF_OUT;
const run = (command, args) => spawnSync(command, args, {
encoding: "utf8",
shell: false,
timeout: 10_000,
});
const getGitCommit = () => {
const result = run("git", ["rev-parse", "--short", "HEAD"]);
return result.status === 0 ? result.stdout.trim() : "unknown";
};
const createDefaultProofPath = (commit) => {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
return `checkpoints/deployment-proof/projectbrain-deployment-proof-${commit}-${stamp}.md`;
};
const checkHttp = async (urlPath) => {
try {
const response = await fetch(`${baseUrl}${urlPath}`, { signal: AbortSignal.timeout(10_000) });
const body = await response.text();
return {
ok: response.ok,
status: String(response.status),
body: body.slice(0, 260).replace(/\s+/g, " ").trim(),
};
} catch (error) {
return {
ok: false,
status: "unreachable",
body: error instanceof Error ? error.message : String(error),
};
}
};
const runSsh = (remoteCommand) => {
if (!fs.existsSync(sshKey)) {
return {
ok: false,
status: "missing-key",
stdout: "",
stderr: `SSH key not found: ${sshKey}`,
};
}
const result = spawnSync("ssh", [
"-i",
sshKey,
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=8",
sshTarget,
remoteCommand,
], {
encoding: "utf8",
shell: false,
timeout: 20_000,
});
return {
ok: result.status === 0,
status: String(result.status),
stdout: result.stdout?.trim() ?? "",
stderr: result.stderr?.trim() || result.error?.message || "",
};
};
const currentCommit = getGitCommit();
const health = await checkHttp("/api/health");
const shellProof = runSsh([
`echo deployed_commit=$(git -C ${deployPath} rev-parse --short HEAD 2>/dev/null || echo unknown)`,
"echo containers_begin",
"docker ps --format '{{.Names}} {{.Status}} {{.Ports}}' | grep projectbrain || true",
"echo containers_end",
].join(" && "));
const deployedCommitMatch = shellProof.stdout.match(/^deployed_commit=(.+)$/m);
const deployedCommit = deployedCommitMatch?.[1]?.trim() ?? "unknown";
const containerBlock = shellProof.stdout.match(/containers_begin\n([\s\S]*?)\n?containers_end/)?.[1]?.trim() ?? "";
const hasFrontend = isRunningContainer(containerBlock, "projectbrain-frontend", "1209->80/tcp");
const hasApi = isRunningContainer(containerBlock, "projectbrain-api");
const hasPostgres = isRunningContainer(containerBlock, "projectbrain-postgres");
const commitFreshness = compareDeploymentCommits(currentCommit, deployedCommit);
const verdict = health.ok && shellProof.ok && commitFreshness === "current" && hasFrontend && hasApi && hasPostgres
? "PASS"
: "ATTENTION REQUIRED";
const proofPath = explicitOutputPath || createDefaultProofPath(currentCommit);
const lines = [
"# ProjectBrain Remote Deployment Proof",
"",
`Generated: ${new Date().toISOString()}`,
`Local commit: ${currentCommit}`,
`Deployment URL: ${baseUrl}`,
`SSH target: ${sshTarget}`,
`Deployment path: ${deployPath}`,
`Remote commit: ${deployedCommit}`,
`Commit freshness: ${commitFreshness}`,
`Verdict: ${verdict}`,
"",
"## HTTP Health",
"",
`- Status: ${health.status}`,
`- Result: ${health.ok ? "reachable" : "not reachable"}`,
`- Body: ${health.body}`,
"",
"## Remote Runtime",
"",
`- SSH result: ${shellProof.ok ? "reachable" : shellProof.status}`,
`- Frontend container: ${hasFrontend ? "up" : "not proven"}`,
`- API container: ${hasApi ? "up" : "not proven"}`,
`- Postgres container: ${hasPostgres ? "up" : "not proven"}`,
"",
"```text",
containerBlock || shellProof.stderr || "No remote container output captured.",
"```",
"",
"## Operator Commands",
"",
"```bash",
"PROJECTBRAIN_DEPLOY_PROOF_WRITE=1 pnpm run release:deployment-proof",
"PROJECTBRAIN_RELEASE_DEPLOYMENT_PROOF=1 pnpm run release:bundle",
"```",
].join("\n");
console.log(lines);
if (shouldWrite) {
fs.mkdirSync(path.dirname(proofPath), { recursive: true });
fs.writeFileSync(proofPath, `${lines}\n`, "utf8");
console.log(`\nRemote deployment proof written to ${proofPath}`);
}
if (verdict !== "PASS") {
process.exitCode = 1;
}