Most teams already measure code coverage somewhere. The problem is that the number lives on a developer's laptop, gets quoted in a stand-up, and then quietly stops mattering. Code coverage in CI/CD is what turns that one-off measurement into a control: every commit is measured the same way, the report travels with the build, and a coverage drop on a pull request becomes something the pipeline can object to before code reaches main.
This article is a practical playbook. We will walk the standard coverage workflow, then show concretely how to publish coverage in Jenkins, Azure DevOps and GitLab CI, how to push it to SonarQube and enforce a quality gate, and how to focus on the code that actually changed in a pull request. Throughout, the goal is the same: make coverage a check that runs without anyone remembering to run it.
Coverage belongs in CI/CD so that every build measures it the same way, publishes it where reviewers can see it, and refuses to merge when changed code arrives untested.
Why code coverage belongs in your CI/CD pipeline
Running a coverage tool by hand answers "how much of this did my tests touch right now." Running it in the pipeline answers a more useful question: "is this change making things better or worse." Three benefits show up immediately once coverage is part of every build:
- You catch regressions automatically. When someone adds a feature with no tests, the pipeline sees coverage fall and can flag or block it. No reviewer has to notice the missing test by reading the diff.
- You enforce thresholds consistently. A documented "80% on new code" rule is just a wiki page until a machine checks it on every merge. CI is where a policy becomes a guardrail.
- You surface gaps inside the pull request. Reviewers see exactly which new lines and branches went untested, next to the diff, while the change is still small and fresh in everyone's mind.
The deeper payoff is cultural. When coverage is measured the same way on every commit, the conversation shifts from "what is our coverage number" to "did this change add risk." That is the question a pipeline is uniquely good at answering.
The CI coverage workflow: build, test, publish, gate
Almost every coverage pipeline, in any language or tool, follows the same four stages. Get this shape right and the platform-specific details become mechanical.
- Build with coverage. Compile or prepare the application with instrumentation enabled so that execution can be recorded. For native code this is a compile-time step; for managed and interpreted languages it is a runtime or load-time hook.
- Run the tests. Execute your existing test suite, unit and integration, against the instrumented build. Coverage data is collected as a side effect of the tests you already run.
- Publish the report. Convert raw coverage data into a report: HTML for humans to browse, and a machine-readable XML file for the platform and for SonarQube to ingest. Store both as build artifacts.
- Gate the merge. Compare the result against a threshold, ideally on changed code only, and pass or fail the build accordingly so the merge button reflects the verdict.
A good code coverage tool slots into this without forcing you to rewrite your build. You prefix or wrap your normal commands, and the report drops out at the end. The richer the report formats, the more of stages three and four you get for free.
Coverage that does not run on every commit is a number you remember, not a control you trust.
Code coverage in Jenkins
Jenkins is the workhorse where many of these pipelines live, and its model is plugin-driven. The pattern is: produce a coverage report during the build, then hand it to a coverage plugin that renders trends and per-build numbers on the build page.
In a declarative Jenkinsfile, a build stage runs your instrumented build and tests, an archive step keeps the HTML report as an artifact, and a publish step feeds the XML to a coverage plugin so Jenkins can draw the trend graph and apply pass/fail thresholds. The HTML lets a reviewer click straight from the build to the exact uncovered lines; the XML lets Jenkins reason about the numbers.
Archive the HTML report and publish the XML in the same stage. Reviewers want the clickable HTML; Jenkins wants the XML to compute the trend and gate. Our Jenkins coverage guide walks through both steps end to end.
Code coverage in Azure DevOps
Azure DevOps Pipelines has first-class support for publishing coverage results. After your build-and-test stage produces a coverage file, a publish task uploads it so the run summary shows a Code Coverage tab with line and branch percentages, and so the result is attached to the pipeline run for history.
The same prefix-the-build approach applies: instrument, run tests, emit a report, then add the publish step that points at the report path. Because Azure DevOps ties pipelines to pull requests, the published number can become a status check on the PR, which is the hook you need to make coverage part of the merge decision. The step-by-step setup lives in our Azure DevOps coverage guide.
Code coverage in GitLab CI
GitLab CI takes a lean, YAML-first approach. In .gitlab-ci.yml a job runs your instrumented build and tests, then exposes coverage two ways. A regex on the job log captures the overall percentage so it appears as a badge and on the job. Separately, an artifacts:reports:coverage_report entry points GitLab at a Cobertura-style XML file, which lets GitLab annotate the merge request diff with covered and uncovered lines.
That XML coverage report is the important half. It is what gives reviewers inline, per-line coverage right inside the merge request, exactly where the discussion is happening. Producing a clean XML report from your build is the one thing worth getting right for GitLab.
SonarQube code coverage and quality gates
SonarQube is where coverage stops being a per-pipeline curiosity and becomes a project-wide policy. A crucial point that trips people up: SonarQube does not measure coverage itself. Your CI job produces the coverage report, and the SonarQube scanner imports it. SonarQube's job is to display it, track it over time, and enforce it through a quality gate.
The flow is consistent across all SonarQube versions, Community through Enterprise, and SonarCloud:
- Your CI job builds with coverage, runs tests, and writes a coverage report in a format the scanner understands.
- The scanner runs as a pipeline step, reads a property pointing at the report path, and uploads results to the server.
- SonarQube shows overall coverage and, more usefully, coverage on new code, and applies the project's quality gate.
The quality gate is the enforcement mechanism. A sensible default gate fails when coverage on new code drops below a threshold, say 80%, which means a change is allowed to merge only if the lines it introduced are tested. Because the scanner runs in CI, a failing gate can surface as a failed pipeline and a red status check on the pull request. Our SonarQube coverage guide covers the scanner properties and the report formats SonarQube accepts.
If SonarQube shows 0% coverage, the report almost certainly was not generated, was written to a different path than the scanner reads, or used a format the scanner does not recognize. Confirm the report exists in the workspace before the scanner step runs.
Delta coverage and merging reports across jobs
Two refinements separate a coverage setup that teams tolerate from one they actually keep.
The first is delta, or changed-code, coverage. Instead of judging the entire codebase, you ask a narrower question on each pull request: of the lines this change added or modified, how many were exercised by tests. This is the metric that scales, because it never punishes a developer for legacy code they did not touch, and it is exactly what SonarQube's "new code" view and GitLab's MR annotations are built to show.
The second is merging reports across jobs. Real pipelines split work: unit tests in one job, integration tests in another, maybe a matrix across platforms. Each job produces a partial coverage report. To get a true picture you merge those partials into one combined report before publishing or before the scanner runs. Skip the merge and your coverage looks artificially low, because each job only saw a slice of the test suite. A tool that can combine reports from multiple runs into a single result removes this whole class of false alarms.
Best practices for coverage gates
- Fail the build on a coverage drop, not just a low absolute number. A project sitting at 55% should not be blocked from merging a well-tested fix. Gate on the direction of change and on new code.
- Focus the gate on changed code. Changed-code coverage is fair, scales to large legacy codebases, and keeps the feedback tied to the diff under review.
- Publish HTML for humans and XML for machines. Reviewers need to click to the uncovered line; the platform and SonarQube need the machine-readable file. Emit both every build.
- Merge partial reports before you gate. Combine coverage from every test job so the number reflects the whole suite, not one shard of it.
- For critical C and C++ modules, measure MC/DC, not just lines. Line coverage can read 100% while logic remains untested. On safety-relevant modules, raise the bar to decision, condition and MC/DC coverage so the pipeline checks the logic, not just whether code ran.
That last point matters most where failure is expensive. A pipeline that only checks line coverage on a flight-control branch is measuring the wrong thing; MC/DC is what proves each condition independently affects the outcome.
How RKTracer fits CI with no workflow change
The friction in most coverage rollouts is the build change. Teams stall because adding instrumentation means touching the build system, rewriting scripts, or maintaining a separate "coverage build." RKTracer is designed to avoid exactly that: you prefix your existing build command and it instruments during compilation, so the pipeline you already have keeps working.
From that single change, RKTracer produces the report formats every stage of this article needs. It emits HTML for reviewers to browse and machine-readable XML for the platform to ingest. It ships plugins for Jenkins and Azure DevOps so the report renders natively on the build page, exports XML for GitLab so merge requests get inline per-line annotations, and publishes coverage to SonarQube so your quality gate has real numbers to enforce. It also measures delta coverage on changed code and merges reports across jobs, the two refinements that keep a gate honest. Explore the full set on the features page, and see the language and metric coverage on the RKTracer overview.
# Stage 1: prefix the normal build, no Jenkinsfile rewrite $ rktracer make app ✓ instrumented 318 files, build scripts unchanged $ ctest # your existing unit + integration tests $ rkresults --report html --report xml --sonar ✓ HTML report/index.html (reviewers) ✓ XML report/coverage.xml (Jenkins / Azure / GitLab) ✓ Sonar published to project (quality gate) ✓ Delta on changed code 86% gate: pass
One prefixed build feeds Jenkins, Azure DevOps, GitLab and SonarQube from the same report.
RKTracer is a code coverage tool: it measures what your tests reached, including decision, condition and MC/DC for C and C++, across C, C++, CUDA, Rust, C#, Java, JavaScript, TypeScript, Go and Python. When a gap needs a new test, RKTracer feeds that gap data to your AI agent over MCP so a generator such as RKMCP can propose the test, but the coverage measurement and the gate are RKTracer's job. Step-by-step CI recipes live in the documentation.
The mental model to keep
- Build with coverage, run tests, publish the report, gate the merge: the same four stages everywhere.
- Publish HTML for reviewers and XML for the platform and SonarQube on every build.
- Gate on changed-code coverage, and merge partial reports before you judge the number.
The bottom line
Code coverage earns its keep only when it runs without being asked. Put it in the pipeline and the same workflow, build with coverage, run tests, publish, gate, protects you in Jenkins, Azure DevOps and GitLab alike, with SonarQube tracking the trend and enforcing the policy. Focus the gate on changed code, merge your partial reports, and raise the bar to MC/DC where the logic has to be right.
The fastest path to all of this is a tool that drops into the build you already have. Prefix the build, get HTML and XML out, publish to your platform and to SonarQube, and let the merge button start telling the truth about whether a change is tested.