Skip to content

Commit 55f65aa

Browse files
zhuangjianguoclaude
andcommitted
ci(lint): rename the ESLint required context to Lint & Repo Gates (#9325)
The `lint` job in lint.yml publishes one check run whose name is its `name:`, and that name was `ESLint` while the job ran ~70 sequential `pnpm check:*` steps. Any one of them failing published a red check called `ESLint` on a PR whose diff may have nothing to do with linting, so the reader had to open the log and scroll to the failing step before they could even decide whose problem it was. That cost was paid three times on 2026-08-17 (#9258, PRs #9256/#9291), and the compounding harm is that a check whose name does not describe what it verifies trains readers to re-run on red -- which is how a real regression in this gate family gets waved through (#5617 is that failure, from the other direction). Renames the job to `Lint & Repo Gates` and moves the pinned literal in the `check-required-contexts.mjs` registry (#6865) in the same diff, so the two repo-side halves cannot drift. The self-test's rename ablation now anchors on the new name and mutates it back to `ESLint`, which makes it the regression test for this very change: reverting lint.yml without the registry is red. ⚠️ This is only the repo-side half. A GitHub required status check is matched by check-run name, so the entry in Settings -> Rulesets must be swapped from `ESLint` to `Lint & Repo Gates` by a maintainer immediately after this merges. Merging without that swap leaves every open PR and the merge queue wedged on a permanently-pending `ESLint` that can never report again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NYgmGheCzM6NrHZN436Cxf
1 parent 1b93550 commit 55f65aa

3 files changed

Lines changed: 50 additions & 13 deletions

File tree

‎.github/workflows/lint.yml‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,21 @@ concurrency:
2222
jobs:
2323

2424
lint:
25-
name: ESLint
25+
# ⚠️ This `name:` IS the required-status-check context in repository
26+
# Settings → Rulesets (a job's check-run name is its `name:`), so it is
27+
# pinned as contract by `scripts/check-required-contexts.mjs` and may only
28+
# be changed together with that registry AND the Settings entry, in one
29+
# maintainer-present sitting (#9325 ruling 2026-08-17). Either half alone
30+
# is an outage: rename-first leaves the old context permanently pending,
31+
# which wedges every open PR and the merge queue; settings-first drops the
32+
# whole gate family to advisory with no signal anywhere, which is #5617
33+
# verbatim.
34+
#
35+
# It is not called `ESLint` any more because it never was only that: the
36+
# `pnpm lint` step below is one of ~70 sequential gate steps, so a red here
37+
# was routinely read as "a lint problem" when it was a repo gate — three
38+
# mis-routed diagnoses on 2026-08-17 alone (#9258, PRs #9256/#9291).
39+
name: Lint & Repo Gates
2640
runs-on: ubuntu-latest
2741
permissions:
2842
contents: read

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,8 +333,9 @@ Even inside your own worktree, operate defensively:
333333
yet"; `in_progress` is not a pass. Arming a red PR does not queue it, it hides it:
334334
every poll then misreads "not on `main` yet" as "queued". Always read *two* things
335335
when checking a landing: the queue branch **and** `origin/main`. And **the queue
336-
enforces only the required set** — **ESLint** and **TypeScript Type Check** block by
337-
maintainer decision (2026-08-07); everything else is advisory and rides through, and
336+
enforces only the required set** — **Lint & Repo Gates** (called `ESLint` until the
337+
#9325 rename) and **TypeScript Type Check** block by maintainer decision
338+
(2026-08-07); everything else is advisory and rides through, and
338339
an advisory red that lands rides `main`'s merge ref into every following PR until
339340
stanched. That is why "arm only on green" is a rule, not a formality.
340341

‎scripts/check-required-contexts.mjs‎

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,19 @@ export const REQUIRED_CONTEXTS = [
157157
{
158158
workflow: 'lint.yml',
159159
job: 'lint',
160-
context: 'ESLint',
161-
authorized: '#5617 maintainer ruling 2026-08-07 — applied to the settings the same day',
160+
// Renamed from 'ESLint' under the #9325 ruling: the old name described one
161+
// of the job's steps and mis-routed the diagnosis of every other gate it
162+
// carries. The rename lands in two halves that CANNOT be atomic — this
163+
// registry + the workflow `name:` here, and the required-context entry in
164+
// repository Settings → Rulesets — so it is done in one maintainer-present
165+
// sitting: merge, then swap the Settings entry immediately. Either half
166+
// alone is an outage (permanently-pending, or advisory-with-no-signal =
167+
// #5617). If you are reading this while a required context named 'ESLint'
168+
// is still live in Settings, that swap has not happened yet.
169+
context: 'Lint & Repo Gates',
170+
authorized:
171+
'#5617 maintainer ruling 2026-08-07 (enrolment, applied to the settings the same day); ' +
172+
'renamed from `ESLint` by the #9325 maintainer ruling 2026-08-17, swapped in the settings in the same sitting as the merge',
162173
carries: 'the whole check:* gate family — the job whose red did not block #5584',
163174
},
164175
{
@@ -540,12 +551,21 @@ async function selfTest() {
540551
// Predicted direction: RED, naming the job, the new name and the required
541552
// context. A rename is the whole defect #6865 is about, so a green here would
542553
// mean the gate is decorative.
543-
const renamedEslint = fixture('rename ESLint', 'lint.yml', (s) => s.replace(' name: ESLint\n', ' name: ESLint (fast)\n'));
554+
// The anchor is the CURRENT name of the gate-family job (renamed away from
555+
// `ESLint` by #9325); the mutation restores the OLD spelling on purpose, so
556+
// this fixture doubles as the regression test for that rename: reverting
557+
// lint.yml alone, without this registry, is exactly the half-landed state
558+
// the #9325 sequencing exists to prevent, and it must be red.
559+
const renamedGateJob = fixture('rename the gate-family job back to ESLint', 'lint.yml', (s) =>
560+
s.replace(' name: Lint & Repo Gates\n', ' name: ESLint\n'),
561+
);
544562
assert(
545-
renamedEslint.problems.some((p) => p.includes("job 'lint'") && p.includes('"ESLint (fast)"') && p.includes("'ESLint'") && p.includes('The name IS the contract')),
546-
"renaming lint.yml's ESLint job ⇒ red, naming the job, the new name and the required context",
563+
renamedGateJob.problems.some(
564+
(p) => p.includes("job 'lint'") && p.includes('"ESLint"') && p.includes("'Lint & Repo Gates'") && p.includes('The name IS the contract'),
565+
),
566+
"renaming lint.yml's gate-family job ⇒ red, naming the job, the new name and the required context",
547567
);
548-
assert(renamedEslint.problems.length === 1, `renaming ESLint produces exactly the one finding — got ${JSON.stringify(renamedEslint.problems)}`);
568+
assert(renamedGateJob.problems.length === 1, `renaming the gate-family job produces exactly the one finding — got ${JSON.stringify(renamedGateJob.problems)}`);
549569

550570
// The second-batch half: a name the maintainer approved on 2026-08-09, which
551571
// had no assertion of any kind before this script.
@@ -609,7 +629,7 @@ async function selfTest() {
609629
// ── (6) the merge_group trigger ──────────────────────────────────────────
610630
const noQueue = fixture('drop merge_group from lint.yml', 'lint.yml', (s) => s.replace('\n merge_group:\n', '\n'));
611631
assert(
612-
noQueue.problems.some((p) => p.includes('merge_group') && p.includes('ESLint') && p.includes('TypeScript Type Check')),
632+
noQueue.problems.some((p) => p.includes('merge_group') && p.includes('Lint & Repo Gates') && p.includes('TypeScript Type Check')),
613633
'a required-context workflow without merge_group ⇒ red, naming every context it would strand',
614634
);
615635
assert(
@@ -782,7 +802,7 @@ async function selfTest() {
782802
const lintJob = uncommented(lintJobStart === -1 ? '' : sources['lint.yml'].slice(lintJobStart, lintJobEnd === -1 ? undefined : lintJobEnd));
783803
assert(
784804
/run: pnpm check:required-contexts\b/.test(lintJob),
785-
'wiring: lint.yml\'s ESLint job must run `pnpm check:required-contexts` — an unwired pin verifies nothing (#4690)',
805+
'wiring: lint.yml\'s `lint` job (the "Lint & Repo Gates" context) must run `pnpm check:required-contexts` — an unwired pin verifies nothing (#4690)',
786806
);
787807
const step = lintJob.split(/\n(?= - name: )/).find((s) => /run: pnpm check:required-contexts\b/.test(s)) ?? '';
788808
assert(
@@ -794,8 +814,10 @@ async function selfTest() {
794814
assert(/check-required-contexts\.mjs --self-test/.test(wiring), 'wiring: `check:required-contexts` must run this file\'s --self-test first');
795815
assert(/check-required-contexts\.mjs(?! --self-test)/.test(wiring), 'wiring: `check:required-contexts` must also run the real pin, not only the self-test');
796816
// The pin lives in a job it also pins. That is deliberate and worth stating:
797-
// renaming the ESLint job turns this gate red under the NEW name, while the
798-
// old required context stops reporting — the PR is blocked from both sides.
817+
// renaming the gate-family job turns this gate red under the NEW name, while
818+
// the old required context stops reporting — the PR is blocked from both
819+
// sides. That is also why the #9325 rename could not be self-serve: the
820+
// repo-side half is checkable here, the Settings half is not reachable at all.
799821
assert(
800822
REQUIRED_CONTEXTS.some((entry) => entry.workflow === 'lint.yml' && entry.job === 'lint'),
801823
'wiring: the job this gate runs in is itself registered, so a rename of it cannot be the one rename nothing notices',

0 commit comments

Comments
 (0)