skip to content

How do you configure detekt reports (SARIF, XML, HTML) and integrate detekt into CI so it gates merges and surfaces findings in code review?

level: middleimportance: should knowfreq 38%

answer

  1. reports { html/xml/txt/md/sarif .required.set(true) }
  2. SARIF = standard JSON; GitHub upload-sarif -> PR annotations
  3. Multi-module: ReportMergeTask to combine SARIF
  4. Gate: task fails on maxIssues; make it a required check
  5. if: always() to upload SARIF even when build fails

basics

~20 s

detekt can output reports in several formats. SARIF is a standard JSON format that GitHub understands, so you upload it to show findings as annotations on pull requests. In CI you run the detekt task and fail the build on findings.

solid answer

~40 s

detekt emits reports configured in the `reports {}` block of each `Detekt` task: `html`, `xml` (Checkstyle), `txt`, `md`, and `sarif`. **SARIF** (Static Analysis Results Interchange Format) is a vendor-neutral JSON schema; GitHub's code-scanning ingests it via the `github/codeql-action/upload-sarif` action, rendering findings as PR **annotations** and Security alerts. For multi-module builds you merge per-module SARIF with `ReportMergeTask`. CI integration: run `./gradlew detekt` (or typed `detektMain`) as a required check; the task **fails the build** when findings exceed `maxIssues`, so the merge is blocked. Use the **baseline** to avoid drowning in legacy debt while still failing on new issues. Set `reports.sarif.required.set(true)` and upload `build/reports/detekt/detekt.sarif`. Pair with `--build-upon-default-config` and pin the detekt version for reproducible CI.

code

kotlin · 9 lines
kotlin
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
    reports {
        sarif.required.set(true)
        html.required.set(true)
        xml.required.set(true)
    }
}
// CI: ./gradlew detekt --continue
//     github/codeql-action/upload-sarif with sarif_file: build/reports/detekt/detekt.sarif (if: always())

go deeper

for a junior

Knows detekt produces reports (e.g. HTML) and runs in CI.

for a middle

Configures the reports block, explains SARIF + GitHub upload, and the failing-task gate.

for a senior

Adds multi-module ReportMergeTask, baseline pairing, typed runs, and required-check setup.

for a principal

Designs the org-wide gate policy: reproducibility, debt strategy, where typed runs live, and review ergonomics.

## Report formats Each `Detekt` task has a `reports {}` block. Available formats: - **html** — human-readable, good for browsing locally. - **xml** — Checkstyle-compatible; many CI dashboards parse it. - **txt** — plain console-like list. - **md** — Markdown. - **sarif** — the strategic one for modern CI. ```kotlin import io.gitlab.arturbosch.detekt.Detekt tasks.withType<Detekt>().configureEach { reports { html.required.set(true) xml.required.set(true) sarif.required.set(true) txt.required.set(false) } } ``` Outputs land in `build/reports/detekt/`. ## What SARIF is and why it matters **SARIF** = *Static Analysis Results Interchange Format*, a standardized **JSON** schema (an OASIS standard) for analyzer output. Because it's standardized, tools like **GitHub code scanning** can ingest *any* SARIF-producing analyzer and render findings uniformly — as inline **PR annotations**, in the Security tab, and with dedup across runs. Upload step (GitHub Actions): ```yaml - name: Run detekt run: ./gradlew detekt --continue # --continue so all modules report - name: Upload SARIF if: always() uses: github/codeql-action/upload-sarif@v3 with: sarif_file: build/reports/detekt/detekt.sarif ``` `if: always()` ensures the SARIF uploads even when detekt failed the build, so reviewers still see annotations. ## Multi-module merge In a multi-project build each module writes its own SARIF. Merge them with detekt's `ReportMergeTask` so you upload one file: ```kotlin val reportMerge by tasks.registering(io.gitlab.arturbosch.detekt.report.ReportMergeTask::class) { output.set(rootProject.layout.buildDirectory.file("reports/detekt/merged.sarif")) } tasks.withType<Detekt>().configureEach { finalizedBy(reportMerge) } reportMerge { input.from(tasks.withType<Detekt>().map { it.sarifReportFile }) } ``` ## Gating merges The core gate: detekt's task **fails** when findings exceed `build.maxIssues` in `detekt.yml` (commonly `0`). Make the detekt job a **required status check** on the protected branch, so a PR can't merge while it's red. To keep the gate practical on legacy code, pair it with a **baseline** (forgive existing, fail on new) and run typed `detektMain` if you rely on `@RequiresTypeResolution` rules. ## CI hygiene - **Pin the detekt version** (and config) for reproducibility. - Use `--continue` so all modules run before failing. - Cache Gradle for speed. - Optionally publish HTML as a build artifact for deep dives. ## Mental model Reports = how findings are *expressed*; SARIF = the *lingua franca* CI tools read; the failing task + required check = the *gate*; the baseline = the *grace period* for old debt.

  • Why upload SARIF with `if: always()` in CI?
    So findings still appear as PR annotations even when detekt failed the job; otherwise a red build would skip the upload and hide the results.
  • How do you avoid uploading many separate SARIF files in a multi-module build?
    Use detekt's ReportMergeTask to merge per-module SARIF into one file, then upload that.

saying these in an interview costs you the question

  • Not knowing SARIF is a standard format consumable by GitHub code scanning
  • Uploading SARIF only on success, hiding findings when the build fails
  • Thinking reports themselves gate the build (the failing task does)
  • Ignoring multi-module merge and uploading nothing or duplicates
  • Not pinning the detekt version, causing nondeterministic CI

context