Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/coverage-summary.mjs
T

158 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
// Renders a Vitest coverage-summary.json into either a shields.io badge URL
// (default) or a sticky PR-comment markdown table (--pr-comment).
//
// Usage:
// node scripts/coverage-summary.mjs # prints shields.io URL
// node scripts/coverage-summary.mjs --pr-comment # prints markdown for PR
// node scripts/coverage-summary.mjs --json # prints raw totals
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const MARKER = "<!-- coverage-report -->";
const repoRoot = path.resolve(fileURLToPath(import.meta.url), "..", "..");
const targetsPath = path.join(repoRoot, "scripts", "coverage-targets.json");
export function resolveSummaryPath({ root = repoRoot, env = process.env, fsApi = fs } = {}) {
if (env.COVERAGE_SUMMARY_PATH) {
return path.resolve(root, env.COVERAGE_SUMMARY_PATH);
}
const candidates = [
path.join(root, "coverage-gate", "coverage-summary.json"),
path.join(root, "coverage", "coverage-summary.json"),
];
return candidates.find((candidate) => fsApi.existsSync(candidate)) ?? candidates[0];
}
function loadSummary() {
const summaryPath = resolveSummaryPath();
if (!fs.existsSync(summaryPath)) return null;
return JSON.parse(fs.readFileSync(summaryPath, "utf8"));
}
function loadTargets() {
if (!fs.existsSync(targetsPath)) return [];
return JSON.parse(fs.readFileSync(targetsPath, "utf8"));
}
function aggregateTotals(summary) {
const total = summary.total ?? null;
if (!total) return null;
return {
statements: total.statements?.pct ?? 0,
branches: total.branches?.pct ?? 0,
functions: total.functions?.pct ?? 0,
lines: total.lines?.pct ?? 0,
};
}
function pickColor(pct) {
if (pct >= 90) return "brightgreen";
if (pct >= 75) return "green";
if (pct >= 60) return "yellowgreen";
if (pct >= 50) return "yellow";
if (pct >= 40) return "orange";
return "red";
}
function shieldsUrl(totals) {
if (!totals) return "https://img.shields.io/badge/coverage-unknown-lightgrey";
const pct = totals.statements.toFixed(1);
return `https://img.shields.io/badge/coverage-${pct}%25-${pickColor(totals.statements)}`;
}
function evaluatePerPath(summary, targets) {
const rows = [];
for (const target of targets) {
const filtered = Object.entries(summary).filter(([file]) => {
if (file === "total") return false;
const rel = path.relative(repoRoot, file);
return target.pathPrefixes.some((prefix) => rel.startsWith(prefix));
});
if (filtered.length === 0) {
rows.push({ name: target.name, statementsPct: null, branchesPct: null, target, status: "skip" });
continue;
}
const totals = filtered.reduce(
(acc, [, m]) => ({
statements: acc.statements + m.statements.covered,
statementsTotal: acc.statementsTotal + m.statements.total,
branches: acc.branches + m.branches.covered,
branchesTotal: acc.branchesTotal + m.branches.total,
}),
{ statements: 0, statementsTotal: 0, branches: 0, branchesTotal: 0 }
);
const statementsPct = totals.statementsTotal > 0 ? (totals.statements / totals.statementsTotal) * 100 : 0;
const branchesPct = totals.branchesTotal > 0 ? (totals.branches / totals.branchesTotal) * 100 : 0;
const passes = statementsPct >= target.statements && branchesPct >= target.branches;
rows.push({
name: target.name,
statementsPct,
branchesPct,
target,
status: passes ? "pass" : "fail",
});
}
return rows;
}
function renderPrComment(totals, rows) {
const lines = [];
lines.push(MARKER);
lines.push("## Coverage Report");
lines.push("");
if (!totals) {
lines.push("_coverage-summary.json not found — vitest with --coverage did not run._");
return lines.join("\n");
}
lines.push("| metric | total |");
lines.push("|---|---|");
lines.push(`| statements | **${totals.statements.toFixed(1)}%** |`);
lines.push(`| branches | **${totals.branches.toFixed(1)}%** |`);
lines.push(`| functions | **${totals.functions.toFixed(1)}%** |`);
lines.push(`| lines | **${totals.lines.toFixed(1)}%** |`);
lines.push("");
lines.push("### Per-path thresholds");
lines.push("");
lines.push("| path | statements | branches | gate | status |");
lines.push("|---|---|---|---|---|");
for (const row of rows) {
const tag = row.target.criticalPath ? " (critical)" : "";
const statements = row.statementsPct === null ? "n/a" : `${row.statementsPct.toFixed(1)}%`;
const branches = row.branchesPct === null ? "n/a" : `${row.branchesPct.toFixed(1)}%`;
const gate = `${row.target.statements}% / ${row.target.branches}%`;
const icon = row.status === "pass" ? "PASS" : row.status === "fail" ? "FAIL" : "SKIP";
lines.push(`| ${row.name}${tag} | ${statements} | ${branches} | ${gate} | ${icon} |`);
}
lines.push("");
lines.push("_Bump thresholds by editing `scripts/coverage-targets.json`. See `docs/architecture/test-coverage.md`._");
return lines.join("\n");
}
export function main() {
const argv = process.argv.slice(2);
const summary = loadSummary();
const targets = loadTargets();
const totals = summary ? aggregateTotals(summary) : null;
if (argv.includes("--pr-comment")) {
const rows = summary ? evaluatePerPath(summary, targets) : [];
process.stdout.write(renderPrComment(totals, rows) + "\n");
return;
}
if (argv.includes("--json")) {
process.stdout.write(JSON.stringify(totals ?? {}, null, 2) + "\n");
return;
}
// Default: shields.io badge URL
process.stdout.write(shieldsUrl(totals) + "\n");
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}