Skip to main content

ESLint in React CI: Enforce Code Quality Checks

ESLint is a static analysis tool that catches bugs, style violations, and code smells in JavaScript before runtime. Running ESLint in CI ensures every commit meets your team's quality standards and prevents inconsistencies from slipping into main. GitHub Actions automatically annotates pull requests with linting errors, so reviewers see issues at a glance.

Why Lint in CI?

Local linting (via IDE extensions or pre-commit hooks) catches many issues, but not everyone runs a linter before pushing. CI linting acts as a safety net: it verifies every commit, enforces uniformity across the team, and documents your code standards. When a linting step fails, the build is rejected and developers get immediate feedback without human review.

Linting in CI also detects configuration drift—if a developer disables ESLint locally or uses an old version, CI will catch it. According to Airbnb's engineering blog (2023), teams that enforce linting in CI reduced code review cycles by 18% because reviewers no longer spend time on style corrections.

Setting Up ESLint in Your Workflow

First, ensure ESLint is installed and configured in your React project:

npm install --save-dev eslint eslint-plugin-react eslint-config-airbnb

Create an .eslintrc.js in your project root:

module.exports = {
extends: ['airbnb', 'airbnb/hooks'],
env: {
browser: true,
node: true,
es2021: true,
},
rules: {
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
'react/prop-types': 0,
},
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
};

Add a lint script to package.json:

{
"scripts": {
"lint": "eslint src --max-warnings 0"
}
}

The --max-warnings 0 flag treats warnings as errors, ensuring strict compliance.

Linting Step in Your Workflow

Add an ESLint job to your workflow file (.github/workflows/ci.yml):

name: React CI

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]

jobs:
lint:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Run ESLint
run: npm run lint

build:
runs-on: ubuntu-latest
needs: lint

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Build React app
run: npm run build

Key points:

  1. Separate job: The lint job runs independently, allowing it to fail fast
  2. needs dependency: The build job includes needs: lint, so builds only proceed if linting passes
  3. Early feedback: Linting completes before builds, so developers see style issues quickly

GitHub Annotations for PR Feedback

By default, GitHub Actions' built-in linting integration (available for some tools) annotates PRs automatically. If you're using a custom ESLint configuration that GitHub doesn't auto-detect, use a third-party action like reviewdog/action-eslint:

- name: Run ESLint with annotations
uses: reviewdog/action-eslint@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: 'github-pr-check'
eslint_args: '--max-warnings=0'
fail_on_error: true

This action:

  • Parses ESLint output
  • Annotates each error as a comment on the specific line in the PR
  • Fails the check if errors exist
  • Summarizes all issues in a check run summary

Handling ESLint Warnings vs. Errors

ESLint distinguishes between warnings and errors. Some rules emit warnings (non-blocking), while others emit errors (blocking). In a strict CI environment, treating warnings as errors ensures consistency:

// .eslintrc.js
module.exports = {
extends: ['airbnb'],
rules: {
'no-unused-vars': 'error', // Error: always fail
'no-console': 'warn', // Warning: may not fail (depends on --max-warnings)
},
};

Use npm run lint -- --format json to inspect warnings and errors:

npm run lint -- --format json | jq '.[] | select(.messages | length > 0)'

Comparison: Linting Strategies

StrategyProsCons
Pre-commit hooks onlyCatches issues before pushDevelopers can bypass with --no-verify
CI linting onlyEnforced, no bypassSlower feedback (after push)
Pre-commit + CIBest coverageMore setup, slight redundancy

Most teams use both: pre-commit hooks for instant developer feedback, and CI as an enforcement gate.

Key Takeaways

  • Linting in CI enforces code quality standards automatically, preventing inconsistent code from reaching main
  • Separate the linting job from the build job so linting failures are immediately visible
  • Use --max-warnings 0 to treat warnings as errors and maintain strict compliance
  • GitHub Actions can annotate PRs with linting errors, providing line-by-line feedback
  • Always lint before building: if code doesn't meet style standards, there's no point building it

Frequently Asked Questions

What if I want to lint only changed files?

Use git diff to detect changed files and pass them to ESLint:

git diff --name-only origin/main | grep -E '\.(js|jsx)$' | xargs eslint

This speeds up CI when working with monorepos or large projects.

Can I autofix ESLint errors in CI?

Yes, use eslint --fix to auto-correct errors, then commit the changes:

npm run lint -- --fix
git add -A
git commit -m 'style: auto-fix ESLint errors'

However, automatic fixes can be surprising to developers. Most teams prefer failing CI and requiring manual fixes.

How do I ignore certain files from linting?

Create an .eslintignore file:

node_modules/
dist/
build/

This prevents ESLint from checking third-party code and build outputs.

What's the difference between warnings and errors?

Errors always cause linting to fail. Warnings do not fail unless you use --max-warnings 0. Errors are typically for critical bugs (e.g., undefined variables), while warnings are for style preferences (e.g., console logs).

Further Reading