Running ESLint Only on Changed Files in CI

2026-02-25 hit count image

Learn how to run ESLint only on changed files in CI to introduce new ESLint rules without friction in a large-scale monorepo.

github_actions

Background

When you add a new rule to ESLint, all existing code is checked against that rule. In a large-scale monorepo, every time you add a new rule, you need to fix a large amount of existing code, and the blast radius grows significantly.

For example, adding just one new rule can trigger errors in dozens or even hundreds of files. This makes the cost of introducing new rules too high, making it difficult to improve code quality.

Ideally, we want to lint only the modified files, reducing the blast radius while gradually improving quality.

The Problem

Each app’s CI workflow was running ESLint against the entire project.

- name: ESLint
  run: yarn lint:app-a

The problems with this approach:

  • When adding a new rule, all existing files become lint targets
  • Bulk fixes to existing code are required, increasing the PR’s blast radius
  • As a result, teams hesitate to introduce new rules

Approaches Considered

Approach A: Maintaining Two Separate Config Files

The first approach we considered was splitting the ESLint config into two files: one for existing rules and one for new rules.

.eslintrc.cjs          ← Existing rules (lint all files)
.eslintrc.new-rules.cjs ← New rules (lint changed files only)
# Existing rules: Run against the entire project
- name: ESLint (existing rules)
  run: yarn lint:app-a

# New rules: Run only on changed files
- name: ESLint (new rules)
  run: npx eslint -c .eslintrc.new-rules.cjs $CHANGED_FILES

This approach has the advantage of maintaining full-project linting for existing rules while gradually introducing new rules. However, it presented several issues in practice:

  • Config file management becomes complex: Two config files must always be kept in sync
  • Rule promotion is required: When a new rule becomes applicable to the entire codebase, it must be moved from .eslintrc.new-rules.cjs to .eslintrc.cjs
  • Hard to track which rules are where: Rules are scattered across two locations, increasing management overhead

Approach B: Apply All Rules to Changed Files Only (Adopted)

The next approach we considered was keeping a single config file but limiting the lint target to changed files only.

.eslintrc.cjs ← Contains both existing and new rules (lint changed files only)

The concern with this approach was “won’t code quality degrade if existing rules aren’t applied to all files?” However, we determined there would be no practical issues for the following reasons:

  • Existing code already passes existing rules: Even without full-project linting, existing files won’t suddenly be in violation
  • Unchanged files don’t need to be linted: Violations only occur when code is changed
  • A single config file keeps management simple: Just add new rules to .eslintrc.cjs

Ultimately, we prioritized simplicity of configuration and ease of operation and adopted Approach B.

Solution: Running ESLint Only on Changed Files in a PR

We used the GitHub API to get the list of changed files in a PR and modified the workflow to run ESLint only on those files.

- name: Get changed files and run ESLint
  uses: actions/github-script@v7
  with:
    script: |
      const files = await github.paginate(github.rest.pulls.listFiles, {
        owner: context.repo.owner,
        repo: context.repo.repo,
        pull_number: context.issue.number,
      });

      const filtered = files
        .filter(f => ['added', 'modified', 'renamed', 'copied'].includes(f.status))
        .filter(f => f.filename.startsWith('apps/app-a/src/'))
        .filter(f => /\.(ts|tsx|js|jsx)$/.test(f.filename))
        .map(f => f.filename);

      if (filtered.length === 0) {
        core.info('No matching files to lint.');
        return;
      }

      await exec.exec('npx', [
        'eslint',
        '-c',
        'apps/app-a/.eslintrc.cjs',
        ...filtered,
      ]);

Key Points

1. Fetching Changed Files via GitHub API

const files = await github.paginate(github.rest.pulls.listFiles, {
  owner: context.repo.owner,
  repo: context.repo.repo,
  pull_number: context.issue.number,
});

We use github.paginate to fetch the list of changed files in the PR. This handles pagination, so all files are retrieved even when there are many changes.

2. Filtering Target Files

const filtered = files
  .filter((f) => ['added', 'modified', 'renamed', 'copied'].includes(f.status))
  .filter((f) => f.filename.startsWith('apps/app-a/src/'))
  .filter((f) => /\.(ts|tsx|js|jsx)$/.test(f.filename))
  .map((f) => f.filename);

Filtering is performed in 3 stages:

FilterPurpose
status checkOnly target added, modified, renamed, or copied files (exclude deleted files)
startsWith checkOnly target files under the app’s src/ directory
Extension checkOnly target .ts, .tsx, .js, .jsx files

3. Skip When No Changed Files

if (filtered.length === 0) {
  core.info('No matching files to lint.');
  return;
}

If there are no target files, ESLint execution is skipped. For example, if only config files or documentation were changed, ESLint won’t run.

4. Running ESLint on Changed Files Only

await exec.exec('npx', [
  'eslint',
  '-c',
  'apps/app-a/.eslintrc.cjs',
  ...filtered,
]);

Instead of yarn lint:app-a (entire project), we pass only the filtered files as arguments to ESLint. When applying to each app, only the src/ path and ESLint config file path need to be changed.

Verification

We verified the actual behavior with a test PR.

When adding a new ESLint rule:

  • Changed files: ESLint errors were detected
  • Unchanged files with issues: ESLint errors were not detected

This confirmed that CI passes without affecting existing code when adding new rules.

Summary

BeforeAfter
Lint targetEntire projectOnly changed files in the PR
Impact of new rulesAll files need fixingOnly changed files need fixes
Existing code qualityRequires bulk remediationGradually improved on modification

With this approach, we can add new ESLint rules without friction and gradually improve code quality over time.

Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!

App promotion

You can use the applications that are created by this blog writer Deku.
Deku created the applications with Flutter.

If you have interested, please try to download them for free.



SHARE
Twitter Facebook RSS