Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
317 changes: 314 additions & 3 deletions action/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -229102,7 +229102,7 @@ var init_discover3 = __esm({
});

// src/cli.ts
import { readFileSync as readFileSync2 } from "node:fs";
import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
import { pathToFileURL } from "node:url";

// src/config/load.ts
Expand Down Expand Up @@ -233539,6 +233539,123 @@ function shouldFail(report, failOn) {
}
}

// src/report/compliance.ts
function buildComplianceReport(results, audit) {
const cycles = [];
const errored = [];
const deferred = [];
const modeSet = /* @__PURE__ */ new Set();
let drift = 0;
let guardrailTrips = 0;
let guardrailBlocked = 0;
let applied = 0;
let failed = 0;
for (const result of results) {
modeSet.add(result.mode);
for (const cr of result.cycles) {
const total = cr.counts.create + cr.counts.update + cr.counts.delete;
const tripped = cr.guardrails.ok ? [] : cr.guardrails.diagnostics.map((d) => d.guardrail);
cycles.push({
cycle: cr.name,
org: cr.org,
drift: { ...cr.counts, total },
guardrails: { ok: cr.guardrails.ok, blocked: cr.guardrailBlocked, tripped },
applied: cr.applied.length,
failed: cr.failed.length
});
drift += total;
if (!cr.guardrails.ok) guardrailTrips++;
if (cr.guardrailBlocked) guardrailBlocked++;
applied += cr.applied.length;
failed += cr.failed.length;
}
for (const ce of result.errored) {
errored.push({ cycle: ce.name, org: ce.org, stage: ce.stage, error: ce.error });
}
deferred.push(...result.deferred.skippedCycles);
}
let auditCompliance;
let auditMergeWorthy = 0;
if (audit) {
auditMergeWorthy = audit.totals.quickWin + audit.totals.needsReview;
auditCompliance = {
total: audit.totals.total,
quickWin: audit.totals.quickWin,
needsReview: audit.totals.needsReview,
reportOnly: audit.totals.reportOnly,
mergeWorthy: auditMergeWorthy
};
}
const clean = drift === 0 && guardrailTrips === 0 && failed === 0 && errored.length === 0 && deferred.length === 0 && auditMergeWorthy === 0;
return {
modes: [...modeSet],
cycles,
audit: auditCompliance,
totals: {
drift,
guardrailTrips,
guardrailBlocked,
applied,
failed,
cyclesReporting: cycles.length,
auditMergeWorthy
},
errored,
deferred,
clean
};
}
function renderComplianceReport(report) {
const lines = [];
lines.push("=== compliance report ===");
if (report.generatedAt) lines.push(`generated: ${report.generatedAt}`);
if (report.modes.length > 0) lines.push(`modes: ${report.modes.join(", ")}`);
lines.push("");
if (report.cycles.length === 0) {
lines.push(" (no cycle results)");
} else {
for (const c of report.cycles) {
const d = c.drift;
let line = ` ${c.cycle}@${c.org} drift=${d.total} (c${d.create}/u${d.update}/d${d.delete}) applied=${c.applied} failed=${c.failed}`;
if (c.guardrails.blocked) line += ` GUARDRAIL-BLOCKED[${c.guardrails.tripped.join(",")}]`;
else if (!c.guardrails.ok) line += ` guardrail-trip[${c.guardrails.tripped.join(",")}]`;
lines.push(line);
}
}
if (report.errored.length > 0) {
lines.push("");
lines.push("--- errored cycles ---");
for (const e of report.errored) {
lines.push(` ${e.cycle}@${e.org} (${e.stage}): ${e.error}`);
}
}
if (report.deferred.length > 0) {
lines.push("");
lines.push(`--- deferred (budget) ---`);
lines.push(` ${report.deferred.join(", ")}`);
}
if (report.audit) {
lines.push("");
lines.push("--- audit ---");
lines.push(
` total=${report.audit.total} merge-worthy=${report.audit.mergeWorthy} (quick-win=${report.audit.quickWin}, needs-review=${report.audit.needsReview}, report-only=${report.audit.reportOnly})`
);
}
lines.push("");
lines.push("--- totals ---");
const t = report.totals;
lines.push(` drift=${t.drift} applied=${t.applied} failed=${t.failed}`);
lines.push(` guardrail-trips=${t.guardrailTrips} guardrail-blocked=${t.guardrailBlocked}`);
lines.push(` cycles-reporting=${t.cyclesReporting} audit-merge-worthy=${t.auditMergeWorthy}`);
lines.push(` status: ${report.clean ? "CLEAN" : "ATTENTION NEEDED"}`);
lines.push("");
return lines.join("\n");
}
function complianceArtifact(report) {
return `${JSON.stringify(report, null, 2)}
`;
}

