Compare commits

...

1 Commits

Author SHA1 Message Date
diegosouzapw
eb5a9f9a4a fix(ci): port the release-green ESLint gate fix to main (base-red #12363)
`main` still runs the pre-#11890 ESLint gate: a 30-minute ceiling, no
`--cache`, and a call site that funnels every non-JSON outcome into the
single detail string "could not parse eslint json".

A cold runner now crosses 30 minutes on this repository, so the gate is
killed, and main's code then reports the timeout as a parser problem —
which is why base-red #12363 lists `ESLint: could not parse eslint json`
with nothing to act on. The release branch fixed exactly this and main
never received it, the case merge-gates §8 prescribes a companion PR for.

Ports the release-side change verbatim:
- `ESLINT_TIMEOUT_MS` (60min) replaces the inline 30-minute ceiling.
- `--cache --cache-location .eslintcache` on the gate's ESLint run.
- `evaluateEslintRun()` keeps a ceiling kill reported as a ceiling kill,
  and only calls the report invalid when ESLint actually exited 0 without
  producing one.

Validated TDD: the ported test fails on main's current script with
`TypeError: evaluateEslintRun is not a function`, and passes (31/31)
with the port. Sibling suites `release-green-docs-drift-7253` (3/3) and
`sync-next-cycle` (11/11) stay green.
2026-09-03 12:41:55 -03:00
2 changed files with 86 additions and 36 deletions

View File

@@ -60,6 +60,7 @@ import { parse as parseYaml } from "yaml";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..", "..");
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
export const ESLINT_TIMEOUT_MS = 60 * 60 * 1000;
// Per-gate captured output. execFileSync buffers everything and the report only
// shows a one-line summary, so without these files every red requires RE-RUNNING
@@ -179,6 +180,56 @@ export function parseEslintJson(out) {
return null;
}
/**
* Turn one ESLint process result into release-green records.
*
* Keep process failures distinct from report parsing failures. In particular, a timed-out
* ESLint process has no JSON report by definition; collapsing its code-124 diagnostic into
* "could not parse eslint json" hides the actionable cause and sends maintainers debugging
* the parser instead of the gate ceiling.
*/
export function evaluateEslintRun({ code, out }, warningBaseline) {
const parsed = parseEslintJson(out);
if (!parsed) {
return [
{
id: "lint",
label: "ESLint",
kind: "hard",
ok: false,
detail:
code === 0
? "ESLint exited successfully but produced no valid JSON report"
: firstFailureLine(out),
},
];
}
const { errors, warnings } = eslintCounts(parsed);
const warningDrift = isDrift(warnings, warningBaseline);
return [
{
id: "lint-errors",
label: "ESLint errors",
kind: "hard",
ok: errors === 0,
detail: `${errors} error(s)`,
},
{
id: "eslint-warnings",
label: "ESLint warnings (ratchet)",
kind: "drift",
ok: !warningDrift,
detail:
warningBaseline == null
? `${warnings} (no baseline)`
: `${warnings} vs baseline ${warningBaseline}${
warningDrift ? ` (+${warnings - warningBaseline} drift → rebaseline at release)` : ""
}`,
},
];
}
/** Pull the cognitive-complexity violation count from the gate's output. */
export function parseCognitiveCount(out) {
const s = String(out || "");
@@ -451,16 +502,19 @@ async function main() {
// ESLint: ONE pass → errors (hard) + warnings (drift)
{
announce("ESLint (errors + warnings — ~5-15min)");
announce("ESLint (errors + warnings — ~15-45min)");
// Suppressions-aware, matching `npm run lint` (Pacote 4 no-new-warnings): the frozen
// pre-existing debt in config/quality/eslint-suppressions.json must not count as
// errors here — only NET-NEW violations are release reds. Timeout raised: a full
// repo pass takes ~14min alone and this pre-flight often runs alongside test suites.
const { out } = run(
// errors here — only NET-NEW violations are release reds. The cold release runner can
// exceed 30 minutes as the repository grows, and this pre-flight often runs under load.
const lintRun = run(
"npx",
[
"eslint",
".",
"--cache",
"--cache-location",
".eslintcache",
"--format",
"json",
"--suppressions-location",
@@ -471,39 +525,16 @@ async function main() {
// reason alone, which used to mask the real `--format json` report (#7837).
"--pass-on-unpruned-suppressions",
],
{ timeout: 30 * 60 * 1000 }
// The cold release runner crossed the old 30-minute ceiling as the repository grew,
// then the timeout text was misreported as invalid JSON. Keep a real upper bound, but
// leave enough headroom for the same full-tree walk that completes immediately after it
// under the complexity config on that runner.
{ timeout: ESLINT_TIMEOUT_MS }
);
const { out } = lintRun;
saveGateLog("lint", out);
const parsed = parseEslintJson(out);
if (!parsed) {
record({
id: "lint",
label: "ESLint",
kind: "hard",
ok: false,
detail: "could not parse eslint json",
});
} else {
const { errors, warnings } = eslintCounts(parsed);
record({
id: "lint-errors",
label: "ESLint errors",
kind: "hard",
ok: errors === 0,
detail: `${errors} error(s)`,
});
const base = baselineValue("eslintWarnings");
const over = isDrift(warnings, base);
record({
id: "eslint-warnings",
label: "ESLint warnings (ratchet)",
kind: "drift",
ok: !over,
detail:
base == null
? `${warnings} (no baseline)`
: `${warnings} vs baseline ${base}${over ? ` (+${warnings - base} drift → rebaseline at release)` : ""}`,
});
for (const result of evaluateEslintRun(lintRun, baselineValue("eslintWarnings"))) {
record(result);
}
}
@@ -638,7 +669,8 @@ async function main() {
// forever) into a visible failure — survives at 100min.
// Measured on idle .113: unavailable (checkout not found). Tightened to 80min from 100min as a conservative step. TODO: re-measure on idle .113 and tighten to ~1.8× measured.
id: "unit",
label: "Unit tests (full suite, CI concurrency — ~30-50min idle, up to ~80min under load (awaiting idle .113 measurement, #9532))",
label:
"Unit tests (full suite, CI concurrency — ~30-50min idle, up to ~80min under load (awaiting idle .113 measurement, #9532))",
args: ["run", "test:unit:ci"],
timeout: 80 * 60 * 1000,
},

View File

@@ -7,6 +7,7 @@ const mod = await import("../../scripts/quality/validate-release-green.mjs");
const {
firstFailureLine,
eslintCounts,
evaluateEslintRun,
parseEslintJson,
parseCognitiveCount,
isDrift,
@@ -17,6 +18,7 @@ const {
fullCiTimeoutFor,
curatedEquivalentId,
fullCiKindFor,
ESLINT_TIMEOUT_MS,
} = mod;
const extract = extractCiGates as (
@@ -50,6 +52,22 @@ test("parseEslintJson tolerates ESLint's trailing unpruned-suppressions stderr s
]);
});
test("evaluateEslintRun preserves an ESLint timeout instead of misreporting invalid JSON", () => {
const timedOut = classifyRunError({ killed: true, code: "ETIMEDOUT" }, 30 * 60 * 1000);
assert.deepEqual(evaluateEslintRun(timedOut, 0), [
{
id: "lint",
label: "ESLint",
kind: "hard",
ok: false,
detail:
"gate exceeded its 1800s ceiling and was killed — treat as a hung/failed gate (e.g. an unreleased DB handle in the unit suite); does NOT pass",
},
]);
assert.equal(ESLINT_TIMEOUT_MS, 60 * 60 * 1000, "cold release lint needs >30m headroom");
});
test("parseCognitiveCount reads the gate's count (en + pt)", () => {
assert.equal(
parseCognitiveCount("[cognitive-complexity] 797 function(s) exceed the threshold (15)."),