Files
2026-07-13 13:39:12 +08:00

171 lines
6.4 KiB
JavaScript

#!/usr/bin/env node
// scripts/quality/run-all-gates.mjs
// Agregador paralelo de quality gates determinísticos.
// Roda os gates filesystem-only em paralelo (pool ~4), agrega resultados, e exibe
// uma tabela consolidada com {gate, status, durationMs}. Sai com código 1 se
// qualquer gate falhar. Alvo: < 3 min no total.
//
// Usage:
// node scripts/quality/run-all-gates.mjs # run all gates
// node scripts/quality/run-all-gates.mjs --fast # skip slow gates (duplication)
//
// Via npm: npm run quality:scan
import { spawn } from "node:child_process";
import { pathToFileURL } from "node:url";
const FAST_ONLY = process.argv.includes("--fast");
// Gates determinísticos filesystem-only, agrupados por tempo estimado.
// Cada entrada: { name: string, cmd: string[], label?: string }
// "slow" gates são omitidos com --fast.
const GATES = [
// Group A — instant (<1s)
{ name: "check:tracked-artifacts", cmd: ["node", "scripts/check/check-tracked-artifacts.mjs"] },
{ name: "check:any-budget:t11", cmd: ["node", "scripts/check/check-t11-any-budget.mjs"] },
{ name: "check:migration-numbering", cmd: ["node", "scripts/check/check-migration-numbering.mjs"] },
{ name: "check:node-runtime", cmd: ["node", "--import", "tsx", "scripts/check/check-supported-node-runtime.ts"] },
// Group B — fast (<5s)
{ name: "check:provider-consistency", cmd: ["node", "--import", "tsx", "scripts/check/check-provider-consistency.ts"] },
{ name: "check:provider-assets", cmd: ["node", "scripts/check/check-provider-assets.mjs"] },
{ name: "check:public-creds", cmd: ["node", "scripts/check/check-public-creds.mjs"] },
{ name: "check:error-helper", cmd: ["node", "scripts/check/check-error-helper.mjs"] },
{ name: "check:fetch-targets", cmd: ["node", "scripts/check/check-fetch-targets.mjs"] },
{ name: "check:openapi-routes", cmd: ["node", "scripts/check/check-openapi-routes.mjs"] },
{ name: "check:deps", cmd: ["node", "scripts/check/check-deps.mjs"] },
// Group C — moderate (<15s)
{ name: "check:db-rules", cmd: ["node", "scripts/check/check-db-rules.mjs"] },
{ name: "check:file-size", cmd: ["node", "scripts/check/check-file-size.mjs"] },
{ name: "check:complexity", cmd: ["node", "scripts/check/check-complexity.mjs"] },
{ name: "check:docs-symbols", cmd: ["node", "scripts/check/check-docs-symbols.mjs"] },
{ name: "check:known-symbols", cmd: ["node", "--import", "tsx", "scripts/check/check-known-symbols.ts"] },
{ name: "check:route-guard-membership", cmd: ["node", "--import", "tsx", "scripts/check/check-route-guard-membership.ts"] },
{ name: "check:test-discovery", cmd: ["node", "scripts/check/check-test-discovery.mjs"] },
{ name: "check:test-masking", cmd: ["node", "scripts/check/check-test-masking.mjs"] },
// Group D — slow (>15s); skipped with --fast
{ name: "check:duplication", cmd: ["node", "scripts/check/check-duplication.mjs"], slow: true },
{ name: "check:cycles", cmd: ["node", "scripts/check/check-cycles.mjs"], slow: true },
];
const CONCURRENCY = 4;
/**
* Run a single gate command, capturing last line of stdout/stderr.
* @param {{ name: string, cmd: string[] }} gate
* @returns {Promise<{ name: string, exitCode: number, durationMs: number, lastLine: string }>}
*/
function runGate(gate) {
return new Promise((resolve) => {
const start = Date.now();
const [bin, ...args] = gate.cmd;
const proc = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"], cwd: process.cwd() });
const lines = [];
const collectLine = (chunk) => {
const text = chunk.toString();
for (const line of text.split("\n")) {
const t = line.trim();
if (t) lines.push(t);
}
};
proc.stdout.on("data", collectLine);
proc.stderr.on("data", collectLine);
proc.on("close", (code) => {
const durationMs = Date.now() - start;
const lastLine = lines[lines.length - 1] ?? "";
resolve({ name: gate.name, exitCode: code ?? 1, durationMs, lastLine });
});
proc.on("error", (err) => {
const durationMs = Date.now() - start;
resolve({ name: gate.name, exitCode: 1, durationMs, lastLine: err.message });
});
});
}
/**
* Run gates with a concurrency pool.
* @param {typeof GATES} gates
* @param {number} concurrency
* @returns {Promise<ReturnType<typeof runGate>[]>}
*/
async function runWithPool(gates, concurrency) {
const results = [];
let idx = 0;
async function worker() {
while (idx < gates.length) {
const gate = gates[idx++];
const result = await runGate(gate);
results.push(result);
}
}
const workers = Array.from({ length: Math.min(concurrency, gates.length) }, () => worker());
await Promise.all(workers);
return results;
}
function formatTable(results) {
const COL_NAME = 35;
const COL_STATUS = 8;
const COL_MS = 10;
const COL_MSG = 60;
const header =
" " +
"GATE".padEnd(COL_NAME) +
"STATUS".padEnd(COL_STATUS) +
"TIME(ms)".padEnd(COL_MS) +
"LAST LINE";
const separator = " " + "─".repeat(COL_NAME + COL_STATUS + COL_MS + COL_MSG);
const rows = results.map((r) => {
const status = r.exitCode === 0 ? "PASS" : "FAIL";
const name = r.name.padEnd(COL_NAME);
const statusCol = (r.exitCode === 0 ? "✔ " + status : "✗ " + status).padEnd(COL_STATUS + 2);
const ms = String(r.durationMs).padStart(6) + "ms ";
const msg = r.lastLine.slice(0, COL_MSG);
return ` ${name}${statusCol}${ms}${msg}`;
});
return [separator, header, separator, ...rows, separator].join("\n");
}
async function main() {
const gates = FAST_ONLY ? GATES.filter((g) => !g.slow) : GATES;
console.log(`\n[quality:scan] Running ${gates.length} gate(s) with concurrency=${CONCURRENCY}...\n`);
const wallStart = Date.now();
const results = await runWithPool(gates, CONCURRENCY);
const wallMs = Date.now() - wallStart;
const failed = results.filter((r) => r.exitCode !== 0);
const passed = results.filter((r) => r.exitCode === 0);
console.log(formatTable(results));
console.log(
`\n Summary: ${passed.length} passed, ${failed.length} failed` +
` | Total: ${(wallMs / 1000).toFixed(1)}s (wall clock)\n`
);
if (failed.length > 0) {
console.error(`[quality:scan] FAIL — ${failed.length} gate(s) failed:`);
for (const r of failed) {
console.error(` ✗ ${r.name}`);
}
process.exit(1);
}
console.log("[quality:scan] All gates passed.");
}
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();