266 lines
7.6 KiB
JavaScript
266 lines
7.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/* eslint-disable no-console */
|
|
import { createHash } from "node:crypto";
|
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { markFailedBaselineGates } from "./baseline-gate-exit.mjs";
|
|
import { spawn } from "node:child_process";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(__dirname, "..");
|
|
export const reportPath = path.join(repoRoot, ".tc/baselines/transactional-mail-provider/baseline-health.md");
|
|
|
|
const ignoredDirectoryNames = new Set([
|
|
".git",
|
|
"node_modules",
|
|
"build",
|
|
"dist",
|
|
".react-router",
|
|
".tc",
|
|
".cache",
|
|
".turbo",
|
|
".vite",
|
|
"coverage",
|
|
]);
|
|
|
|
const postmarkScannerSelfFileAllowlist = new Set([
|
|
"scripts/verify-transactional-mail-provider-baseline.mjs",
|
|
"scripts/verify-transactional-mail-provider-baseline.test.mjs",
|
|
]);
|
|
|
|
export const gates = [
|
|
{ id: "typecheck", command: "pnpm", args: ["run", "typecheck"] },
|
|
{ id: "lint", command: "pnpm", args: ["run", "lint"] },
|
|
{ id: "vitest-mail-runtime", command: "pnpm", args: ["exec", "vitest", "run", "packages/mail/src/mailersend.test.ts"] },
|
|
{ id: "postmark-scan", command: "internal", args: ["scan-postmark"] },
|
|
];
|
|
|
|
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 countPostmarkScanFailures(stdout, output) {
|
|
try {
|
|
const scan = JSON.parse(stdout);
|
|
return scan.matchCount ?? 0;
|
|
} 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-mail-runtime") return countVitestFailures(output);
|
|
if (gateId === "postmark-scan") return countPostmarkScanFailures(stdout, output);
|
|
return 0;
|
|
}
|
|
|
|
function isIgnoredDirectory(name) {
|
|
return ignoredDirectoryNames.has(name) || name.endsWith(".cache");
|
|
}
|
|
|
|
async function* walkFiles(root, directory = root) {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory() && isIgnoredDirectory(entry.name)) continue;
|
|
|
|
const absolutePath = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
yield* walkFiles(root, absolutePath);
|
|
} else if (entry.isFile()) {
|
|
yield absolutePath;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function scanPostmarkOccurrences(root = repoRoot) {
|
|
const matches = [];
|
|
for await (const filePath of walkFiles(root)) {
|
|
const relativePath = path.relative(root, filePath).split(path.sep).join("/");
|
|
if (postmarkScannerSelfFileAllowlist.has(relativePath)) continue;
|
|
|
|
let content;
|
|
try {
|
|
content = await readFile(filePath, "utf8");
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
const lines = content.split(/\r?\n/);
|
|
lines.forEach((line, index) => {
|
|
if (/postmark/i.test(line)) {
|
|
matches.push({
|
|
file: relativePath,
|
|
line: index + 1,
|
|
text: line.trim().slice(0, 220),
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
matches.sort((a, b) => `${a.file}:${a.line}`.localeCompare(`${b.file}:${b.line}`));
|
|
return {
|
|
matchCount: matches.length,
|
|
matches,
|
|
};
|
|
}
|
|
|
|
async function runPostmarkScanGate() {
|
|
const startedAt = Date.now();
|
|
const scan = await scanPostmarkOccurrences(repoRoot);
|
|
const stdout = `${JSON.stringify(scan, null, 2)}\n`;
|
|
return {
|
|
id: "postmark-scan",
|
|
command: "internal scan-postmark",
|
|
exitCode: 0,
|
|
durationMs: Date.now() - startedAt,
|
|
failureCount: failureCount("postmark-scan", stdout, ""),
|
|
fingerprint: outputFingerprint(stdout),
|
|
};
|
|
}
|
|
|
|
function runShellGate(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),
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function runGate(gate) {
|
|
if (gate.id === "postmark-scan") return runPostmarkScanGate();
|
|
return runShellGate(gate);
|
|
}
|
|
|
|
export function renderReport(results) {
|
|
const lines = [
|
|
"# Transactional Mail Provider Baseline Health",
|
|
"",
|
|
`Generated: ${new Date().toISOString()}`,
|
|
"",
|
|
"This report captures non-E2E baseline gates for Transactional Mail Provider 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)}`);
|
|
markFailedBaselineGates(results);
|
|
}
|
|
|
|
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,
|
|
`# Transactional Mail Provider Baseline Health\n\nRunner failed before report completion.\n\n\`\`\`text\n${error?.stack ?? error}\n\`\`\`\n`,
|
|
"utf8",
|
|
);
|
|
} finally {
|
|
process.exit(1);
|
|
}
|
|
});
|
|
}
|