diff --git a/scripts/run-oxlint.d.mts b/scripts/run-oxlint.d.mts index c334bb14517c..61d5e14e33ea 100644 --- a/scripts/run-oxlint.d.mts +++ b/scripts/run-oxlint.d.mts @@ -29,3 +29,11 @@ export function filterSparseMissingOxlintTargets( * Applies wrapper policy and runs oxlint with the final argument list. */ export function main(argv?: string[], runtimeEnv?: NodeJS.ProcessEnv): Promise; +/** + * CLI entry: converts wrapper crashes into exit 1 and ends every failing run + * with a stable `[oxlint] FAILED (exit N)` final line. + */ +export function runOxlintCliEntry( + run?: () => Promise, + log?: (message: unknown) => void, +): Promise; diff --git a/scripts/run-oxlint.mjs b/scripts/run-oxlint.mjs index b22619d54544..021cea0b4144 100644 --- a/scripts/run-oxlint.mjs +++ b/scripts/run-oxlint.mjs @@ -281,6 +281,24 @@ export async function main(argv = process.argv.slice(2), runtimeEnv = process.en } } -if (import.meta.main) { - await main(); +/** + * CLI entry: converts wrapper crashes into a nonzero exit and ends every + * failing run with one stable final line. That line must survive output + * truncation (`… | tail -N`): without it, a crash or lint failure whose + * diagnostics scrolled away reads as success when only the tail is inspected. + */ +export async function runOxlintCliEntry(run = main, log = console.error) { + try { + await run(); + } catch (error) { + log(error); + process.exitCode = 1; + } + if (typeof process.exitCode === "number" && process.exitCode !== 0) { + log(`[oxlint] FAILED (exit ${process.exitCode})`); + } +} + +if (import.meta.main) { + await runOxlintCliEntry(); } diff --git a/test/scripts/run-oxlint.test.ts b/test/scripts/run-oxlint.test.ts index 936ed8de40c5..b07875b264c6 100644 --- a/test/scripts/run-oxlint.test.ts +++ b/test/scripts/run-oxlint.test.ts @@ -20,6 +20,7 @@ import { } from "../../scripts/run-oxlint-shards.mjs"; import { filterSparseMissingOxlintTargets, + runOxlintCliEntry, shouldPrepareExtensionPackageBoundaryArtifacts, } from "../../scripts/run-oxlint.mjs"; import { createScriptTestHarness } from "./test-helpers.js"; @@ -49,6 +50,60 @@ function isProcessAlive(pid: number): boolean { } describe("run-oxlint", () => { + it("ends a failing run with a stable final status line", async () => { + const priorExitCode = process.exitCode; + const lines: unknown[] = []; + try { + process.exitCode = 0; + await runOxlintCliEntry( + async () => { + process.exitCode = 2; + }, + (line: unknown) => lines.push(line), + ); + expect(lines).toEqual(["[oxlint] FAILED (exit 2)"]); + } finally { + process.exitCode = priorExitCode; + } + }); + + it("converts a wrapper crash into a nonzero exit with the status line last", async () => { + // The original incident: a crashed wrapper printed only a stack trace, and + // truncated output read as success. The marker must be the final line. + const priorExitCode = process.exitCode; + const lines: unknown[] = []; + try { + process.exitCode = 0; + await runOxlintCliEntry( + async () => { + throw new Error("artifact prep failed"); + }, + (line: unknown) => lines.push(line), + ); + expect(process.exitCode).toBe(1); + expect(lines).toHaveLength(2); + expect(lines[0]).toBeInstanceOf(Error); + expect(lines[1]).toBe("[oxlint] FAILED (exit 1)"); + } finally { + process.exitCode = priorExitCode; + } + }); + + it("stays silent on a clean run", async () => { + const priorExitCode = process.exitCode; + const lines: unknown[] = []; + try { + process.exitCode = 0; + await runOxlintCliEntry( + async () => {}, + (line: unknown) => lines.push(line), + ); + expect(lines).toEqual([]); + } finally { + process.exitCode = priorExitCode; + } + }); + it("prepares extension package boundary artifacts for normal lint runs", () => { expect(shouldPrepareExtensionPackageBoundaryArtifacts([])).toBe(true); expect(shouldPrepareExtensionPackageBoundaryArtifacts(["src/index.ts"])).toBe(true);