- Published on
C/C++ Documentation Coverage Reporting With Doxygen & GitHub CI/CD Badges
Background
Just thought I'd share a quick guide for Doxygen coverage reporting as I struggled to find the information that I needed, when I needed it. I implemented this as a part of SignalEasel; the SVG badge sits at the top of the README.
Tools Used
- Doxygen - Your favorite documentation generator.
- Coverxygen - A tool that generates a coverage report from Doxygen XML output. It's a Python script that you can install with
pip3 install coverxygen
. - GitHub CI/CD - Running the Doxygen and Coverxygen scripts and updating the badge data in a Gist.
- schneegans/dynamic-badges-action - A GitHub action for uploading badge data to a Gist.
- GitHub Gist - Used to store the badge data so that the badge can be updated without having to sign up for another service.
- shields.io - An open source project that provides a free to use URL that returns a SVG badge based on the data stored in the Gist. No sign up needed, just use the URL in your markdown.
Coverxygen and its Doxyfile
Coverxygen requires XML output from Doxygen. I have this setup as a separate Doxyfile so that I don't have to waste time building the XML when I don't need it.
Here is my Doxyfile for Coverxygen, Doxyfile_coverage
, which covers an entire project.
repo_root/project/doxygen/Doxyfile_coverage
:
# Doxyfile 1.9.1
DOXYFILE_ENCODING = UTF-8
# Configure input
# These paths are relative to where you run the script/Doxygen command from
INPUT = src
INPUT += include
INPUT += tests
RECURSIVE = YES
INPUT_ENCODING = UTF-8
FILE_PATTERNS = *.cpp \
*.hpp
# Configure output
OUTPUT_DIRECTORY = ./build/dox_coverage/
GENERATE_XML = YES # the important bit
# Configure Processing
BUILTIN_STL_SUPPORT = NO # increases covered by 1 when set to YES
DISTRIBUTE_GROUP_DOC = YES
XML_NS_MEMB_FILE_SCOPE = YES # include namespace members in file scope
EXTRACT_PRIVATE = YES # Check coverage of private members
HIDE_IN_BODY_DOCS = YES # Look for proper doxygen blocks
# When set to yes, coverxygen will add a whole new section called "pages" with
# one item in it.
GENERATE_TODOLIST = NO
# Warning / Exit Behavior
QUIET = YES
WARNINGS = NO
WARN_IF_UNDOCUMENTED = NO
WARN_IF_DOC_ERROR = NO
WARN_NO_PARAMDOC = NO
WARN_AS_ERROR = NO
WARN_FORMAT = "$file:$line: $text"
# Prevent UML Generation
HAVE_DOT = NO
UML_LOOK = NO
CLASS_GRAPH = NO
# Run with 15 threads
NUM_PROC_THREADS = 15
Run Doxygen and Coverxygen
For CI/CD I've put this into a bash script. You can pick commands out of it to run it manually. I run this script from the repo root directory.
repo_root/project/doxygen_coverage.sh
:
#!/bin/bash
# Exit on error
set -e
# The Doxyfile that generates the XML
DOXYFILE="project/doxygen/Doxyfile_coverage"
# Passed to coverxygen
SRC_DIR="src"
# The output directory for Doxygen, defined in the Doxyfile
DOXYGEN_OUTPUT_DIR="build/dox_coverage/xml"
# Where to put the coverage report
DOC_COVERAGE_FILE="build/doc-coverage.info"
# Remove the old coverage data file and doxygen output if they exists
rm -f $DOC_COVERAGE_FILE
rm -rf $DOXYGEN_OUTPUT_DIR
# Create the output directory
mkdir -p $DOXYGEN_OUTPUT_DIR
# Run Doxygen / Generate XML
doxygen $DOXYFILE
# Run coverxygen / Generate coverage report
python3 -m coverxygen --xml-dir $DOXYGEN_OUTPUT_DIR --src-dir $SRC_DIR --output $DOC_COVERAGE_FILE --format summary
# Print the report for visability in CI/CD logs
cat $DOC_COVERAGE_FILE
# Extract data from the report into the format `31.9% (284/891)`
SUMMARY=$(tail -1 $DOC_COVERAGE_FILE | cut -d ":" -f 2 | awk '{$1=$1;print}')
# If the script is running in GitHub Actions, set the following environment
# variables for use in the badge generation step
if [ ! -z "$GITHUB_ACTIONS" ]; then
echo "DOC_COVERAGE_LABEL=$SUMMARY" >> $GITHUB_ENV
echo "DOC_COVERAGE_VALUE=$(echo $SUMMARY | cut -d "%" -f 1)" >> $GITHUB_ENV
fi
Coverage Badge with GitHub Actions
GitHub Gist & Secrets
This step uses schneegans/dynamic-badges-action to generate a badge for the README. This action is great because it uses GitHub Gists to store the badge image, so you don't have to sign up for a service.
To set this up, you need to create a Gist, I went with doxygen_coverage.json
, and create a token so that the action can update the Gist. If you require more details, this setup is described here. You can store multiple badges in the same Gist, they just need to have different filenames.
Once you have the Gist ID and the token, create two secrets in your repository:
CODE_COVERAGE_GIST_ID
- The ID of the Gist that will store the badge dataCODE_COVERAGE_GIST_SECRET
- The token that the action will use to update the Gist

