GitLab CI/CD Code Coverage Integration Prompt
Configure code coverage parsing, MR coverage diff, trend graphs, badge integration, multi-language combined coverage.
- Target user
- Engineers integrating test coverage into GitLab CI
- Difficulty
- Intermediate
- Tools
- Claude, ChatGPT
The prompt
You are a senior engineer who has set up code coverage reporting across many language stacks in GitLab CI/CD. You know how to parse output, configure the per-job coverage regex, generate Cobertura/JaCoCo XML for the MR diff feature, and avoid the multi-job coverage merge trap.
I will provide:
- The test framework and coverage tool (pytest+coverage.py, jest, go test -cover, JaCoCo, etc.)
- The current pipeline / coverage config
- The goal: enable coverage / fix display / require minimum / combine multi-job
Your job:
1. **Two layers of coverage in GitLab**:
- **Coverage % parsed from job log** (the "old way" — still works) — set `coverage:` regex on the job
- **Coverage diff in MR** (the "new way") — requires `artifacts:reports:coverage_report` in Cobertura format
2. **For coverage regex**:
- The regex matches the line in the test output that contains the coverage percentage
- Examples:
- **pytest-cov**: `/^TOTAL.+?(\d+\%)$/`
- **simplecov (Ruby)**: `/\(\d+\.\d+\%\) covered/`
- **jest**: `/All files[^|]*\|[^|]*\s+([\d\.]+)/`
- **go test -cover**: `/total:\s+\(statements\)\s+(\d+\.\d+%)/`
- **JaCoCo (Maven)**: `/Total.*?([0-9]{1,3})%/`
- Validate at Project → CI/CD → coverage regex tester
3. **For MR coverage diff (Cobertura XML)**:
- Most coverage tools can output Cobertura format
- **pytest-cov**: `pytest --cov-report=xml:coverage.xml`
- **jest**: `jest --coverage --coverageReporters=cobertura`
- **go**: `go test -coverprofile=cover.out && gocov-xml < cover.out > coverage.xml`
- **JaCoCo**: `mvn jacoco:report` then convert with `cover2cover.py`
- In CI: `artifacts: reports: coverage_report: { coverage_format: cobertura, path: coverage.xml }`
4. **For multi-job coverage**:
- If you split tests into parallel jobs, each job reports its own coverage
- GitLab averages them (since 14.2) when each job has `coverage:` set
- For combined report: download all artifacts in a final job, merge (use `coverage combine` for pytest, etc.), output single Cobertura
5. **For coverage gating**:
- GitLab doesn't natively enforce minimum coverage
- Workarounds:
- Job exits non-zero if coverage below threshold (`coverage report --fail-under=80`)
- Custom diff calculation between MR and target branch
- Approval rules on `coverage_decrease` (some projects use bot to comment)
6. **For badge**:
- Project → Settings → General → Badges
- URL: `https://gitlab.example.com/%{project_path}/badges/%{default_branch}/coverage.svg`
- Image to display in README
7. **For trend graphs**:
- Project → Analytics → Repository → Code Coverage
- Shows coverage over time per default branch
8. **For paths exclusion**:
- Each tool has its own exclude config:
- pytest: `--cov-config=.coveragerc` with `omit` list
- jest: `coveragePathIgnorePatterns` in config
- go: build tags
Mark DESTRUCTIVE: dropping coverage threshold under pressure (lowering bar), excluding code from coverage to "improve" the number (misleading metric).
---
Test framework + coverage tool: [DESCRIBE]
Current pipeline:
```yaml
[PASTE relevant job]
```
Symptom (if debugging): [coverage not displayed / wrong %, MR diff missing, multi-job confusion]
Goal: [enable / fix / require threshold / combine]
Why this prompt works
Coverage configuration is tool-specific and the regex syntax is a frequent stumbling block. The MR diff feature requires Cobertura format which not every tool emits natively. This prompt walks the recipes per language.
How to use it
- Set coverage regex first for the number-in-UI.
- Add Cobertura artifact for MR diff.
- For multi-job, plan combining upfront.
- For threshold gating, fail the job if below.
Useful commands
# Test regex (matches what's expected from test output)
# Project → Settings → CI/CD → expand "General pipelines" → "Test coverage parsing"
# Convert formats
# Go coverage to Cobertura
go install github.com/AlekSi/gocov-xml@latest
go install github.com/axw/gocov/gocov@latest
go test -coverprofile=cover.out ./...
gocov convert cover.out | gocov-xml > coverage.xml
# JaCoCo to Cobertura
pip install jacoco-converter
jacoco-converter target/site/jacoco/jacoco.xml > coverage.xml
# Or use: https://gitlab.com/haynes/jacoco2cobertura
Patterns
Python pytest
test-python:
image: python:3.12
script:
- pip install -e . pytest pytest-cov
- pytest --cov=mypackage --cov-report=term --cov-report=xml:coverage.xml --cov-fail-under=80
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
junit: report.xml
expire_in: 1 week
Node.js Jest
test-node:
image: node:20
script:
- npm ci
- npm test -- --coverage --coverageReporters=text-summary --coverageReporters=cobertura
coverage: '/All files\s*\|\s*([\d\.]+)/'
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
expire_in: 1 week
Go
test-go:
image: golang:1.23
script:
- go install github.com/axw/gocov/gocov@latest
- go install github.com/AlekSi/gocov-xml@latest
- go test -coverprofile=cover.out ./...
- go tool cover -func=cover.out
- gocov convert cover.out | gocov-xml > coverage.xml
coverage: '/total:\s+\(statements\)\s+(\d+\.\d+%)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
expire_in: 1 week
Multi-job parallel + combined
test:
parallel: 4
script:
- pytest --cov=mypackage --cov-report=xml:coverage-${CI_NODE_INDEX}.xml \
--tests-per-worker auto
artifacts:
paths: ["coverage-*.xml"]
expire_in: 1 hour
combine-coverage:
needs: [test]
script:
- pip install coverage
- coverage combine
- coverage xml -o coverage.xml
- coverage report
coverage: '/(?i)total.*? (\d+\%)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
Coverage gate
coverage-gate:
needs: [test]
script:
- pip install coverage
- coverage combine || true
- coverage report --fail-under=80
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
Common findings this catches
- Coverage shows N/A in UI → regex not matching test output; test in CI/CD settings.
- MR doesn’t show coverage diff →
coverage_reportartifact missing or wrong format. - Coverage % wrong → regex matches a sub-total, not the total.
- Multi-job parallel coverage = avg of partials → expected if not combined; combine for accurate %.
- Excluded paths skew coverage →
.coveragercomitlist too broad. - Coverage drops on every MR → flaky tests dropping out; quarantine flakies before fixing coverage.
- JaCoCo XML format not understood by GitLab — must convert to Cobertura.
When to escalate
- Coverage trending downward despite team focus — process issue, not pipeline.
- Cobertura conversion edge cases (e.g., parallel package coverage in Java) — engage tooling team.
- Coverage gating blocking critical fix — temporary lower OR explicit waiver path.
Related prompts
-
GitLab CI/CD Cache vs Artifacts Design Prompt
Choose between cache and artifacts in GitLab CI/CD — design cache keys that invalidate correctly, set artifact expiry, and avoid the common 'cache as artifact' mistake.
-
GitLab CI/CD Debugging Prompt
Diagnose failing GitLab CI/CD pipelines from job logs, .gitlab-ci.yml, and runner configuration.
-
GitLab CI/CD Pipeline Optimization Prompt
Speed up slow GitLab pipelines — DAG with `needs:`, cache vs artifacts, parallel jobs, image pre-builds, dependency proxy, and shallow clones.