import { execFileSync, spawnSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(SCRIPT_DIR, ".."); const BASELINE_PATH = path.join(SCRIPT_DIR, "governance-baseline.json"); const MAX_FILE_LINES = 500; const MAINTAINABILITY_RULES = [ 'complexity: ["error", { "max": 20 }]', 'max-lines-per-function: ["error", { "skipComments": true, "max": 150 }]', 'max-params: ["error", 4]', 'max-depth: ["error", 4]', ]; const GOVERNED_ROOTS = ["src/", "config/"]; const EXCLUDE_DIR = /(^|\/)(mocks|test|__mocks__|node_modules|dist|coverage|e2e)\//; const EXCLUDE_NAME = /\.(mock|test|spec)\.(ts|tsx)$|\.d\.ts$/; // Repository-level gates that run after the source gates. Each is an npm // script so it can also be run on its own. const REPOSITORY_GATES = [ ["Terraform import-plan contract", "test:terraform-import-plan"], ["Terraform release-plan contract", "test:terraform-release-plan"], ["Terraform isolation gate", "test:terraform-isolation"], ["Terraform formatting and validation", "test:terraform"], ["HCP run guard", "test:hcp-run-guard"], ["CloudFront release verify", "test:cloudfront-release-verify"], ["GitHub workflow shell", "test:github-workflows"], ]; function isGoverned(relativePath) { return ( GOVERNED_ROOTS.some((root) => relativePath.startsWith(root)) && /\.(ts|tsx)$/.test(relativePath) && !EXCLUDE_DIR.test(relativePath) && !EXCLUDE_NAME.test(relativePath) ); } function gitText(args) { return execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }).trim(); } function gitLines(args) { return gitText(args).split("\n").filter(Boolean); } function governedFiles() { const tracked = gitLines(["ls-files"]); const untracked = gitLines(["ls-files", "--others", "--exclude-standard"]); return [...new Set([...tracked, ...untracked])].filter( (relativePath) => isGoverned(relativePath) && existsSync(path.join(ROOT, relativePath)), ); } function lineCount(relativePath) { const content = readFileSync(path.join(ROOT, relativePath), "utf8"); if (content.length === 0) return 0; return content.endsWith("\n") ? content.split("\n").length - 1 : content.split("\n").length; } function readBaseline() { return JSON.parse(readFileSync(BASELINE_PATH, "utf8")); } function readBaselineAtRef(ref) { try { const content = execFileSync("git", ["show", `${ref}:scripts/governance-baseline.json`], { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }); return JSON.parse(content); } catch { return null; } } function godfileRatchet(baseRef) { const baseline = readBaseline(); const cap = baseline.maxFileLines ?? MAX_FILE_LINES; const debtEntries = new Map( (baseline.godfileDebt ?? []).map((entry) => [entry.path, entry.maxLines]), ); const files = governedFiles(); const newDebt = []; const grownDebt = []; for (const file of files) { const lines = lineCount(file); const debtCap = debtEntries.get(file); if (lines > cap && debtCap === undefined) { newDebt.push({ path: file, lines }); } else if (debtCap !== undefined && lines > debtCap) { grownDebt.push({ path: file, lines, maxLines: debtCap }); } } const stale = []; const remaining = []; for (const [debtPath, maxLines] of debtEntries) { const lines = files.includes(debtPath) ? lineCount(debtPath) : -1; if (lines === -1 || lines <= cap) { stale.push({ path: debtPath, lines }); } else { remaining.push({ path: debtPath, lines, maxLines }); } } const baselineLoosening = []; const baseBaseline = baseRef ? readBaselineAtRef(baseRef) : null; if (baseBaseline) { const baseCap = baseBaseline.maxFileLines ?? MAX_FILE_LINES; if (cap > baseCap) { baselineLoosening.push(`global cap increased from ${baseCap} to ${cap}`); } const baseEntries = new Map( (baseBaseline.godfileDebt ?? []).map((entry) => [entry.path, entry.maxLines]), ); for (const [debtPath, maxLines] of debtEntries) { const priorMax = baseEntries.get(debtPath); if (priorMax === undefined) { baselineLoosening.push(`new debt entry: ${debtPath}`); } else if (maxLines > priorMax) { baselineLoosening.push(`cap increased for ${debtPath}: ${priorMax} -> ${maxLines}`); } } } return { cap, newDebt, grownDebt, stale, remaining, baselineLoosening, comparedBaseline: Boolean(baseBaseline), }; } function resolveBaseRef() { if (process.env.GOVERNANCE_BASE) return process.env.GOVERNANCE_BASE; if (process.env.GITHUB_BASE_REF) return `origin/${process.env.GITHUB_BASE_REF}`; for (const candidate of ["origin/dev", "origin/main"]) { try { execFileSync("git", ["rev-parse", "--verify", candidate], { cwd: ROOT, encoding: "utf8", stdio: "ignore", }); return candidate; } catch { // candidate ref not present locally; try the next } } return null; } function changedGovernedFiles(baseRef) { let mergeBase; try { mergeBase = execFileSync("git", ["merge-base", baseRef, "HEAD"], { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }).trim(); } catch { return null; } const diffed = gitLines(["diff", "--name-only", "--diff-filter=AMR", mergeBase, "HEAD"]); const untracked = gitLines(["ls-files", "--others", "--exclude-standard"]); return [...new Set([...diffed, ...untracked])].filter(isGoverned); } function maintainabilityGate(files) { if (files.length === 0) { return { skipped: true, reason: "no changed governed TS/TSX files" }; } // Invoke eslint via node so Windows (no shebang exec) and Unix both work. const eslintJs = path.join(ROOT, "node_modules", "eslint", "bin", "eslint.js"); const ruleArgs = MAINTAINABILITY_RULES.flatMap((rule) => ["--rule", rule]); const result = spawnSync( process.execPath, [ eslintJs, ...files, ...ruleArgs, "--max-warnings=0", "--no-warn-ignored", "--no-error-on-unmatched-pattern", ], { cwd: ROOT, encoding: "utf8" }, ); return { skipped: false, status: result.status, stdout: result.stdout?.trim() ?? "", stderr: result.stderr?.trim() ?? "", files, }; } function plural(count, word) { return `${count} ${word}${count === 1 ? "" : "s"}`; } function runRepositoryGate(label, script) { // Reuse the npm that launched us when available (matches its version and // config); fall back to PATH for direct `node scripts/governance-check.mjs`. const npmCli = process.env.npm_execpath; const executable = npmCli ? process.execPath : "npm"; const args = npmCli ? [npmCli, "run", script] : ["run", script]; const result = spawnSync(executable, args, { cwd: ROOT, encoding: "utf8", stdio: "inherit", }); return { label, status: result.status, error: result.error }; } function main() { const failures = []; const baseRef = resolveBaseRef(); if (!baseRef) { failures.push( "base ref is required but was not found. Set GOVERNANCE_BASE to a valid commit or fetch origin/dev.", ); } else { try { gitText(["merge-base", baseRef, "HEAD"]); } catch { failures.push( `base ref '${baseRef}' cannot be resolved against HEAD. Fetch it or set GOVERNANCE_BASE correctly.`, ); } } console.log("─".repeat(64)); console.log("godfile ratchet: legacy caps may only shrink"); const god = godfileRatchet(baseRef); console.log( ` cap: ${god.cap} lines | grandfathered debt: ${plural(god.remaining.length, "file")} | new violations: ${god.newDebt.length}`, ); for (const entry of god.remaining) { console.log(` debt ${String(entry.lines).padStart(4)}/${entry.maxLines} ${entry.path}`); } for (const entry of god.newDebt) { console.log(` NEW ${String(entry.lines).padStart(4)} ${entry.path}`); } for (const entry of god.grownDebt) { console.log(` GREW ${String(entry.lines).padStart(4)}/${entry.maxLines} ${entry.path}`); } if (god.newDebt.length > 0) { failures.push( `godfile ratchet: ${plural(god.newDebt.length, "file")} exceed ${god.cap} lines. Refactor them under the cap; new baseline debt is forbidden.`, ); } if (god.grownDebt.length > 0) { failures.push( `godfile ratchet: ${plural(god.grownDebt.length, "grandfathered file")} exceeded its frozen line cap.`, ); } if (god.baselineLoosening.length > 0) { failures.push( `governance baseline was loosened: ${god.baselineLoosening.join("; ")}. Only cap reductions and entry removals are allowed.`, ); } if (!god.comparedBaseline) { console.log(" baseline comparison unavailable (initial adoption or missing base file)"); } if (god.stale.length > 0) { console.log(` stale baseline entries (now compliant — remove to ratchet tighter):`); for (const entry of god.stale) { console.log(` stale ${entry.path}`); } } console.log("─".repeat(64)); if (!baseRef) { console.log("changed-file maintainability gate: FAIL (no valid base ref)"); } else { const files = changedGovernedFiles(baseRef); console.log( `changed-file maintainability gate (base: ${baseRef}): ${files === null ? "unresolvable" : plural(files.length, "changed governed file")}`, ); if (files === null) { console.log(" failed — base ref could not be resolved against HEAD"); } else { const gate = maintainabilityGate(files); if (gate.skipped) { console.log(` skipped — ${gate.reason}`); } else { const clean = gate.status === 0; console.log( ` result: ${clean ? "PASS" : "FAIL"} (complexity<=20, function<=150 lines, params<=4, depth<=4)`, ); if (!clean) { if (gate.stdout) console.log(gate.stdout); if (gate.stderr) console.log(gate.stderr); failures.push( "changed-file maintainability gate: see ESLint output above. Extract functions/components to meet the thresholds; do not relax the thresholds.", ); } } } } for (const [label, script] of REPOSITORY_GATES) { console.log("─".repeat(64)); console.log(`${label}: npm run ${script}`); const gate = runRepositoryGate(label, script); if (gate.error) { failures.push(`${label}: could not start: ${gate.error.message}`); } else if (gate.status !== 0) { failures.push(`${label}: failed with exit code ${gate.status ?? "unknown"}`); } } console.log("─".repeat(64)); if (failures.length > 0) { console.log(`RESULT: FAIL (${plural(failures.length, "gate")})`); for (const failure of failures) console.log(` - ${failure}`); process.exit(1); } console.log("RESULT: PASS — all governance gates green"); } main();