chore(ci): gate deploys on new advisories, restore lint and typecheck
Second review follow-up. Audit gate: a flat `npm audit --audit-level=<x>` cannot work here at any level - the tree reports 102 advisories (12 low, 38 moderate, 52 high) and `--omit=dev` does not change the counts, because gatsby itself is a runtime dependency of the build. Gating on the tree as a whole stops publishing permanently; ignoring it lets a new advisory through. scripts/audit-gate.mjs fails on any critical/high advisory that is not in scripts/audit-baseline.json, which carries an owner and a reason, so today's set is a tracked exception and anything new blocks the deploy. Verified both ways: exit 0 on the committed baseline, exit 1 with an entry removed. Lint and typecheck: the deleted GitHub workflow was the only place they ran, so `npm run typecheck` and `npm run lint` are now steps in the publish workflow, ahead of the build. Lint could not simply be switched on - prettier reported 8 pre-existing files on main, all single-line indentation - so those are fixed here and the check is clean rather than baselined. Verified locally in the order the workflow runs: audit gate 0, typecheck clean, lint clean (8 files reformatted, indentation only), build 136 pages.
This commit is contained in:
1 parent
bef1b99c78
commit
2eb5d74f59
11 files changed
+1072
-16
No files matched your search
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Audit gate: fail the build on any advisory that is not a tracked exception.
|
||||
*
|
||||
* `npm audit --audit-level=<x>` is all-or-nothing, and this tree cannot pass a
|
||||
* flat gate: the Gatsby 5 build toolchain carries ~100 advisories at
|
||||
* low/moderate/high with no non-breaking fix, because gatsby is a runtime
|
||||
* dependency of the build. Blocking on any of those stops publishing
|
||||
* permanently; ignoring the tree lets a genuinely new advisory through. So the
|
||||
* known set is committed to audit-baseline.json with an owner and a reason,
|
||||
* and this script fails only when something NEW appears at or above the
|
||||
* threshold.
|
||||
*
|
||||
* Usage: node scripts/audit-gate.mjs [--write-baseline]
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const BASELINE = join(HERE, 'audit-baseline.json');
|
||||
const BLOCKING = new Set(['critical', 'high']);
|
||||
|
||||
/** npm audit exits non-zero when it finds anything, so ignore the status. */
|
||||
function auditReport() {
|
||||
let out;
|
||||
try {
|
||||
out = execFileSync('npm', ['audit', '--json', '--omit=optional'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
maxBuffer: 32 * 1024 * 1024
|
||||
});
|
||||
} catch (err) {
|
||||
out = err.stdout;
|
||||
if (!out) throw err;
|
||||
}
|
||||
return JSON.parse(out);
|
||||
}
|
||||
|
||||
/** Flatten the report to one key per package/severity/advisory triple. */
|
||||
function currentFindings(report) {
|
||||
const findings = new Map();
|
||||
for (const [pkg, v] of Object.entries(report.vulnerabilities ?? {})) {
|
||||
if (!BLOCKING.has(v.severity)) continue;
|
||||
const advisories = (v.via ?? []).filter((x) => typeof x === 'object');
|
||||
if (advisories.length === 0) {
|
||||
// via is a chain of parent package names - attribute to the package.
|
||||
findings.set(`${pkg}|${v.severity}|transitive`, { pkg, severity: v.severity, advisory: 'transitive' });
|
||||
continue;
|
||||
}
|
||||
for (const a of advisories) {
|
||||
const id = (a.url ?? '').split('/').pop() || a.title || 'unknown';
|
||||
findings.set(`${pkg}|${v.severity}|${id}`, { pkg, severity: v.severity, advisory: id, title: a.title, range: a.range });
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
const report = auditReport();
|
||||
const current = currentFindings(report);
|
||||
|
||||
if (process.argv.includes('--write-baseline')) {
|
||||
const accepted = [...current.values()].sort((a, b) => `${a.pkg}${a.advisory}`.localeCompare(`${b.pkg}${b.advisory}`));
|
||||
writeFileSync(
|
||||
BASELINE,
|
||||
JSON.stringify(
|
||||
{
|
||||
$comment:
|
||||
'Advisories at critical/high accepted for this tree. scripts/audit-gate.mjs fails the build on anything not listed here, so removing an entry re-arms the gate for that advisory. Regenerate with: node scripts/audit-gate.mjs --write-baseline',
|
||||
owner: 'Site maintainers (allan@4gl.io)',
|
||||
reason:
|
||||
'Gatsby 5 build toolchain: no non-breaking fix available upstream, and these are build-time dependencies of a statically generated site. Re-check on each Gatsby major bump.',
|
||||
accepted
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n'
|
||||
);
|
||||
console.log(`wrote ${accepted.length} accepted advisories to ${BASELINE}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const baseline = JSON.parse(readFileSync(BASELINE, 'utf8'));
|
||||
const accepted = new Set((baseline.accepted ?? []).map((a) => `${a.pkg}|${a.severity}|${a.advisory}`));
|
||||
|
||||
const fresh = [...current.entries()].filter(([key]) => !accepted.has(key));
|
||||
const resolved = [...accepted].filter((key) => !current.has(key));
|
||||
|
||||
for (const [, f] of fresh) {
|
||||
console.error(`NEW ${f.severity.toUpperCase()}: ${f.pkg} - ${f.title ?? f.advisory} (${f.advisory})`);
|
||||
}
|
||||
|
||||
if (resolved.length > 0) {
|
||||
// Not a failure: a bumped or removed dependency closes advisories, and the
|
||||
// baseline should be trimmed in the same change that closes them.
|
||||
console.log(`${resolved.length} baseline entries no longer present - consider re-running with --write-baseline:`);
|
||||
for (const key of resolved) console.log(` stale: ${key}`);
|
||||
}
|
||||
|
||||
if (fresh.length > 0) {
|
||||
console.error(`\n${fresh.length} untracked advisory/ies at critical or high.`);
|
||||
console.error('Fix it, or add it to scripts/audit-baseline.json with a reason and an owner.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`audit gate: no new critical/high advisories (${current.size} tracked in baseline)`);
|
||||
Reference in new issue
Block a user