How do you integrate ktlint into a CI quality gate, and how does a baseline help you adopt it on an existing codebase?
answer
- CI runs ktlintCheck via ./gradlew check (read-only)
- ktlintFormat is local / pre-commit, never CI
- Baseline XML ignores existing, fails on new violations
- Burn down + regenerate baseline over time
- SARIF/Checkstyle reporters for PR annotations
basics
~20 sRun 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 sFor 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
Knows ktlintCheck should run in CI to fail on style violations.
Adds the check to CI, uses ktlintFormat locally, and knows a baseline exists for legacy code.
Designs the gate end-to-end: check vs format separation, baseline burn-down, SARIF reporting, version pinning, .editorconfig as source of truth.
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