Skip to content

Commit ac77fa9

Browse files
committed
fix(ci): retry transient audit responses
1 parent 1bcb6bf commit ac77fa9

2 files changed

Lines changed: 67 additions & 18 deletions

File tree

‎scripts/check-npm-audit.mjs‎

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,8 @@ function run() {
1111
const policyPath = fileURLToPath(new URL('./npm-audit-policy.json', import.meta.url));
1212
const policy = JSON.parse(readFileSync(policyPath, 'utf8'));
1313
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
14-
const audit = spawnSync(npmCommand, ['audit', '--json'], {
15-
encoding: 'utf8',
16-
maxBuffer: 16 * 1024 * 1024,
17-
timeout: 420_000,
18-
});
19-
20-
if (audit.error) {
21-
console.error(`Unable to run npm audit: ${audit.error.message}`);
22-
process.exit(1);
23-
}
24-
25-
let report;
26-
try {
27-
report = JSON.parse(audit.stdout);
28-
} catch {
29-
console.error('npm audit did not return valid JSON.');
30-
if (audit.stderr) console.error(audit.stderr.trim());
14+
const report = runAudit(npmCommand);
15+
if (!report) {
3116
process.exit(1);
3217
}
3318

@@ -51,6 +36,45 @@ function run() {
5136
}
5237
}
5338

39+
export function runAudit(
40+
npmCommand,
41+
execute = spawnSync,
42+
warn = console.warn,
43+
fail = console.error
44+
) {
45+
const maxAttempts = 3;
46+
let lastFailure = 'unknown npm audit failure';
47+
48+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
49+
const audit = execute(npmCommand, ['audit', '--json'], {
50+
encoding: 'utf8',
51+
maxBuffer: 16 * 1024 * 1024,
52+
timeout: 180_000,
53+
});
54+
55+
if (audit.error) {
56+
lastFailure = audit.error.message;
57+
} else {
58+
try {
59+
const report = JSON.parse(audit.stdout);
60+
if (report?.auditReportVersion === 2 && isRecord(report.vulnerabilities)) {
61+
return report;
62+
}
63+
lastFailure = `unexpected JSON response: ${summarize(report)}`;
64+
} catch {
65+
lastFailure = audit.stderr.trim() || 'npm audit did not return valid JSON';
66+
}
67+
}
68+
69+
if (attempt < maxAttempts) {
70+
warn(`npm audit attempt ${attempt} failed (${lastFailure}); retrying.`);
71+
}
72+
}
73+
74+
fail(`Unable to obtain a valid npm audit report after ${maxAttempts} attempts: ${lastFailure}`);
75+
return undefined;
76+
}
77+
5478
export function validateReport(reportValue, policyValue, now) {
5579
const validationErrors = [];
5680
if (reportValue?.auditReportVersion !== 2 || !isRecord(reportValue.vulnerabilities)) {
@@ -136,3 +160,8 @@ function sameStrings(actual, expected) {
136160
function isRecord(value) {
137161
return typeof value === 'object' && value !== null && !Array.isArray(value);
138162
}
163+
164+
function summarize(value) {
165+
const summary = JSON.stringify(value) ?? String(value);
166+
return summary.length > 500 ? `${summary.slice(0, 500)}...` : summary;
167+
}

‎scripts/check-npm-audit.test.mjs‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'node:assert/strict';
22
import { describe, it } from 'node:test';
3-
import { validateReport } from './check-npm-audit.mjs';
3+
import { runAudit, validateReport } from './check-npm-audit.mjs';
44

55
const advisory = {
66
source: 1,
@@ -49,6 +49,26 @@ function policy() {
4949
}
5050

5151
describe('npm audit policy', () => {
52+
it('retries transient and malformed audit responses before accepting a valid report', () => {
53+
const expected = report();
54+
const responses = [
55+
{ error: new Error('temporary registry failure') },
56+
{ stdout: '{"error":"registry busy"}', stderr: '' },
57+
{ stdout: JSON.stringify(expected), stderr: '' },
58+
];
59+
const warnings = [];
60+
61+
const actual = runAudit(
62+
'npm',
63+
() => responses.shift(),
64+
(message) => warnings.push(message),
65+
() => assert.fail('valid final response must not fail')
66+
);
67+
68+
assert.deepEqual(actual, expected);
69+
assert.equal(warnings.length, 2);
70+
});
71+
5272
it('accepts only the exact, unexpired report described by policy', () => {
5373
assert.deepEqual(validateReport(report(), policy(), new Date('2026-09-04T00:00:00Z')), []);
5474
});

0 commit comments

Comments
 (0)