// src/cli.ts
var CliError = class extends Error {
constructor(code, message) {
Expand Down Expand Up @@ -233723,6 +233840,96 @@ function parseAuditArgs(argv) {
}
return args;
}
function parseReportArgs(argv) {
const args = {
config: "",
appIdEnv: void 0,
installationIdEnv: void 0,
tokenEnv: void 0,
cycles: [],
out: void 0,
audit: false,
failOn: "none"
};
const knownFlags = /* @__PURE__ */ new Set([
"--config",
"--token-env",
"--app-id-env",
"--installation-id-env",
"--cycles",
"--out",
"--audit",
"--fail-on"
]);
let i = 0;
while (i < argv.length) {
const flag = argv[i];
if (!flag.startsWith("--")) throw new CliError(2, `unexpected positional argument: ${flag}`);
if (!knownFlags.has(flag)) throw new CliError(2, `unknown flag: ${flag}`);
switch (flag) {
case "--config": {
const val = argv[++i];
if (val === void 0 || val.startsWith("--")) throw new CliError(2, "--config requires a value");
args.config = val;
break;
}
case "--token-env": {
const val = argv[++i];
if (val === void 0 || val.startsWith("--")) throw new CliError(2, "--token-env requires a value");
args.tokenEnv = val;
break;
}
case "--app-id-env": {
const val = argv[++i];
if (val === void 0 || val.startsWith("--")) throw new CliError(2, "--app-id-env requires a value");
args.appIdEnv = val;
break;
}
case "--installation-id-env": {
const val = argv[++i];
if (val === void 0 || val.startsWith("--"))
throw new CliError(2, "--installation-id-env requires a value");
args.installationIdEnv = val;
break;
}
case "--cycles": {
const val = argv[++i];
if (val === void 0 || val.startsWith("--")) throw new CliError(2, "--cycles requires a value");
args.cycles = val.split(",").map((s) => s.trim()).filter(Boolean);
break;
}
case "--out": {
const val = argv[++i];
if (val === void 0 || val.startsWith("--")) throw new CliError(2, "--out requires a value");
args.out = val;
break;
}
case "--audit": {
args.audit = true;
break;
}
case "--fail-on": {
const val = argv[++i];
if (val !== "none" && val !== "attention") {
throw new CliError(2, `--fail-on must be "none" or "attention", got: ${val ?? "(missing)"}`);
}
args.failOn = val;
break;
}
}
i++;
}
if (!args.config) throw new CliError(2, "--config is required");
const hasTokenAuth = !!args.tokenEnv;
const hasAppAuth = !!(args.appIdEnv && args.installationIdEnv);
if (!hasTokenAuth && !hasAppAuth) {
throw new CliError(
2,
"auth is required: supply --token-env <VAR>, or both --app-id-env <VAR> and --installation-id-env <VAR>"
);
}
return args;
}
function buildClient(args) {
if (args.tokenEnv) {
const token = env(args.tokenEnv);
Expand Down Expand Up @@ -233774,8 +233981,12 @@ async function main(argv = process.argv.slice(2)) {
await runAudit(argv.slice(1));
return;
}
if (subcommand === "report") {
await runReport(argv.slice(1));
return;
}
if (subcommand !== "reconcile") {
die(2, `unknown subcommand: ${subcommand}. Did you mean "reconcile" or "audit"?`);
die(2, `unknown subcommand: ${subcommand}. Did you mean "reconcile", "audit", or "report"?`);
}
let args;
try {
Expand Down Expand Up @@ -234089,6 +234300,97 @@ async function runAudit(argv) {
}
process.exit(0);
}
async function runReport(argv) {
let reportArgs;
try {
reportArgs = parseReportArgs(argv);
} catch (err) {
if (err instanceof CliError) die(err.code, err.message);
throw err;
}
let rawConfig;
try {
const text = readFileSync2(reportArgs.config, "utf-8");
rawConfig = parseConfigFile(reportArgs.config, text);
} catch (err) {
die(3, `failed to read config file "${reportArgs.config}": ${errMsg(err)}`);
}
let config2;
try {
config2 = loadGovernanceConfig(rawConfig);
} catch (err) {
die(2, `invalid governance config: ${errMsg(err)}`);
}
let client;
try {
client = buildClient(reportArgs);
} catch (err) {
die(3, `auth setup failed: ${errMsg(err)}`);
}
let cycles;
if (reportArgs.cycles.length === 0) {
cycles = Object.values(CYCLE_REGISTRY);
} else {
cycles = [];
for (const name of reportArgs.cycles) {
const cycle = CYCLE_REGISTRY[name];
if (!cycle) {
die(2, `unknown cycle: "${name}". Known cycles: ${Object.keys(CYCLE_REGISTRY).join(", ")}`);
}
cycles.push(cycle);
}
}
let result;
try {
result = await runReconcile({ config: config2, client, cycles, mode: "dry-run" });
} catch (err) {
die(3, `reconcile failed: ${errMsg(err)}`);
}
let auditReport;
if (reportArgs.audit) {
const repoUrls = [];
for (const [orgName, orgCfg] of Object.entries(config2.orgs)) {
for (const repoName of Object.keys(orgCfg.repos ?? {})) {
repoUrls.push(`https://github.com/${orgName}/${repoName}`);
}
}
if (repoUrls.length > 0) {
try {
let token;
if (reportArgs.tokenEnv) {
token = process.env[reportArgs.tokenEnv];
} else {
const appId = env(reportArgs.appIdEnv);
const installationId = env(reportArgs.installationIdEnv);
const privateKeyPem = process.env["GOVERNANCE_APP_PRIVATE_KEY"] ?? process.env["GITHUB_APP_PRIVATE_KEY"] ?? die(2, "private key not found: set GOVERNANCE_APP_PRIVATE_KEY (or GITHUB_APP_PRIVATE_KEY) to the PEM");
const { mintInstallationToken: mintInstallationToken2 } = await Promise.resolve().then(() => (init_app_client(), app_client_exports));
const { token: minted } = await mintInstallationToken2({ appId, installationId, privateKeyPem });
token = minted;
}
auditReport = await auditRepos(repoUrls, token);
} catch (err) {
die(3, `audit failed: ${errMsg(err)}`);
}
}
}
const report = buildComplianceReport([result], auditReport);
report.generatedAt = (/* @__PURE__ */ new Date()).toISOString();
process.stdout.write(renderComplianceReport(report));
if (reportArgs.out) {
try {
writeFileSync(reportArgs.out, complianceArtifact(report), "utf-8");
process.stdout.write(`wrote artifact: ${reportArgs.out}
`);
} catch (err) {
die(3, `failed to write artifact "${reportArgs.out}": ${errMsg(err)}`);
}
}
if (reportArgs.failOn === "attention" && !report.clean) {
process.stderr.write("github-warden: compliance report needs attention (--fail-on attention)\n");
process.exit(4);
}
process.exit(0);
}
function printUsage() {
process.stdout.write(
[
Expand All @@ -234097,6 +234399,7 @@ function printUsage() {
"Subcommands:",
" reconcile Load config, authenticate, and run governance cycles.",
" audit Audit managed repos for security/correctness posture.",
" report Aggregate cycle drift (+ optional audit) into a compliance snapshot.",
"",
"Flags (reconcile):",
" --config <path> Path to governance config file (YAML or JSON).",
Expand All @@ -234115,12 +234418,20 @@ function printUsage() {
" --fail-on merge-worthy|any|none",
" Exit 4 when findings exceed threshold (default: none).",
"",
"Flags (report):",
" --config <path> Path to governance config file (YAML or JSON).",
" --token-env / --app-id-env / --installation-id-env Auth (as reconcile).",
" --cycles <name[,name...]> Cycles to include (default: all).",
" --out <path> Write the JSON compliance artifact to this path.",
" --audit Include an audit pass in the report.",
" --fail-on none|attention Exit 4 when the report needs attention (default: none).",
"",
"Exit codes:",
" 0 Success.",
" 1 Guardrail block (apply mode, override not set).",
" 2 Argument or config error.",
" 3 Runtime error.",
" 4 Audit: findings exceed --fail-on threshold.",
" 4 Audit/report: threshold exceeded or report needs attention.",
""
].join("\n")
);
Expand Down
Loading
Loading