GitHub Actions
CI/CD performs the following steps:
- Run the Doxygen coverage script from above
- Update the badge data in a Gist
There are a few prerequisites for this to work:
python3
&pip3
to installcoverxygen
doxygen
to generate the XML, can be installed withapt-get install doxygen
coverxygen
to generate the coverage report, can be installed withpip3 install coverxygen
The only thing you need to change below is the path to the script and the name of the Gist file, which is set to doxygen_coverage.json
.
repo_root/.github/workflows/doxygen_coverage.yml
:
name: CI/CD
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
doxygen_coverage:
name: Doxygen Coverage
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Tools
run: |
apt-get update
apt-get install -y doxygen
pip3 install coverxygen
- name: Run doxygen coverage script (coverxygen)
run: bash project/doxygen_coverage.sh
- name: Update doxygen coverage badge (Main Branch Only)
# Only update the badge after a merge into the main branch, otherwise the
# badge in the main branch will be updated with the coverage of other
# branches.
if: ${{ github.ref == 'refs/heads/main' }}
uses: schneegans/dynamic-badges-[email protected]
with:
auth: ${{ secrets.CODE_COVERAGE_GIST_SECRET }}
gistID: ${{ secrets.CODE_COVERAGE_GIST_ID }}
filename: doxygen_coverage.json # The Gist file to update
label: Doxygen Coverage # The badge label
# The following variables are set as part of the doxygen_coverage.sh script
message: ${{ env.DOC_COVERAGE_LABEL }}
valColorRange: ${{ env.DOC_COVERAGE_VALUE }} # The actual value that will be displayed on the badge.
minColorRange: 50 # If valColorRange is at or below this, the badge will be red.
maxColorRange: 90 # If valColorRange is at or above this, the badge will be green.
Getting the Badge
After CI/CD runs, you'll see something like this in the gist:
{
"label":"Doxygen Coverage",
"message":"31.7% (285/898)",
"schemaVersion":1,
"color":"hsl(0, 100%, 40%)"
}
You can then use the following markdown to display the badge in your README, replacing <user>
and <gist_id>
with your GitHub username and the Gist ID.

For example, the badge for SignalEasel is

which renders as
Conclusion
That's all there is to it. Feel free to reach out if you have any questions or suggestions.
Notes
I keep the tools installed in a Docker image to avoid tool installation.
...
runs-on: ubuntu-latest
container:
image: joshuajerred/signaleasel
...