Codecov flags and carryforward: a coverage gate that doesn't fight your monorepo
Codecov status checks only work as a merge gate if they measure the right code. Flags, tuned thresholds, and deliberate carryforward get you there.
25 Apr 2026, 11:18 UTC

A pull request that changes three lines in a billing service fails its merge gate. The code is fine. The problem is that this PR's workflow only ran the API test suite, so Codecov has no fresh coverage number for the frontend, and a blunt project-level check treats the missing data as a regression. Faced with this a few times, most teams do one of two bad things: raise the tolerance until the gate approves everything, or delete the check and go back to arguing about coverage in code review.
Neither is necessary. Codecov's status checks work as a merge gate only when they measure the right slice of code, and the tooling supports that directly. Three configuration decisions do most of the work: flags that split coverage by test suite, thresholds that separate "no regressions" from "every changed line is tested," and carryforward for the reality that monorepo CI rarely runs everything on every pull request.
How Codecov evaluates a pull request
When CI uploads a coverage report for the head commit, Codecov compares it against the base commit's reports and posts two things back to the hosting platform: a summary comment with the coverage change and a per-flag breakdown, and status checks that pass or fail based on your rules. All of that behavior is driven by a codecov.yml file at the repository root, which is where thresholds, flag definitions, and status-check rules live.
One caveat before going further: Codecov's SaaS UI and YAML options have evolved over time, and self-hosted instances can differ, so treat the exact key names below as a map rather than a spec and confirm them against the current documentation for your setup.
Slice coverage with flags before you gate on it
A flag is a named slice of coverage — by test suite, component, or language. Each flag is reported independently and can have its own thresholds, which is what makes a monorepo gate fair: the API check shouldn't care that the frontend tests didn't run.
Suppose a repository has services/api and services/worker, each tested in its own workflow, plus apps/web tested on every PR. A configuration that makes the gate meaningful looks like this:
# codecov.yml at the repository root
coverage:
status:
project:
default:
target: auto # match the base commit's coverage
threshold: 0.5% # tolerate at most a 0.5% drop
api:
target: 80% # hard floor for the API service only
flags:
- api
patch:
default:
target: 70% # changed lines should be mostly tested
flags:
api:
paths:
- services/api/
carryforward: true # reuse last value when this suite doesn't run
worker:
paths:
- services/worker/
carryforward: true
web:
paths:
- apps/web/
carryforward: false # web tests run on every PR; missing data should fail loudlyWhat this buys you:
- The default project status uses
target: auto, meaning the head commit only needs to match the base commit's coverage, with a 0.5% tolerance for noise. - The
apiproject status evaluates only theapiflag against a hard 80% target, so a web-only PR can't fail it. - The patch status applies to the lines the PR actually touched — usually the check developers care about most, since it asks "did you test your change?" rather than "did you move a legacy number?"
apiandworkerare markedcarryforward: true, so a PR that skips their workflows still gets a sensible aggregate.
The upload side is one extra argument per job. In the job that runs the API suite, after the coverage report is generated:
# Run in CI, in the job that executes the API test suite,
# after the coverage report has been produced:
./codecov -f services/api/coverage.xml -F apiRun this in CI with the upload token available as a secret — required for private repositories, and the token's scope must allow Codecov to read commits and post checks. The uploader tooling itself has changed over time (the old bash uploader was deprecated in favor of the universal uploader and, more recently, the Codecov CLI), so confirm the currently recommended invocation in Codecov's docs before wiring this up. [Verify against current docs before publishing.]
The two status types answer different questions, and it's worth setting them deliberately:
| Status | Question it answers | Reasonable starting point |
|---|---|---|
| project | Did overall coverage regress? | target: auto with a small threshold |
| patch | Are the changed lines tested? | 70–80%, tightened over time |
Carryforward is a loan, not a gift
Carryforward solves the missing-data problem by reusing a flag's most recent upload when the head commit has none. That's the right call for partial CI — but be precise about the trade. A carried-forward number is old data. If the worker suite silently stops running, its flag keeps contributing its last known value indefinitely, and the aggregate looks healthy while the code underneath goes untested.
Two habits keep this honest:
- Don't carryforward flags that must be measured on every change. In the example above,
webhascarryforward: falseprecisely because its tests run on every PR; if a web upload goes missing, you want the check to say so rather than paper over it. - Watch flag names. Uploading with a typo like
-F apiicreates a brand-new flag with no thresholds attached, and your gate quietly stops covering that code. The PR comment lists every flag it saw; that list is the cheapest audit available.
The Codecov dashboard also shows flag definitions and upload history per commit, which is where you'd notice a flag that hasn't had a fresh upload in weeks.
Making the check actually gate — and verifying it
A pass/fail status check only blocks merges if your hosting platform requires it. On GitHub, that means adding Codecov's checks (typically codecov/project and codecov/patch) to branch protection's required checks; other platforms have equivalents. This is the step teams most often skip, and it's the difference between a gate and a decoration.
To verify the whole setup end to end:
- Read the effective
codecov.ymlfor the repository — the Codecov UI shows the parsed config — and confirm thresholds, flags, and carryforward rules match your intent. - Open a low-risk pull request and review both the bot comment's flag breakdown and the status check results on the PR.
- On the dashboard, confirm which flags were attached to the head commit and whether carryforward was applied.
If coverage gating in your repo currently means one blunt number, start small: add flags for your two largest test suites, set a patch target you'd defend in review, and require the Codecov check on your main branch. Then open a no-op PR and read the comment it produces. A coverage gate earns its place only when a failure tells you something you didn't already know — and that's a property of the configuration, not the tool.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.