Linting & Formatting in CI
Linting & Formatting in CI enforce code quality invariants before human review burns time on style debates.
Introduction
Linting & Formatting in CI enforce code quality invariants before human review burns time on style debates. ESLint catches logic smells and unsafe patterns; Prettier eliminates formatting diffs. The staff-level decision is not "which tool" but where enforcement lives: developer laptop (pre-commit), PR gate (CI), or both — with different failure modes for each.
The story
An open-source team merged a 847-file Prettier reformat PR after months of drift — every subsequent PR conflicted for weeks. Meanwhile, a fintech startup ran ESLint only in CI with no pre-commit hook; developers pushed, waited 12 minutes, failed on a missing semicolon rule, fixed, pushed again. They adopted lint-staged + Husky for instant local feedback and kept CI as the non-bypassable gate tied to branch protection. Style PRs disappeared; CI lint failures dropped 90%.
Understanding the topic
Two tools, two jobs: Prettier is an opinionated formatter (whitespace, quotes, line breaks). ESLint is a linter (unused vars, React hooks rules, import order, security plugins). Running both in CI without local hooks creates slow feedback; running only pre-commit without CI lets developers skip hooks with --no-verify.
- ESLint: configurable rules, auto-fix for some; use
@typescript-eslint,eslint-plugin-security, framework plugins (React, Angular). - Prettier: single format output; integrate via
eslint-config-prettierto avoid rule conflicts. - Pre-commit (local): Husky + lint-staged runs ESLint/Prettier only on staged files — sub-second to few seconds.
- CI gate (remote): full-repo lint on PR — catches bypassed hooks, ensures main stays clean; should match local config exactly.
- Policy: pre-commit = fast feedback; CI = enforcement + audit trail; never CI-only without documented escape hatch for emergencies.
Internal architecture
Lint/format flow — local fast path plus CI as source of truth.
Developer edit↓Pre-commit hook (lint-staged)├─ ESLint --fix on staged *.ts└─ Prettier --write on staged files↓Push → PR↓CI job: eslint . && prettier --check .↓Branch protection: required check "lint"↓Merge allowed only if green
Visual explanation
Two diagrams show where Linting & Formatting in CI lives in the delivery path and how teams implement it in production.
Step-by-step explanation
- Add Prettier and ESLint with shared config in repo root — commit
.prettierrc,eslint.config.js(flat config for ESLint 9+). - Install Husky:
npx husky init; add pre-commit hook runningnpx lint-staged. - Configure lint-staged in package.json:
"*.{ts,tsx}": ["eslint --fix", "prettier --write"]. - Add CI job running
npm run lint(eslint) andnpm run format:check(prettier --check) — no auto-fix in CI, fail on diff. - Enable branch protection requiring the lint CI check on main.
- One-time: run Prettier on entire repo in dedicated commit; add
.prettierignorefor generated files.
Production implementation
package.json + GitHub Actions — pre-commit vs CI gate:
// package.json{"scripts": {"lint": "eslint . --max-warnings 0","format:check": "prettier --check .","format:write": "prettier --write ."},"lint-staged": {"*.{ts,tsx,js}": ["eslint --fix --max-warnings 0", "prettier --write"]}}# .github/workflows/lint.ymljobs:lint:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with: { node-version: 20, cache: npm }- run: npm ci- run: npm run lint- run: npm run format:check
Execution workflow
Standardize config
Single Prettier + ESLint package in repo or org preset.
Real-world use
Airbnb's ESLint config became an industry reference. Prettier ended the "tabs vs spaces" war at Facebook and beyond. Google uses Tricorder-style CI plugins surfacing lint findings as review comments. Most SOC2-ready orgs show auditors CI logs proving lint ran on every merge to main — pre-commit alone is insufficient evidence because it can be skipped.
Enterprise use cases
200-repo organization: shared @corp/eslint-config npm package versioned semver; Renovate bumps it. Prettier config extends corp preset. CI runs lint in a reusable workflow; SonarQube optional second gate for complexity. Pre-commit hooks distributed via repo template — not mandated globally (Windows/WSL edge cases).
- Monorepo: Nx
@nx/eslint-pluginenforces module boundary rules beyond style. - Legacy: ESLint
--max-warnings Nratchet — warnings decrease each quarter until zero. - Emergency: break-glass label + admin merge bypass logged to audit; lint debt ticket required within 48h.
Production case study
E-commerce platform (120 engineers): inconsistent formatting caused 30% of review comments to be nits; senior engineers stopped reviewing junior PRs.
- Challenge: no shared formatter; ESLint configs copied per repo with drift.
- Decision: corp Prettier preset + shared ESLint flat config; Husky in template; CI required check.
- Outcome: review comment volume down 40%; time-to-merge improved 1.2 days median.
- Lesson: big-bang format commit scheduled off-hours with merge freeze — communicated one week ahead.
Trade-offs
- Pre-commit only: fast but bypassable; no audit evidence for compliance.
- CI only: authoritative but slow feedback loop — developers context-switch waiting for lint failures.
- Both: best UX and enforcement; maintenance cost of keeping hook and CI configs in sync.
- Strict rules day one on legacy: blocks all progress — use ratchet or per-directory overrides.
- Auto-fix in CI: mutates PR branches unexpectedly — prefer local fix or dedicated bot.
Security implications
ESLint security plugins catch common CI/CD and app vulnerabilities early:
eslint-plugin-securityflags eval, non-literal regex, unsafe child_process patterns.- Custom rules can ban
dangerouslySetInnerHTMLor hardcoded secrets ( complementing gitleaks). - Pre-commit hooks run arbitrary code — pin Husky/lint-staged versions; review hook scripts in PRs.
- Malicious PR could weaken eslint config — treat config changes as security-sensitive review.
Scalability analysis
Full-repo ESLint on 500k LOC monorepos exceeds 10-minute budgets:
- Use Nx/Turborepo affected lint — only changed projects on PR.
- ESLint cache:
eslint --cache --cache-location .eslintcachein CI with cache action. - Split lint job from typecheck (tsc) for parallel PR feedback.
- Distributed lint (eslint-remote) for mega-monorepos — rare, usually affected-graph suffices.
Staff engineer insights
- Pre-commit is UX; CI is policy. Never argue which "replaces" the other — they serve different trust boundaries.
- If developers run
git commit --no-verifyregularly, your pre-commit is too slow — fix lint-staged scope, not the policy. - ESLint flat config (v9) simplifies monorepos — migrate before copying deprecated .eslintrc across 50 repos.
- Format-on-save in IDE + pre-commit + CI check is redundant but cheap — redundancy prevents drift.
Best practices
- Use eslint-config-prettier to disable conflicting ESLint formatting rules.
- Pin tool versions in package.json — CI and local must run identical ESLint/Prettier.
- Exclude generated code via .eslintignore and .prettierignore.
- Run lint in parallel with unit tests in CI — independent jobs, same PR gate.
- Document --no-verify policy: allowed only with ticket + follow-up lint fix same day.
Common mistakes
- Different ESLint versions locally vs CI — "passes on my machine" lint failures.
- Running Prettier and ESLint format rules that conflict — endless auto-fix loops.
- CI-only lint on 20-minute pipeline — developers don't run lint locally ever.
- Grandfathering entire directories with eslint-disable without expiry date — permanent debt.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionPre-commit hooks vs CI lint gate — when do you use both?+
Answer
Follow-up
2BeginnerQuestionHow do ESLint and Prettier divide responsibility?+
Answer
Follow-up
3AdvancedQuestionLegacy monorepo has 12,000 ESLint warnings. Rollout plan?+
Answer
Follow-up
4AdvancedQuestionDesign CI lint for a 40-package monorepo.+
Answer
Follow-up
5IntermediateQuestionWhat lint evidence do auditors expect for SOC2?+
Answer
Follow-up
Hands-on exercise
Lab: Add Husky + lint-staged + CI lint job to a sample Node repo. Intentionally push a formatting violation with --no-verify; confirm CI fails. Fix via pre-commit hook; confirm merge allowed.
- Configure eslint-config-prettier.
- Add branch protection rule documentation (even if simulated).
- Measure pre-commit duration — target under 5s on 10 changed files.
Summary
You know how to layer lint and format checks: Husky + lint-staged locally, ESLint + Prettier --check in CI, branch protection as the non-bypassable gate. Explain the pre-commit vs CI split as feedback speed vs policy enforcement — the combination senior teams standardize on.