skip to content

How do you integrate ktlint into a CI quality gate, and how does a baseline help you adopt it on an existing codebase?

level: seniorimportance: should knowfreq 35%

answer

  1. CI runs ktlintCheck via ./gradlew check (read-only)
  2. ktlintFormat is local / pre-commit, never CI
  3. Baseline XML ignores existing, fails on new violations
  4. Burn down + regenerate baseline over time
  5. SARIF/Checkstyle reporters for PR annotations

basics

~20 s

Run ktlintCheck in CI so the build fails on style violations. On a big legacy codebase, generate a baseline file that records existing violations so CI only fails on new ones, then fix the old ones gradually.

solid answer

~50 s

For CI, you wire **`ktlintCheck`** into the build — it is already a dependency of the `check` lifecycle task, so `./gradlew check` runs it. CI must run check, not format, so the pipeline is read-only and deterministic, and developers run `ktlintFormat` locally (or via a pre-commit hook) to fix before pushing. Adopting ktlint on a large existing codebase all-at-once is painful, so the plugin supports a **baseline**: a generated XML file (`ktlintBaseline` task / `baseline` config) listing every current violation. After it exists, `ktlintCheck` ignores baselined violations and fails only on **new** ones — letting you turn the gate on immediately and burn down the backlog over time. You should also pin the plugin version, output **SARIF/Checkstyle reporters** for CI annotations, and keep `.editorconfig` as the single source of truth so the IDE and CLI agree. Optionally fail fast by making the lint job independent of the test job for clearer signal.

go deeper

for a junior

Knows ktlintCheck should run in CI to fail on style violations.

for a middle

Adds the check to CI, uses ktlintFormat locally, and knows a baseline exists for legacy code.

for a senior

Designs the gate end-to-end: check vs format separation, baseline burn-down, SARIF reporting, version pinning, .editorconfig as source of truth.

for a principal

Rolls out ktlint across many repos with consistent policy, sequences adoption to minimize friction, and positions it within a layered quality strategy alongside detekt and tests.

## Wiring ktlint into CI The Gradle plugin attaches `ktlintCheck` (per source set) to the standard **`check`** lifecycle task. So a CI step of `./gradlew check` runs ktlint alongside tests and other verification. Keep CI on **check**, never **format**: CI should be **read-only** (no mutating checked-out sources) and **deterministic** (same input → same pass/fail). Developers fix locally with `ktlintFormat`, ideally via an installed Git **pre-commit hook** or an IDE save action. ```bash # CI ./gradlew check # runs ktlintCheck + tests # Local ./gradlew ktlintFormat # auto-fix before committing ``` ## The baseline The hard part of adopting any linter on a mature repo is the **wall of pre-existing violations**. A **baseline** solves this: - Generate it once (`./gradlew ktlintGenerateBaseline`, producing e.g. `config/ktlint/baseline.xml`). - It records each existing violation (file + rule + position). - Afterwards `ktlintCheck` **suppresses** baselined violations and fails only on **newly introduced** ones. This means you can flip the gate to *blocking* on day one without a giant cleanup PR. You then **burn down** the baseline incrementally (often module-by-module with `ktlintFormat`), regenerating it as it shrinks. A baseline that only ever grows is a smell — treat shrinking it as ongoing tech-debt work. ## Reporters for CI ktlint can emit machine-readable reports — **Checkstyle**, **JSON**, and **SARIF**. SARIF is especially useful: CI platforms (e.g. GitHub code scanning) ingest it to show inline annotations on the PR diff instead of buried log lines. ## Reproducibility and source of truth - **Pin** the plugin/ktlint version so CI and laptops behave identically and upgrades are deliberate. - Keep all tunables in **`.editorconfig`** so the IDE plugin, CLI, and Gradle plugin agree — no drift. - Consider running ktlint as its **own CI job/step** so a style failure is distinct from a test failure (clearer signal, faster feedback). ## Where ktlint sits in the gate ktlint covers **formatting/style**. Pair it with **detekt** (code smells, complexity) and tests for a layered gate; ktlint is the cheap, fast, deterministic first line.

  • Why should CI run ktlintCheck instead of ktlintFormat?
    CI must be read-only and deterministic. Running format would mutate the checked-out sources, hide violations, and make pass/fail depend on auto-edits rather than the committed code.
  • Your baseline keeps growing each sprint. What does that signal and what do you do?
    It signals violations are being baselined instead of fixed. Add ktlintFormat to the dev workflow/pre-commit, schedule module-by-module cleanup, and regenerate the baseline so it only shrinks.
  • How do you get ktlint findings to show on the PR diff rather than in raw logs?
    Enable the SARIF reporter and feed it to the CI platform's code-scanning ingestion, which renders findings as inline annotations on the changed lines.

A baseline is like quarantining old debt: the gate stops the bleeding (new violations) while you pay down the legacy balance on your own schedule.

saying these in an interview costs you the question

  • Running ktlintFormat in CI and mutating sources
  • Refusing to adopt ktlint because of the legacy violation wall (ignoring baseline)
  • Letting the baseline grow indefinitely
  • Not pinning the version, causing nondeterministic CI
  • Putting config in build.gradle instead of .editorconfig, causing IDE/CLI drift

context