CI: add a jest-config typecheck gate and a non-blocking lint report (#10119)

Nothing in CI ran `tsc --noEmit` or ESLint. That is why a duplicated `transformIgnorePatterns`
key sat in `packages/core/jest.config.ts` undetected (TS1117, fixed in #10116), and why ESLint
had been broken repo-wide long enough for 93 of 93 projects to fail (fixed in #10117). Both
gaps are now covered, deliberately with very different postures.

`typecheck-configs` is a real gate. `tools/tsconfig.jest-configs.json` type-checks all 95
jest.config.ts files and is green today, so the job fails if that stops being true. It is built
to need nothing but the compiler — no `extends`, `types: []`, `noResolve`, and a small shim in
`tools/jest-config-globals.d.ts` for the 53 CommonJS configs plus the one `@nx/jest` import in
the root config. The compile is 0.5s across all 95 files; the whole job measured 68s in CI,
dominated by runner provisioning. A cold `yarn install` on this fleet is measured in hours, so
avoiding one is the entire design constraint.

Proven, not assumed: exit 0 on the current tree, and exit 2 with TS1117 when a duplicate key is
reintroduced.

`lint` is NOT a gate. It runs `nx run-many -t lint` and writes the outcome to the run summary.
The repair in #10117 left roughly 2,650 errors and 6,100 warnings, all predating the job, so a
blocking lint gate would wedge every PR on day one. Non-blocking at both the step level (so the
summary still runs) and the job level (so a cache miss or install failure on the fleet cannot
redden the check either). Verified end to end on this PR: ESLint exited 1 with real findings,
the summary step ran, and the check reported green.

TypeScript is installed pinned with `--ignore-scripts` rather than fetched through `npx --yes`,
which SonarCloud correctly flagged as a vulnerability twice (S6505/S8543) for running a
network-fetched package's lifecycle scripts in CI. It is also faster.

Neither job is wired into branch protection; `develop` has no required status checks at all, so
these are visible signals rather than hard gates. The tracking task for burning down the ESLint
backlog lives in the agent workspace at `knowledge/TASKS.md`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ruslan Konviser
2026-09-05 16:44:16 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 52ae8427fe
commit be59bce929
4 changed files with 207 additions and 0 deletions
+1
View File
@@ -4,6 +4,7 @@
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
"ignoreRegExpList": ["GHSA-[A-Za-z0-9-]+"],
"words": [
"typecheck",
"ELIFECYCLE",
"PRIMARYKEY",
"NOTNULL",
+144
View File
@@ -0,0 +1,144 @@
name: 'Static Checks'
# Two checks that nothing in CI did before, added after a duplicated `transformIgnorePatterns`
# key sat in `packages/core/jest.config.ts` undetected (TS1117):
#
# * `typecheck-configs` — `tsc --noEmit` over every jest.config.ts, sub-second for the compile
# itself and under a minute for the whole job. This one is REAL: it is green today and it
# fails the job if it ever stops being. It is the check that would have caught that bug.
# * `lint` — reports the ESLint backlog. Deliberately NON-BLOCKING (`continue-on-error`), and
# it must stay that way until the backlog below is worked through. As of the repair in
# #10117 the workspace reports roughly 2,650 errors and 6,100 warnings, so a blocking lint
# gate would wedge every PR on day one. The tracking task lives in the agent workspace at
# `knowledge/TASKS.md` ("Burn down the ESLint backlog").
#
# Promote `lint` to blocking only once `nx run-many -t lint` is actually green.
on:
push:
branches:
- develop
pull_request:
branches:
- develop
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
# Least-privilege scope for the automatic GITHUB_TOKEN: these jobs only read the checkout.
permissions:
contents: read
env:
NODE_MODULES_ARCHIVE: node-modules.tar.zst
jobs:
typecheck-configs:
name: typecheck-configs
runs-on: ${{ vars.RUNNER_LINUX_X64_4 || 'ubuntu-latest' }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
with:
# Read-only job; the checkout credential does not need to outlive the step.
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 24
# No `yarn install`. `tools/tsconfig.jest-configs.json` is standalone — `types: []`,
# `noResolve`, and a two-declaration shim in `tools/jest-config-globals.d.ts` — precisely so
# this job needs nothing but the compiler. The compile itself is well under a second across
# all 95 configs; fetching the compiler dominates. Against the hours a cold `yarn install`
# costs on this fleet, that is the whole trade.
#
# Installed rather than run through `npx --yes`, which fetches on demand and runs the
# fetched package's lifecycle scripts — Sonar flags that as a vulnerability (rules S6505
# and S8543). Exact version, no scripts, no lockfile writes, and into RUNNER_TEMP so it
# cannot disturb the checkout. It is also faster: ~4s to install against ~37s for npx.
- name: Install TypeScript
run: npm install --no-save --no-package-lock --ignore-scripts --prefix "$RUNNER_TEMP/tsc" typescript@5.9.3
- name: Type-check every jest.config.ts
run: |
"$RUNNER_TEMP/tsc/node_modules/.bin/tsc" -p tools/tsconfig.jest-configs.json
lint:
name: lint (non-blocking)
runs-on: ${{ vars.RUNNER_LINUX_X64_8 || 'ubuntu-latest' }}
# Non-blocking at the JOB level, not just on the ESLint step. The step-level flag below keeps
# a red lint result from failing the job; this keeps anything else in the job — a cache miss,
# a registry hiccup, an install failure on the fleet — from doing it either. The requirement
# is that this job cannot redden CI for anyone, and a job that goes red because a runner had
# a bad day would break that just as surely as a lint error would.
continue-on-error: true
# A cold dependency install on this fleet is measured in hours, not minutes. The ceiling costs
# nothing on the warm path, which is a cache lookup.
timeout-minutes: 360
steps:
# Action versions deliberately match `build.yml` rather than the newer majors used elsewhere:
# this job consumes the node_modules cache that workflow writes, so producer and consumer are
# kept on the same actions.
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Restore node_modules archive
# Best effort: the same key the build workflow writes, so a push that already built this
# exact lockfile reuses that tree instead of installing again. The restore script below
# installs from scratch when the archive is absent, so a miss is not fatal.
uses: actions/cache/restore@v4
continue-on-error: true
with:
path: ${{ env.NODE_MODULES_ARCHIVE }}
key: ${{ runner.os }}-${{ runner.arch }}-node-modules-${{ hashFiles('yarn.lock', 'package.json', 'patches/**', '.scripts/postinstall.js') }}
# Placed AFTER the cache restore because this action rewrites yarn.lock's resolved URLs, and
# `hashFiles()` in the key above evaluates at step runtime — configuring the registry first
# would move the key and split the cache namespace by VIP reachability.
- name: Configure Registry
uses: ever-co/ever-gauzy/.github/actions/configure-registry@aa4ee19926fabcf820aa1294385a76aec6bdb548
with:
verdaccio-registry: ${{ vars.VERDACCIO_REGISTRY }}
verdaccio-token: ${{ secrets.VERDACCIO_TOKEN }}
force-public: ${{ vars.VERDACCIO_FORCE_PUBLIC }}
expect-vip: ${{ vars.RUNNER_LINUX_X64_8 != '' }}
- name: Restore node_modules
shell: bash
run: .github/scripts/restore-node-modules.sh
# NON-BLOCKING, on purpose. See the header. `--nxBail=false` so one failing project still
# lets every other project report; without it the first failure hides the rest.
- name: Run ESLint (report only)
id: eslint
continue-on-error: true
run: yarn nx run-many -t lint --nxBail=false --parallel=2
# Surface the size of the backlog on the run page, so it is visible without opening logs and
# so progress against it is legible over time.
- name: Summarize
if: always()
shell: bash
run: |
{
echo "## ESLint"
echo
if [ "${{ steps.eslint.outcome }}" = "success" ]; then
echo "\`nx run-many -t lint\` is **green**."
echo
echo "This job can now be promoted to blocking: drop \`continue-on-error\` from the"
echo "\`Run ESLint\` step in \`.github/workflows/static-checks.yml\`."
else
echo "\`nx run-many -t lint\` reported problems. **This does not fail CI** — the"
echo "backlog predates the job (see \`knowledge/TASKS.md\` in the agent workspace)."
echo
echo "Full output is in the \`Run ESLint (report only)\` step above."
fi
} >> "$GITHUB_STEP_SUMMARY"
+26
View File
@@ -0,0 +1,26 @@
/**
* Minimal ambient declarations for `tools/tsconfig.jest-configs.json`.
*
* That project deliberately sets `"types": []` so it can run with nothing installed but
* TypeScript itself — a CI job that needed `yarn install` first would cost minutes instead of
* seconds, and the whole point of the check is that it is cheap enough to run on every PR.
*
* `@types/node` is therefore unavailable, but 53 of the workspace's jest configs are CommonJS
* (`module.exports = { ... }`). These two declarations are all they need. Do not grow this file
* into a general-purpose Node shim — if a config needs more than this, it probably belongs in a
* project that has real types.
*/
declare const module: { exports: unknown };
declare const require: (id: string) => unknown;
/**
* The root `jest.config.ts` is the only one of the 95 that imports anything; the other 94 are
* self-contained data literals. Declaring the module here keeps the check at 95/95 with no
* exclusion to remember, without resolving into `node_modules`.
*
* This deliberately does not describe `@nx/jest`'s real API — the point of the check is the
* syntax and shape of the config files themselves, not the types of a third-party package.
*/
declare module '@nx/jest' {
export function getJestProjectsAsync(): Promise<unknown>;
}
+36
View File
@@ -0,0 +1,36 @@
{
// Type-checks every `jest.config.ts` in the workspace, and nothing else.
//
// Why this exists: a duplicated `transformIgnorePatterns` key sat in
// `packages/core/jest.config.ts` undetected (TS1117). Node 24's native type-stripping hid it,
// but on any runtime without stripping Jest fell into its ts-node path and failed to load the
// config at all — zero suites, zero tests. Nothing in CI ran `tsc --noEmit`, so nothing caught
// it. See `.github/workflows/static-checks.yml`.
//
// Deliberately does NOT extend the root tsconfig: this must run with nothing installed but
// TypeScript itself, so the CI job costs seconds rather than a full `yarn install`. That is
// also why `types` is empty and `tools/jest-config-globals.d.ts` supplies `module`/`require`
// for the CommonJS configs.
"compilerOptions": {
"noEmit": true,
"skipLibCheck": true,
"target": "ES2022",
"lib": ["ES2022"],
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
// The configs are plain data. Resolving `preset: '../../jest.preset.js'` and friends is
// not the point of this check, and doing so would drag in the dependency graph this
// project exists to avoid.
"noResolve": true,
"types": []
},
"include": [
"jest-config-globals.d.ts",
"../jest.config.ts",
"../apps/*/jest.config.ts",
"../packages/*/jest.config.ts",
"../packages/plugins/*/jest.config.ts"
]
}