CI/CD Automation Tutorial 0/46 lessons ~6 min read Lesson 32

    Linting & Formatting in CI

    Linting & Formatting in CI enforce code quality invariants before human review burns time on style debates.

    Course progress0%
    Focus
    20 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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-prettier to 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.

    text
    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.

    Linting & Formatting in CI — system view
    Trigger
    Git event
    Pipeline
    Stages
    Artifact
    Immutable
    Deploy
    Gated
    Where this topic sits in the delivery path.
    Linting & Formatting in CI — execution flow
    Plan
    Design
    Build
    Verify
    Release
    Promote
    Observe
    Metrics
    Follow this loop when designing or reviewing pipelines.

    Step-by-step explanation

    1. Add Prettier and ESLint with shared config in repo root — commit .prettierrc, eslint.config.js (flat config for ESLint 9+).
    2. Install Husky: npx husky init; add pre-commit hook running npx lint-staged.
    3. Configure lint-staged in package.json: "*.{ts,tsx}": ["eslint --fix", "prettier --write"].
    4. Add CI job running npm run lint (eslint) and npm run format:check (prettier --check) — no auto-fix in CI, fail on diff.
    5. Enable branch protection requiring the lint CI check on main.
    6. One-time: run Prettier on entire repo in dedicated commit; add .prettierignore for generated files.

    Production implementation

    package.json + GitHub Actions — pre-commit vs CI gate:

    yaml
    // 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.yml
    jobs:
    lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    with: { node-version: 20, cache: npm }
    - run: npm ci
    - run: npm run lint
    - run: npm run format:check

    Execution workflow

    1Rolling out lint & format gates
    1 / 5

    Standardize config

    Single Prettier + ESLint package in repo or org preset.

    Commit configs before enabling gates.

    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-plugin enforces module boundary rules beyond style.
    • Legacy: ESLint --max-warnings N ratchet — 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-security flags eval, non-literal regex, unsafe child_process patterns.
    • Custom rules can ban dangerouslySetInnerHTML or 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 .eslintcache in 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-verify regularly, 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.

    5 questions
    1IntermediateQuestionPre-commit hooks vs CI lint gate — when do you use both?+

    Answer

    Pre-commit (lint-staged) gives instant feedback on staged files — fixes before push. CI runs full tree with --check — non-bypassable enforcement and audit evidence. Use both: pre-commit for speed, CI for policy. Branch protection on CI check; treat --no-verify as exception requiring ticket.

    Follow-up

    How do you handle developers who always skip hooks?
    2BeginnerQuestionHow do ESLint and Prettier divide responsibility?+

    Answer

    Prettier owns all formatting (semicolons, quotes, line width) — no config debates. ESLint owns code quality (unused vars, hooks deps, import cycles, security rules). Use eslint-config-prettier to turn off ESLint formatting rules. Run Prettier last in lint-staged.

    Follow-up

    Would you ever run Prettier in CI with --write?
    3AdvancedQuestionLegacy monorepo has 12,000 ESLint warnings. Rollout plan?+

    Answer

    Ratchet: set --max-warnings to current count, fail on increase. Fix warnings in touched files (boy scout). Quarterly reduce budget. Optional per-package overrides for worst modules with owner + deadline. Never big-bang fix unless dedicated sprint — feature freeze cost too high.

    Follow-up

    SonarQube vs ESLint in CI?
    4AdvancedQuestionDesign CI lint for a 40-package monorepo.+

    Answer

    Affected-only lint via Nx/Turbo on PR. Full lint on main nightly as safety net. Shared eslint-config package. Cache .eslintcache in CI. Parallel lint job with unit tests. Required check aggregates all package lint results.

    Follow-up

    Module boundary rules — how?
    5IntermediateQuestionWhat lint evidence do auditors expect for SOC2?+

    Answer

    CI logs showing lint job ran and passed on each merge to main — timestamped, tied to commit SHA, retained per policy. Pre-commit alone insufficient because --no-verify bypasses. Export check results to GRC or store workflow run URLs.

    Follow-up

    Static analysis vs lint?

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.