From 5dafb136149f3aa7612311b0fa1a0479f74450ac Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 21 Jul 2026 22:53:12 -0700 Subject: [PATCH] chore(scripts): end failing oxlint runs with a stable status line (#112532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(scripts): end failing oxlint runs with a stable status line A crashed run-oxlint wrapper printed only a stack trace, and a lint invocation whose output was truncated (cmd | tail -N) read as success — which recently let a wrapper crash (stale node_modules after a dep-adding merge) masquerade as a clean lint. Route the CLI entry through a small wrapper that converts crashes into exit 1 and ends every failing run with '[oxlint] FAILED (exit N)' as the final line. * chore(scripts): declare runOxlintCliEntry in the script declaration contract check-guards verifies .d.mts contracts against .mjs exports and check-test-types consumes them; the new entry export needed both the declaration and explicit log-parameter annotations in the test. --- scripts/run-oxlint.d.mts | 8 +++++ scripts/run-oxlint.mjs | 22 +++++++++++-- test/scripts/run-oxlint.test.ts | 55 +++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) 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);