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@v1
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@v6
- name: Set up Python
uses: actions/setup-python@v6
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@v1
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) |
hide-emoji |
false |
Hide emojis from the test summary table |
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@v1
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@v1
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@v1
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.
Hide emojis for a cleaner appearance
- name: Coverage comment
uses: MishaKav/pytest-coverage-comment@v1
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
hide-emoji: trueDefault behavior (with emojis):
| Tests | Skipped | Failures | Errors | Time |
|---|---|---|---|---|
| 109 | 2 π€ | 1 β | 0 π₯ | 0.583s β±οΈ |
With hide-emoji: true:
| Tests | Skipped | Failures | Errors | Time |
|---|---|---|---|---|
| 109 | 2 | 1 | 0 | 0.583s |
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@v1
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@v1
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@v6
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@v1
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@v1
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@v1
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@v1
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@v1
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@v1
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: Permission denied when a pull request is opened from a fork.
Root Cause: GitHub restricts the GITHUB_TOKEN to read-only for pull_request events triggered from forks. This is a security measure β even if your workflow has pull-requests: write, the token is downgraded for fork PRs.
Solution: Use the pull_request_target event, which runs in the context of the base branch and gets a token with write permissions:
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: writeSecurity Warning:
pull_request_targetruns with access to the base repo's secrets and a write token. Never checkout and run untrusted code from the fork with these elevated permissions. Only check out the base branch code, or carefully limit what fork code is executed.
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!


