diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f6d5836a3..2e6cfff39 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,30 @@ name: Lint +# Until #2923 this workflow was `workflow_dispatch`-only, so ESLint had never +# gated a PR. That mattered more than it looked: `eslint.config.js` sets three +# `object-ui/*` rules to `error` *specifically* so a new violation fails CI +# (ADR-0054 Phase 5, #2879, and the objectql.ts ratchet), and each author +# pre-cleaned the existing sites so the rule would lint clean. All three +# ratchets were inert because nothing ran them. +# +# `--max-warnings` is deliberately not set: the 7,724 warnings repo-wide are 81% +# `no-explicit-any`, plus React Compiler rules the config downgrades on purpose. +# This gate is about errors. on: + push: + branches: [main, develop] + paths-ignore: + - '**/*.md' + - 'content/**' + - 'docs/**' + - '.changeset/**' + pull_request: + branches: [main, develop] + paths-ignore: + - '**/*.md' + - 'content/**' + - 'docs/**' + - '.changeset/**' workflow_dispatch: concurrency: @@ -33,6 +57,12 @@ jobs: node-version: '20.x' cache: 'pnpm' + # Every package must run ESLint or be declared a known gap. turbo skips + # scriptless packages silently, so without this a package reads as clean + # because nothing linted it. Runs before install: only reads package.json. + - name: Verify lint coverage + run: node scripts/check-lint-coverage.mjs + - name: Turbo Cache uses: actions/cache@v6 with: diff --git a/apps/site/app/(home)/page.tsx b/apps/site/app/(home)/page.tsx index 34ce10b65..882d47d87 100644 --- a/apps/site/app/(home)/page.tsx +++ b/apps/site/app/(home)/page.tsx @@ -88,7 +88,8 @@ export default function HomePage() { rel="noopener noreferrer" className="inline-block transition hover:opacity-80" > - {/* eslint-disable-next-line @next/next/no-img-element */} + {/* Plain , not next/image: these are third-party badge + endpoints that render their own SVG and need no optimisation. */} {b.alt} [p.name, p])); + +const errors = []; + +// 1. Undeclared gap — a package that never runs ESLint. +for (const pkg of packages) { + if (pkg.hasScript) continue; + if (DEBT[pkg.name]) continue; + errors.push( + `${pkg.name} (${pkg.dir}) has no "lint" script, so \`pnpm lint\` skips it entirely.\n` + + ` Add "lint": "eslint ." to its package.json. If it still has ESLint errors,\n` + + ` add it to DEBT in scripts/check-lint-coverage.mjs with an error count.` + ); +} + +// 2. Ratchet — a declared gap that has been closed must leave the list. +for (const name of Object.keys(DEBT)) { + const pkg = byName.get(name); + if (!pkg) { + errors.push(`${name} is listed in DEBT but is not a workspace package any more — delete the entry.`); + } else if (pkg.hasScript) { + errors.push( + `${name} now has a "lint" script — delete its DEBT entry so the gap cannot reopen` + + `${DEBT[name].issue ? ` (and close #${DEBT[name].issue} if it is done)` : ""}.` + ); + } +} + +// ── Report ─────────────────────────────────────────────────────────────────── +const linted = packages.filter((p) => p.hasScript).length; +const debtCount = Object.keys(DEBT).length; +const debtErrors = Object.values(DEBT).reduce((sum, d) => sum + d.errors, 0); + +if (errors.length === 0) { + console.log( + `✅ lint coverage: ${linted}/${packages.length} packages linted, ` + + `${debtCount} with outstanding errors (${debtErrors} total).` + ); + process.exit(0); +} + +console.error("❌ lint coverage regressed:\n"); +for (const message of errors) { + console.error(` • ${message}`); +} +console.error( + "\nA package with no `lint` script is not clean — turbo skips it and CI sees nothing.\n" + + "See https://github.com/objectstack-ai/objectui/issues/2923 for why this guard exists." +); +process.exit(1);