feat(ci): enforce lint on PRs and guard coverage policy drift

- Add a Lint job to ci.yml running nx run-many -t lint --all, so
  module-boundary tags, legacy-alias bans, and max-lines gate merges.
- Fix the root lint script (was linting only electron-backend).
- Add tools/coverage/check-coverage-policy.mjs: fails CI when a project
  with a test target is missing from coverage-policy.json; wired into
  coverage:ci as coverage:policy:check.
- Run Tier B/C unit tests in CI without coverage (list derived from the
  policy), so website/packaging/remote-control tests run on PRs.
- Replace the hand-picked 16-project test:unit:ci list with --all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-02 00:41:59 +02:00
1 parent 8e7104d3c5
commit 0948a56ba2
3 files changed
+128 -3

No files matched your search

+39
View File
@@ -10,6 +10,33 @@ on:
workflow_dispatch:
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint all projects
run: pnpm nx run-many --target=lint --all --parallel=3 --output-style=static
env:
CI: true
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
unit-and-typecheck:
name: Unit Tests and Typechecks
runs-on: ubuntu-latest
@@ -43,6 +70,18 @@ jobs:
CI: true
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
- name: Run Tier B/C unit tests without coverage
run: |
projects="$(node tools/coverage/check-coverage-policy.mjs --list-non-tier-a)"
if [ -n "$projects" ]; then
pnpm nx run-many --target=test --projects "$projects" --parallel=3 --output-style=static
else
echo "No Tier B/C projects with a test target."
fi
env:
CI: true
NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false
- name: Upload unit coverage artifact
if: always()
uses: actions/upload-artifact@v4
+4 -3
View File
@@ -32,11 +32,12 @@
"test:frontend": "nx test web",
"test:backend": "nx test electron-backend",
"test:unit:all": "nx run-many --target=test --all --parallel=3",
"test:unit:ci": "nx run-many --target=test --projects=electron-backend,web,services,m3u-state,portal-stalker-data-access,portal-shared-util,playlist-shared-ui,portal-xtream-data-access,portal-xtream-feature,workspace-dashboard-data-access,components,@iptvnator/pipes,packaging,epg-data-access,workspace-shell-util,database --parallel=3 --output-style=static",
"test:unit:ci": "nx run-many --target=test --all --parallel=3 --output-style=static",
"coverage:unit:ci": "node tools/coverage/run-tier-a-coverage.mjs",
"coverage:merge": "node tools/coverage/merge-coverage.mjs",
"coverage:health": "node tools/coverage/coverage-health.mjs",
"coverage:ci": "pnpm run coverage:unit:ci && pnpm run coverage:merge && node tools/coverage/coverage-health.mjs --require-report",
"coverage:policy:check": "node tools/coverage/check-coverage-policy.mjs",
"coverage:ci": "pnpm run coverage:policy:check && pnpm run coverage:unit:ci && pnpm run coverage:merge && node tools/coverage/coverage-health.mjs --require-report",
"coverage:e2e:summary": "node tools/coverage/e2e-semantic-summary.mjs",
"coverage:e2e:v8:web": "node tools/coverage/e2e-v8-web.mjs",
"typecheck:web": "tsc -p apps/web/tsconfig.app.json --noEmit",
@@ -59,7 +60,7 @@
"release:artwork:manifest": "tsx tools/release/generate-marketing-artwork.ts --manifest",
"release:artwork:generate": "tsx tools/release/generate-marketing-artwork.ts --generate",
"release:artwork:validate": "tsx tools/release/generate-marketing-artwork.ts --validate",
"lint": "nx lint electron-backend",
"lint": "nx run-many --target=lint --all",
"build": "nx build electron-backend",
"wiki:export": "node tools/wiki/export-wiki-context.mjs",
"test:wiki-export": "node --test tools/wiki/export-wiki-context.test.mjs"
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env node
/**
* Guards tools/coverage/coverage-policy.json against drift.
*
* Every Nx project with a `test` target must be classified in a coverage
* tier (A, B, or C). This makes CI fail loudly when a new library is added
* without deciding how it is tested, instead of silently skipping it.
*
* Usage:
* node tools/coverage/check-coverage-policy.mjs
* Validate the policy. Exits non-zero on unclassified or stale entries.
* node tools/coverage/check-coverage-policy.mjs --list-non-tier-a
* Print a comma-separated list of Tier B/C projects that have a test
* target, for running them in CI without coverage.
*/
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import process from 'node:process';
const workspaceRoot = process.cwd();
const policyPath = path.join(workspaceRoot, 'tools/coverage/coverage-policy.json');
const policy = JSON.parse(readFileSync(policyPath, 'utf8'));
const tiers = {
tierA: policy.unitCoverage.tierA ?? [],
tierB: policy.unitCoverage.tierB ?? [],
tierC: policy.unitCoverage.tierC ?? [],
};
function showProjects(extraArgs = []) {
const output = execFileSync(
'pnpm',
['nx', 'show', 'projects', '--json', ...extraArgs],
{ cwd: workspaceRoot, encoding: 'utf8' }
);
return new Set(JSON.parse(output.slice(output.indexOf('['))));
}
const testProjects = showProjects(['--withTarget', 'test']);
const allProjects = showProjects();
const classified = new Map(
Object.entries(tiers).flatMap(([tier, projects]) =>
projects.map((project) => [project.name, tier])
)
);
if (process.argv.includes('--list-non-tier-a')) {
const nonTierA = [...tiers.tierB, ...tiers.tierC]
.map((project) => project.name)
.filter((name) => testProjects.has(name));
process.stdout.write(nonTierA.join(','));
process.exit(0);
}
const unclassified = [...testProjects].filter((name) => !classified.has(name));
const stale = [...classified.keys()].filter((name) => !allProjects.has(name));
if (unclassified.length > 0) {
console.error(
'Coverage policy is missing projects that have a test target:\n' +
unclassified.map((name) => ` - ${name}`).join('\n') +
'\n\nAdd each project to a tier in tools/coverage/coverage-policy.json:' +
'\n tierA: runs in CI with coverage (Codecov)' +
'\n tierB/tierC: runs in CI without coverage'
);
process.exit(1);
}
if (stale.length > 0) {
console.error(
'Coverage policy lists projects that no longer have a test target:\n' +
stale.map((name) => ` - ${name}`).join('\n') +
'\n\nRemove them from tools/coverage/coverage-policy.json.'
);
process.exit(1);
}
console.log(
`Coverage policy OK: ${testProjects.size} test projects classified ` +
`(${tiers.tierA.length} Tier A, ${tiers.tierB.length} Tier B, ${tiers.tierC.length} Tier C).`
);