Combining multiple test coverage reports for SonarCloud in GitHub Actions workflows
In a typical multi-tier test setup, we run different test suites across several GitHub Actions jobs, each of which produces a coverage report. For example, we might start with unit tests running locally on the GitHub Actions runner and subsequently move on to integration tests running against a test deployment.
Our goal is to monitor and analyze the total test coverage on SonarCloud. The most straightforward solution would be to submit each coverage report individually once it has been generated. But SonarCloud treats each push as an independent report. So we need to send all coverage reports in one go.
Thanks to GitHub Action's artifacts it's not the most difficult thing to set up. But as always, there are some subtleties we have to get right. So here's a complete example.
The example below uses Python with pytest
and pytest-cov
to generate the coverage reports, as described in detail in my previous post on Python test coverage reporting in GitHub monorepos. But the pattern applies to other languages just the same.
Note that we'll use version 4 of GitHub Action's artifacts which uses an entirely new setup under the hood and is not backwards compatible. (With the deprecated previous version 3 it was possible to upload to the same artifact multiple times. This meant you could rename the coverage.xml
files to coverage-unit.xml
and coverage-integration.xml
and push to a single coverage
artifact, which over time accumulated all coverage reports.)
Collecting coverage reports as artifacts
We use GitHub's actions/upload-artifact
action to upload the coverage report generated from our test suite.
Here's what the relevant bits of the complete test workflow look like:
# ...
unit-test:
# ...
steps:
- name: Run unit tests
run: |
pytest -m "unit" --cov=. --cov-report=xml tests/
- name: Store coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-unit
path: coverage.xml
# ...
integration-test:
# ...
steps:
- name: Run integration tests
run: |
pytest -m "integration" --cov=. --cov-report=xml tests/
- name: Store coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-integration
path: coverage.xml
# ...
Combining and uploading the coverage reports
Once all our tests are completed, we can retrieve all artifacts using GitHub's actions/download-artifact
action:
report-coverage:
# ...
depends-on: [unit-test, integration-test]
steps:
- name: Download coverage reports
uses: actions/download-artifact@v4
This will download all artifacts, storing each in a directory with the name of the artifact, i.e.,
coverage-unit/
├─ coverage.xml
coverage-integration/
├─ coverage.xml
Hence, the sonar.*.coverage.reportPaths
parameter in the sonarcloud-project.properties
file has to look as follows:
...
sonar.tests=tests
sonar.python.coverage.reportPaths=coverage-unit/coverage.xml,coverage-integration/coverage.xml
Then we can push to SonarCloud using the official sonarsource/sonarcloud-github-action
action:
report-coverage:
# ...
- name: Download coverage reports
uses: actions/download-artifact@v4
- name: SonarCloud Scan
uses: sonarsource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
If you need this in several workflows or repositories, you can create a reusable workflow or a composite action.
Subscribe to my newsletter
Read articles from Kilian Kluge directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Kilian Kluge
Kilian Kluge
My journey into software and infrastructure engineering started in a physics research lab, where I discovered the merits of loose coupling and adherence to standards the hard way. I like automated testing, concise documentation, and hunting complex bugs.