diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 719600d207..7dfba8dc25 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,7 +22,21 @@ concurrency: jobs: lint: - name: ESLint + # ⚠️ This `name:` IS the required-status-check context in repository + # Settings → Rulesets (a job's check-run name is its `name:`), so it is + # pinned as contract by `scripts/check-required-contexts.mjs` and may only + # be changed together with that registry AND the Settings entry, in one + # maintainer-present sitting (#9325 ruling 2026-08-17). Either half alone + # is an outage: rename-first leaves the old context permanently pending, + # which wedges every open PR and the merge queue; settings-first drops the + # whole gate family to advisory with no signal anywhere, which is #5617 + # verbatim. + # + # It is not called `ESLint` any more because it never was only that: the + # `pnpm lint` step below is one of ~70 sequential gate steps, so a red here + # was routinely read as "a lint problem" when it was a repo gate — three + # mis-routed diagnoses on 2026-08-17 alone (#9258, PRs #9256/#9291). + name: Lint & Repo Gates runs-on: ubuntu-latest permissions: contents: read diff --git a/AGENTS.md b/AGENTS.md index c9166308bf..3536241183 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -333,10 +333,13 @@ Even inside your own worktree, operate defensively: yet"; `in_progress` is not a pass. Arming a red PR does not queue it, it hides it: every poll then misreads "not on `main` yet" as "queued". Always read *two* things when checking a landing: the queue branch **and** `origin/main`. And **the queue - enforces only the required set** — **ESLint** and **TypeScript Type Check** block by - maintainer decision (2026-08-07); everything else is advisory and rides through, and - an advisory red that lands rides `main`'s merge ref into every following PR until - stanched. That is why "arm only on green" is a rule, not a formality. + enforces only the required set** — **Lint & Repo Gates** (the whole `check:*` gate + family, formerly published under a name that described only one of its steps) and + **TypeScript Type Check** block by maintainer decision (2026-08-07); everything else + is advisory and rides through, and an advisory red that lands rides `main`'s merge ref + into every following PR until stanched. A required context is matched by check-run + name, so a job rename detaches its gate silently — treat those names as contract. + That is why "arm only on green" is a rule, not a formality. **Re-arm awareness** — none of these is a reason to avoid the queue; all are reasons to confirm a PR is still *in* it: a red queue build **ejects** your entry and drops diff --git a/scripts/check-required-contexts.mjs b/scripts/check-required-contexts.mjs index f2b5a3d4f1..fbfe3fbb2f 100644 --- a/scripts/check-required-contexts.mjs +++ b/scripts/check-required-contexts.mjs @@ -157,8 +157,19 @@ export const REQUIRED_CONTEXTS = [ { workflow: 'lint.yml', job: 'lint', - context: 'ESLint', - authorized: '#5617 maintainer ruling 2026-08-07 — applied to the settings the same day', + // Renamed from 'ESLint' under the #9325 ruling: the old name described one + // of the job's steps and mis-routed the diagnosis of every other gate it + // carries. The rename lands in two halves that CANNOT be atomic — this + // registry + the workflow `name:` here, and the required-context entry in + // repository Settings → Rulesets — so it is done in one maintainer-present + // sitting: merge, then swap the Settings entry immediately. Either half + // alone is an outage (permanently-pending, or advisory-with-no-signal = + // #5617). If you are reading this while a required context named 'ESLint' + // is still live in Settings, that swap has not happened yet. + context: 'Lint & Repo Gates', + authorized: + '#5617 maintainer ruling 2026-08-07 (enrolment, applied to the settings the same day); ' + + 'renamed from `ESLint` by the #9325 maintainer ruling 2026-08-17, swapped in the settings in the same sitting as the merge', carries: 'the whole check:* gate family — the job whose red did not block #5584', }, { @@ -540,12 +551,21 @@ async function selfTest() { // Predicted direction: RED, naming the job, the new name and the required // context. A rename is the whole defect #6865 is about, so a green here would // mean the gate is decorative. - const renamedEslint = fixture('rename ESLint', 'lint.yml', (s) => s.replace(' name: ESLint\n', ' name: ESLint (fast)\n')); + // The anchor is the CURRENT name of the gate-family job (renamed away from + // `ESLint` by #9325); the mutation restores the OLD spelling on purpose, so + // this fixture doubles as the regression test for that rename: reverting + // lint.yml alone, without this registry, is exactly the half-landed state + // the #9325 sequencing exists to prevent, and it must be red. + const renamedGateJob = fixture('rename the gate-family job back to ESLint', 'lint.yml', (s) => + s.replace(' name: Lint & Repo Gates\n', ' name: ESLint\n'), + ); assert( - renamedEslint.problems.some((p) => p.includes("job 'lint'") && p.includes('"ESLint (fast)"') && p.includes("'ESLint'") && p.includes('The name IS the contract')), - "renaming lint.yml's ESLint job ⇒ red, naming the job, the new name and the required context", + renamedGateJob.problems.some( + (p) => p.includes("job 'lint'") && p.includes('"ESLint"') && p.includes("'Lint & Repo Gates'") && p.includes('The name IS the contract'), + ), + "renaming lint.yml's gate-family job ⇒ red, naming the job, the new name and the required context", ); - assert(renamedEslint.problems.length === 1, `renaming ESLint produces exactly the one finding — got ${JSON.stringify(renamedEslint.problems)}`); + assert(renamedGateJob.problems.length === 1, `renaming the gate-family job produces exactly the one finding — got ${JSON.stringify(renamedGateJob.problems)}`); // The second-batch half: a name the maintainer approved on 2026-08-09, which // had no assertion of any kind before this script. @@ -609,7 +629,7 @@ async function selfTest() { // ── (6) the merge_group trigger ────────────────────────────────────────── const noQueue = fixture('drop merge_group from lint.yml', 'lint.yml', (s) => s.replace('\n merge_group:\n', '\n')); assert( - noQueue.problems.some((p) => p.includes('merge_group') && p.includes('ESLint') && p.includes('TypeScript Type Check')), + noQueue.problems.some((p) => p.includes('merge_group') && p.includes('Lint & Repo Gates') && p.includes('TypeScript Type Check')), 'a required-context workflow without merge_group ⇒ red, naming every context it would strand', ); assert( @@ -782,7 +802,7 @@ async function selfTest() { const lintJob = uncommented(lintJobStart === -1 ? '' : sources['lint.yml'].slice(lintJobStart, lintJobEnd === -1 ? undefined : lintJobEnd)); assert( /run: pnpm check:required-contexts\b/.test(lintJob), - 'wiring: lint.yml\'s ESLint job must run `pnpm check:required-contexts` — an unwired pin verifies nothing (#4690)', + 'wiring: lint.yml\'s `lint` job (the "Lint & Repo Gates" context) must run `pnpm check:required-contexts` — an unwired pin verifies nothing (#4690)', ); const step = lintJob.split(/\n(?= - name: )/).find((s) => /run: pnpm check:required-contexts\b/.test(s)) ?? ''; assert( @@ -794,8 +814,10 @@ async function selfTest() { assert(/check-required-contexts\.mjs --self-test/.test(wiring), 'wiring: `check:required-contexts` must run this file\'s --self-test first'); assert(/check-required-contexts\.mjs(?! --self-test)/.test(wiring), 'wiring: `check:required-contexts` must also run the real pin, not only the self-test'); // The pin lives in a job it also pins. That is deliberate and worth stating: - // renaming the ESLint job turns this gate red under the NEW name, while the - // old required context stops reporting — the PR is blocked from both sides. + // renaming the gate-family job turns this gate red under the NEW name, while + // the old required context stops reporting — the PR is blocked from both + // sides. That is also why the #9325 rename could not be self-serve: the + // repo-side half is checkable here, the Settings half is not reachable at all. assert( REQUIRED_CONTEXTS.some((entry) => entry.workflow === 'lint.yml' && entry.job === 'lint'), 'wiring: the job this gate runs in is itself registered, so a rename of it cannot be the one rename nothing notices',