Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-api-auth-routing-baseline.mjs
T

174 lines
5.3 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..");
const reportPath = path.join(repoRoot, ".tc/baselines/api-auth-routing/baseline-health.md");
const authCentralResolverFile = "apps/web/app/utils/services/apiService.ts";
export const gates = [
{ id: "typecheck", command: "pnpm", args: ["run", "typecheck"] },
{ id: "lint", command: "pnpm", args: ["run", "lint"] },
{ id: "vitest-app", command: "pnpm", args: ["exec", "vitest", "run", "apps/web/app/"] },
{ id: "api-auth-surface", command: "node", args: ["scripts/verify-api-auth-surface.mjs", "--json"] },
];
function shellCommand({ command, args }) {
return [command, ...args].join(" ");
}
function outputFingerprint(output) {
const normalized = output
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.slice(-40)
.join("\n");
const hash = createHash("sha256").update(output).digest("hex").slice(0, 12);
return {
hash,
excerpt: normalized.slice(0, 900) || "(no output)",
};
}
function parseNumber(value) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) ? parsed : 0;
}
function countTypeScriptFailures(output) {
return (output.match(/error TS\d+/g) ?? []).length;
}
function countEslintFailures(output) {
const problemsMatch = output.match(/✖\s+(\d+)\s+problems?\s+\((\d+)\s+errors?/i);
if (problemsMatch) return parseNumber(problemsMatch[2]);
const compactMatch = output.match(/(\d+)\s+errors?/i);
if (compactMatch) return parseNumber(compactMatch[1]);
return 0;
}
function countVitestFailures(output) {
const failedTestMatch = output.match(/Tests\s+\d+\s+failed/i);
if (failedTestMatch) {
const countMatch = failedTestMatch[0].match(/Tests\s+(\d+)\s+failed/i);
return countMatch ? parseNumber(countMatch[1]) : 1;
}
const failedFilesMatch = output.match(/Test Files\s+(\d+)\s+failed/i);
return failedFilesMatch ? parseNumber(failedFilesMatch[1]) : 0;
}
function countAuthSurfaceFailures(stdout, output) {
try {
const inventory = JSON.parse(stdout);
return (inventory.directValidateApiKeyCallsites ?? []).filter((entry) => entry.file !== authCentralResolverFile).length;
} catch {
return output.trim() ? 1 : 0;
}
}
export function failureCount(gateId, stdout, stderr) {
const output = `${stdout}\n${stderr}`;
if (gateId === "typecheck") return countTypeScriptFailures(output);
if (gateId === "lint") return countEslintFailures(output);
if (gateId === "vitest-app") return countVitestFailures(output);
if (gateId === "api-auth-surface") return countAuthSurfaceFailures(stdout, output);
return 0;
}
function runGate(gate) {
return new Promise((resolve) => {
const startedAt = Date.now();
const child = spawn(gate.command, gate.args, {
cwd: repoRoot,
env: { ...process.env, CI: "1" },
shell: false,
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("error", (error) => {
stderr += `\n${error.message}`;
});
child.on("close", (code) => {
const output = `${stdout}\n${stderr}`;
resolve({
id: gate.id,
command: shellCommand(gate),
exitCode: code ?? 1,
durationMs: Date.now() - startedAt,
failureCount: failureCount(gate.id, stdout, stderr),
fingerprint: outputFingerprint(output),
});
});
});
}
export function renderReport(results) {
const lines = [
"# API Auth Routing Baseline Health",
"",
`Generated: ${new Date().toISOString()}`,
"",
"This report captures non-E2E baseline gates for API Auth Routing baseline. The runner exits 0 when every gate was executed and this report was written, even if individual gates fail.",
"",
];
for (const result of results) {
lines.push(
`## ${result.id}`,
"",
`- Command: \`${result.command}\``,
`- Exitcode: ${result.exitCode}`,
`- Failure Count: ${result.failureCount}`,
`- Duration: ${result.durationMs}ms`,
`- Output Fingerprint: ${result.fingerprint.hash}`,
"",
"```text",
result.fingerprint.excerpt,
"```",
"",
);
}
return `${lines.join("\n")}\n`;
}
async function main() {
const results = [];
for (const gate of gates) {
console.log(`Running ${gate.id}: ${shellCommand(gate)}`);
results.push(await runGate(gate));
}
await mkdir(path.dirname(reportPath), { recursive: true });
await writeFile(reportPath, renderReport(results), "utf8");
console.log(`Wrote ${path.relative(repoRoot, reportPath)}`);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch(async (error) => {
console.error(error);
try {
await mkdir(path.dirname(reportPath), { recursive: true });
await writeFile(reportPath, `# API Auth Routing Baseline Health\n\nRunner failed before report completion.\n\n\`\`\`text\n${error?.stack ?? error}\n\`\`\`\n`, "utf8");
} finally {
process.exit(1);
}
});
}