fix(ci): surface what Oxlint printed when the changed-code gate cannot parse it

Oxlint writes configuration failures to stdout, and the slice from the first
brace to the last one takes a wrapper's own warning for the report, so the gate
died with "did not return Oxlint JSON output" and discarded the only
explanation of why.
This commit is contained in:
Brennan Benson
2026-08-29 18:58:12 -07:00
committed by Merge Sim
parent 3039f50341
commit 0a8da47f40
2 changed files with 32 additions and 3 deletions
+12 -3
View File
@@ -115,10 +115,19 @@ export function collectAddedLineRanges(root, requestedBase) {
function parseOxlintOutput(stdout, label) {
const start = stdout.indexOf('{')
const end = stdout.lastIndexOf('}')
if (start === -1 || end === -1) {
throw new Error(`${label} did not return Oxlint JSON output.`)
if (start !== -1 && end !== -1) {
try {
return JSON.parse(stdout.slice(start, end + 1))
} catch {
// Fall through so the caller sees what Oxlint actually printed.
}
}
return JSON.parse(stdout.slice(start, end + 1))
// Why echo it: Oxlint writes configuration failures to stdout, and a wrapper's own
// warning can carry braces that this slice mistakes for the report, so discarding the
// output leaves the gate dying with no reason anywhere in the log.
throw new Error(
`${label} did not return Oxlint JSON output. Oxlint printed:\n${stdout.trim().slice(0, 2000)}`
)
}
function normalizedDiagnosticPath(root, filename) {
@@ -276,6 +276,26 @@ describe('diagnostic collection across batches', () => {
])
})
// Why: Oxlint writes configuration failures to stdout, so swallowing it leaves the gate
// dying with no reason in the log.
it('surfaces what Oxlint printed when the output is not a report', () => {
const failure = 'Failed to parse oxlint configuration file.\n\n x Rule not found\n'
expect(() => runOxlintScan('/repo', scan, ['src/a.ts'], () => failure)).toThrow(
/Rule not found/
)
})
// Why: the slice from the first brace to the last one takes a wrapper's own warning for
// the report, and the raw SyntaxError names neither the scan nor the warning.
it('surfaces a wrapper warning whose braces shadow the report', () => {
const polluted = ` WARN Unsupported engine: wanted: {"node":"24"}\n${JSON.stringify({ diagnostics: [] })}`
expect(() => runOxlintScan('/repo', scan, ['src/a.ts'], () => polluted)).toThrow(
/Unsupported engine/
)
})
// Why: the whole point is that no single invocation carries the full argument list.
it('never hands the whole oversized set to one invocation', () => {
const batchSizes = observeBatches().map((batch) => batch.length)