A GitHub Action that adds pytest coverage reports as comments to your pull requests, helping you track and improve test coverage with visual feedback.
- π Visual Coverage Reports - Automatically comments on PRs with detailed coverage tables
- π·οΈ Coverage Badges - Dynamic badges showing coverage percentage with color coding
- π Test Statistics - Shows passed, failed, skipped tests with execution time
- π Direct File Links - Click to view uncovered lines directly in your repository
- π Multiple Reports - Support for monorepo with multiple coverage reports
- π¨ Customizable - Flexible titles, badges, and display options
- π XML Support - Works with both text and XML coverage formats
- π Smart Updates - Updates existing comments instead of creating duplicates
Click to expand
Before using this action, ensure you have the following installed in your Python environment:
- Python - Version 3.6+ (Python 3.9+ recommended for latest pytest/pytest-cov versions)
- pytest - Python testing framework
- pytest-cov - Coverage plugin for pytest (provides
--covand--cov-reportflags)
pip install pytest pytest-covNote: The
--covand--cov-reportflags used in the examples below are provided bypytest-cov, not pytest itself. If you see an error likepytest: error: unrecognized arguments: --cov, you need to installpytest-cov.
Python version compatibility
- Python 3.9+: Supported by latest pytest (8.4+) and pytest-cov (6.0+) versions
- Python 3.8: Use pytest-cov < 6.0.0 (e.g., pytest-cov 5.x)
- Python 3.7: Use pytest-cov < 5.0.0 (e.g., pytest-cov 4.x)
- Python 3.6 and older: Use older versions of pytest and pytest-cov
For most users, we recommend using Python 3.9+ with the latest versions of pytest and pytest-cov to get the latest features and security updates.
Add this action to your workflow:
- name: Pytest coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xmlπ Complete workflow example
name: pytest-coverage-comment
on:
pull_request:
branches:
- '*'
permissions:
contents: read
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install dependencies
run: |
pip install pytest pytest-cov
- name: Run tests with coverage
run: |
pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=src tests/ | tee pytest-coverage.txt
- name: Pytest coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xmlπ Core Inputs
| Name | Required | Default | Description |
|---|---|---|---|
github-token |
β | ${{github.token}} |
GitHub token for API access to create/update comments |
pytest-coverage-path |
./pytest-coverage.txt |
Path to pytest text coverage output (from --cov-report=term-missing) |
|
pytest-xml-coverage-path |
Path to XML coverage report (from --cov-report=xml:coverage.xml) |
||
junitxml-path |
Path to JUnit XML file for test statistics (passed/failed/skipped) | ||
issue-number |
Pull request number to comment on (required for workflow_dispatch/workflow_run events) |
π¨ Display Options
| Name | Default | Description |
|---|---|---|
title |
Coverage Report |
Main title for the coverage comment (useful for monorepo projects) |
badge-title |
Coverage |
Text shown on the coverage percentage badge |
junitxml-title |
Title for the test summary section from JUnit XML | |
hide-badge |
false |
Hide the coverage percentage badge from the comment |
hide-report |
false |
Hide the detailed coverage table (show only summary and badge) |
hide-comment |
false |
Skip creating PR comment entirely (useful for using outputs only) |
report-only-changed-files |
false |
Show only files changed in the current pull request |
xml-skip-covered |
false |
Hide files with 100% coverage from XML coverage reports |
remove-link-from-badge |
false |
Remove hyperlink from coverage badge (badge becomes plain image) |
remove-links-to-files |
false |
Remove file links from coverage table to reduce comment size |
remove-links-to-lines |
false |
Remove line number links from coverage table to reduce comment size |
text-instead-badge |
false |
Use simple text instead of badge images for coverage display |
π§ Advanced Options
| Name | Default | Description |
|---|---|---|
create-new-comment |
false |
Create new comment on each run instead of updating existing comment |
unique-id-for-comment |
Unique identifier for matrix builds to update separate comments (e.g., ${{ matrix.python-version }}) |
|
default-branch |
main |
Base branch name for file links in coverage report (e.g., main, master) |
coverage-path-prefix |
Prefix to add to file paths in coverage report links | |
multiple-files |
Generate single comment with multiple coverage reports (useful for monorepos) |
π€ Available Outputs
| Name | Example | Description |
|---|---|---|
coverage |
85% |
Coverage percentage from pytest report |
color |
green |
Badge color based on coverage percentage (red/orange/yellow/green/brightgreen) |
coverageHtml |
HTML string | Full HTML coverage report with clickable links to uncovered lines |
summaryReport |
Markdown string | Test summary in markdown format with statistics (tests/skipped/failures/errors/time) |
warnings |
42 |
Number of coverage warnings from pytest-cov |
tests |
109 |
Total number of tests run (from JUnit XML) |
skipped |
2 |
Number of skipped tests (from JUnit XML) |
failures |
0 |
Number of failed tests (from JUnit XML) |
errors |
0 |
Number of test errors (from JUnit XML) |
time |
12.5 |
Test execution time in seconds (from JUnit XML) |
notSuccessTestInfo |
JSON string | JSON details of failed, errored, and skipped tests (from JUnit XML) |
Standard PR Comment
- name: Run tests
run: |
pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=src tests/ | tee pytest-coverage.txt
- name: Coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xmlUsing coverage.xml instead of text output
- name: Generate XML coverage
run: |
pytest --cov-report=xml:coverage.xml --cov=src tests/
- name: Coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-xml-coverage-path: ./coverage.xml
junitxml-path: ./pytest.xmlMultiple coverage reports in a single comment
- name: Coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
multiple-files: |
Backend API, ./backend/pytest-coverage.txt, ./backend/pytest.xml
Frontend SDK, ./frontend/pytest-coverage.txt, ./frontend/pytest.xml
Data Pipeline, ./pipeline/pytest-coverage.txt, ./pipeline/pytest.xmlThis creates a consolidated table showing all coverage reports:
| Title | Coverage | Tests | Time |
|---|---|---|---|
| Backend API | 85% | 156 | 23.4s |
| Frontend SDK | 92% | 89 | 12.1s |
| Data Pipeline | 78% | 234 | 45.6s |
Output: Combined table showing coverage and test results for all packages.
Running tests inside Docker containers
- name: Run tests in Docker
run: |
docker run -v /tmp:/tmp $IMAGE_TAG \
python -m pytest \
--cov-report=term-missing:skip-covered \
--junitxml=/tmp/pytest.xml \
--cov=src tests/ | tee /tmp/pytest-coverage.txt
- name: Coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: /tmp/pytest-coverage.txt
junitxml-path: /tmp/pytest.xmlSeparate comments for each matrix combination
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
os: [ubuntu-latest, windows-latest]
steps:
- name: Coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
unique-id-for-comment: ${{ matrix.python-version }}-${{ matrix.os }}
title: Coverage for Python ${{ matrix.python-version }} on ${{ matrix.os }}Keep coverage badge in README always up-to-date
First, add placeholders to your README.md:
<!-- Pytest Coverage Comment:Begin -->
<!-- Pytest Coverage Comment:End -->Then use this workflow:
name: Update Coverage Badge
on:
push:
branches: [main]
permissions:
contents: write
jobs:
update-badge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- name: Run tests
run: |
pytest --junitxml=pytest.xml --cov-report=term-missing --cov=src tests/ | tee pytest-coverage.txt
- name: Coverage comment
id: coverage
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
hide-comment: true
- name: Update README
run: |
sed -i '/<!-- Pytest Coverage Comment:Begin -->/,/<!-- Pytest Coverage Comment:End -->/c\<!-- Pytest Coverage Comment:Begin -->\n${{ steps.coverage.outputs.coverageHtml }}\n<!-- Pytest Coverage Comment:End -->' ./README.md
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'docs: update coverage badge'
file_pattern: README.mdHere's what the generated coverage comment looks like:
Coverage Report
| File | Stmts | Miss | Cover | Missing |
|---|---|---|---|---|
| functions/example_completed | ||||
| Β Β example_completed.py | 64 | 19 | 70% | 33, 39β45, 48β51, 55β58, 65β70, 91β92 |
| functions/example_manager | ||||
| Β Β example_manager.py | 44 | 11 | 75% | 31β33, 49β55, 67β69 |
| Β Β example_static.py | 40 | 2 | 95% | 60β61 |
| functions/my_exampels | ||||
| Β Β example.py | 20 | 20 | 0% | 1β31 |
| functions/resources | ||||
| Β Β resources.py | 26 | 26 | 0% | 1β37 |
| TOTAL | 1055 | 739 | 30% | Β |
| Tests | Skipped | Failures | Errors | Time |
|---|---|---|---|---|
| 109 | 2 π€ | 1 β | 0 π₯ | 0.583s β±οΈ |
π Text-Based Coverage Display
- name: Coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
text-instead-badge: trueDisplays coverage as 85% (42/50) instead of a badge image.
π Using Output Variables
- name: Coverage comment
id: coverage
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
- name: Dynamic Badges
uses: schneegans/dynamic-badges-action@v1.7.0
with:
auth: ${{ secrets.GIST_SECRET }}
gistID: your-gist-id
filename: coverage.json
label: Coverage
message: ${{ steps.coverage.outputs.coverage }}
color: ${{ steps.coverage.outputs.color }}
- name: Fail if coverage too low
if: ${{ steps.coverage.outputs.coverage < 80 }}
run: |
echo "Coverage is below 80%!"
exit 1π― Show Only Changed Files
- name: Coverage comment (changed files only)
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
report-only-changed-files: trueThis is particularly useful for large codebases where you want to focus on coverage for files modified in the PR.
π Workflow Dispatch Support
name: Manual Coverage Report
on:
workflow_dispatch:
inputs:
pr_number:
description: 'Pull Request number'
required: true
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- name: Coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
issue-number: ${{ github.event.inputs.pr_number }}β‘ Performance Optimization
For large coverage reports that might exceed GitHub's comment size limits:
- name: Coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
hide-report: true # Show only summary and badge
xml-skip-covered: true # Skip files with 100% coverage
report-only-changed-files: true # Only show changed files
remove-links-to-files: true # Remove clickable file links
remove-links-to-lines: true # Remove clickable line number linksLink Removal Options:
remove-links-to-files: true- Removes clickable links to files. Instead of[example.py](link), shows plainexample.pyremove-links-to-lines: true- Removes clickable links to line numbers. Instead of[14-18](link), shows plain14-18
These options significantly reduce comment size while preserving all coverage information.
Coverage badges automatically change color based on the percentage:
| Coverage | Badge | Color |
|---|---|---|
| 0-40% | Red | |
| 40-60% | Orange | |
| 60-80% | Yellow | |
| 80-90% | Green | |
| 90-100% | Bright Green |
If you want auto-update the coverage badge on your README, you can see the workflow example above.
View example outputs
With text-instead-badge: true, coverage displays as simple text:
85% (42/50)
Instead of a badge image:
Common Issues and Solutions
Issue: The action runs successfully but no comment appears on the PR.
Root Cause: This is usually caused by insufficient GitHub token permissions. The GITHUB_TOKEN needs write access to create/update PR comments.
Common Error Messages:
Error: Resource not accessible by integrationHttpError: Resource not accessible by integration403 Forbiddenerrors in the action logs
Solutions:
-
Add permissions block to your workflow (Recommended):
permissions: contents: read # Required for checkout and comparing commits pull-requests: write # Required for creating/updating PR comments
-
For
pushevents with commit comments, use:permissions: contents: write # Required for creating commit comments pull-requests: write # If you also want PR comments
-
Repository/Organization Settings (Admin access required):
- Go to Settings > Actions > General
- Under "Workflow permissions", select "Read and write permissions"
- Note: This affects all workflows, so adding permissions to individual workflows is more secure
-
Other checks:
- For
workflow_dispatchevents, provide theissue-numberinput - Verify
hide-commentis not set totrue - Check branch protection rules aren't blocking automated comments
- For
Why it works on forks but not main repos: Forks often have different default permission settings than the main repository. Organizations frequently set restrictive defaults for security.
Issue: pytest: error: unrecognized arguments: --cov --cov-report
Root Cause: The pytest-cov plugin is not installed. The --cov and --cov-report flags are provided by pytest-cov, not pytest itself.
Solution:
Install the pytest-cov package in your Python environment:
pip install pytest-covOr add it to your requirements.txt or pyproject.toml:
# requirements.txt
pytest>=8.0.0
pytest-cov>=5.0.0# pyproject.toml
[project]
dependencies = [
"pytest>=8.0.0",
"pytest-cov>=5.0.0",
]Make sure the installation step runs before executing pytest commands in your workflow:
- name: Install dependencies
run: |
pip install pytest pytest-cov
- name: Run tests with coverage
run: |
pytest --cov=src --cov-report=term-missing tests/Issue: "Comment is too long (maximum is 65536 characters)"
Solutions:
- Use
xml-skip-covered: trueto hide fully covered files - Enable
report-only-changed-files: true - Set
hide-report: trueto show only summary - Use
remove-links-to-files: trueto remove clickable file links - Use
remove-links-to-lines: trueto remove clickable line number links - Use
--cov-report=term-missing:skip-coveredin pytest
Issue: "GitHub Action Summary too big" (exceeds 1MB limit)
Solution: As of v1.1.55, the action automatically truncates summaries that exceed GitHub's 1MB limit.
Issue: "No such file or directory" errors
Solutions:
- Use absolute paths or paths relative to
$GITHUB_WORKSPACE - For Docker workflows, ensure volumes are mounted correctly
- Check that coverage files are generated before the action runs
Issue: Links in the coverage report point to wrong files or 404
Solutions:
- Set
default-branchto your repository's main branch - Use
coverage-path-prefixif your test paths differ from repository structure - Ensure the action runs on the correct commit SHA
We welcome all contributions! Please feel free to submit pull requests or open issues for bugs, feature requests, or improvements.
# Clone the repository
git clone https://github.com/MishaKav/pytest-coverage-comment.git
cd pytest-coverage-comment
# Install dependencies
npm install
# Run tests (if available)
npm test
# Build the action
npm run buildMIT Β© Misha Kav
For JavaScript/TypeScript projects using Jest: Check out jest-coverage-comment - a similar action with even more features for Jest test coverage.
If you find this action helpful, please consider giving it a β on GitHub!


