Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,139 @@ jobs:
apps/*/cypress/screenshots/**/*.png
packages/**/cypress/screenshots/**/*.png
retention-days: 1

# ---------------------------------------------------------------------------
# Lint performance benchmark (fork only).
#
# Measures the wall-clock cost of a full, uncached workspace lint so that the
# delta introduced by a new ESLint rule can be quantified. Runs only in the
# `mainframev` fork; every job above is gated on `microsoft` and therefore
# skips there, leaving this job the whole runner.
# ---------------------------------------------------------------------------
lint-benchmark:
name: Lint benchmark
if: ${{ github.repository_owner == 'mainframev' }}
runs-on: ubuntu-latest
timeout-minutes: 300
permissions:
contents: 'read'
actions: 'read'

env:
# ubuntu-latest is a 4-core runner; the workflow-level default of 6 is tuned
# for macos-14-xlarge. Pinned so both sides of the benchmark match exactly.
NX_PARALLEL: 4
# the workflow-level default of `true` adds I/O overhead and log noise
NX_VERBOSE_LOGGING: false
LINT_BENCHMARK_RUNS: 3
TIMING_PACKAGE: packages/react-components/react-button/library

steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- uses: actions/setup-node@v6
with:
cache: 'yarn'
node-version: '22'

- run: yarn install --immutable

# NOT timed. `packages/eslint-plugin/src/configs/react/config.js` requires the
# *built* `@fluentui/eslint-plugin-react-components`, so without this every
# package consuming the react config dies with "Cannot find module" before
# linting a single file. Nx builds this project's dependencies transitively.
- name: Build lint prerequisites (not timed)
run: yarn nx run-many -t build -p eslint-plugin-react-components

- name: Record environment
run: |
{
echo "## Lint benchmark environment"
echo ""
echo "| Property | Value |"
echo "| --- | --- |"
echo "| Commit | \`${GITHUB_SHA}\` |"
echo "| Runner | \`${RUNNER_OS}\` / \`$(getconf _NPROCESSORS_ONLN)\` CPUs |"
echo "| Node | \`$(node --version)\` |"
echo "| ESLint | \`$(yarn run -T eslint --version)\` |"
echo "| Nx parallelism | \`${NX_PARALLEL}\` |"
echo "| Nx cache | disabled (\`--skipNxCache\`) |"
echo "| Task dependencies | excluded (\`--excludeTaskDependencies\`) |"
echo "| Projects with a \`lint\` target | \`$(yarn nx show projects --with-target lint | wc -l | tr -d ' ')\` |"
echo ""
} >> "$GITHUB_STEP_SUMMARY"

- name: Full workspace lint (timed, uncached)
run: |
{
echo "## Full workspace lint"
echo ""
echo "\`yarn nx run-many -t lint --all --parallel=${NX_PARALLEL} --skipNxCache --excludeTaskDependencies\`"
echo ""
echo "| Run | Duration (s) | Failed tasks | Exit code |"
echo "| --- | --- | --- | --- |"
} >> "$GITHUB_STEP_SUMMARY"

best=""

for run in $(seq 1 "$LINT_BENCHMARK_RUNS"); do
started_at=$(date +%s)

set +e
yarn nx run-many -t lint --all --parallel="$NX_PARALLEL" --skipNxCache --excludeTaskDependencies \
2>&1 | tee "lint-run-${run}.log"
status=${PIPESTATUS[0]}
set -e

duration=$(( $(date +%s) - started_at ))

# A high count means projects died before linting anything (e.g. a missing
# build), which would make the duration meaningless. Surfaced so the
# measurement can be sanity-checked rather than trusted blindly.
failed_tasks=$(grep -cE '^- \S+:lint$' "lint-run-${run}.log" || true)

# echoed to stdout as well: step summaries are not retrievable via the API
echo "BENCHMARK_RESULT run=${run} duration_seconds=${duration} failed_tasks=${failed_tasks} exit=${status}"
echo "| ${run} | ${duration} | ${failed_tasks} | ${status} |" >> "$GITHUB_STEP_SUMMARY"

rm -f "lint-run-${run}.log"

if [ -z "$best" ] || [ "$duration" -lt "$best" ]; then
best=$duration
fi
done

echo "BENCHMARK_BEST duration_seconds=${best}"

{
echo ""
echo "**Fastest run: ${best}s**"
echo ""
echo "> A non-zero exit code is expected when the benchmarked rule reports"
echo "> violations. It does not affect the timing: no \`--nxBail\` is used, so"
echo "> every project is linted on every run."
echo ""
} >> "$GITHUB_STEP_SUMMARY"

- name: Per-rule timing (single package)
working-directory: ${{ env.TIMING_PACKAGE }}
run: |
set +e
TIMING=25 yarn run -T eslint src 2>&1 | tee eslint-timing.log
set -e

{
echo "## Per-rule timing — \`${TIMING_PACKAGE}\`"
echo ""
echo "Runner-independent attribution from ESLint's built-in profiler"
echo "(\`TIMING=25 eslint src\`)."
echo ""
echo '```'
sed -n '/^Rule/,$p' eslint-timing.log
echo '```'
echo ""
} >> "$GITHUB_STEP_SUMMARY"

rm -f eslint-timing.log
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const __internal = {
'@nx/workspace-consistent-callback-type': 'error',
'@nx/workspace-base-hook-signature': 'error',
'@nx/workspace-base-hook-no-forbidden-runtime': 'error',
'@nx/workspace-prefer-direct-reexport': 'error',
'@nx/workspace-no-restricted-globals': restrictedGlobals.react,
'@nx/workspace-no-missing-jsx-pragma': ['error', { runtime: 'automatic' }],
},
Expand Down
2 changes: 2 additions & 0 deletions tools/eslint-rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
RULE_NAME as baseHookNoForbiddenRuntimeName,
rule as baseHookNoForbiddenRuntime,
} from './rules/base-hook-no-forbidden-runtime';
import { RULE_NAME as preferDirectReexportName, rule as preferDirectReexport } from './rules/prefer-direct-reexport';

/**
* Import your custom workspace rules at the top of this file.
Expand Down Expand Up @@ -39,6 +40,7 @@ module.exports = {
[consistentCallbackTypeName]: consistentCallbackType,
[baseHookSignatureName]: baseHookSignature,
[baseHookNoForbiddenRuntimeName]: baseHookNoForbiddenRuntime,
[preferDirectReexportName]: preferDirectReexport,
[noRestrictedGlobalsName]: noRestrictedGlobals,
[noMissingJsxPragmaName]: noMissingJsxPragma,
},
Expand Down
Loading