A reusable composite GitHub Action that parses LCOV coverage files, reports coverage summaries, and optionally enforces coverage thresholds by comparing against a baseline.
- Automatic baseline management: Stores coverage on main-branch pushes, auto-retrieves it on PRs
- Language-agnostic: Works with any language that produces LCOV data (Dart, Go, Python, TypeScript, C/C++, Ruby, etc.) — file extensions are derived automatically from the LCOV data
- Summary-only mode: Report overall and per-file coverage without enforcing any rules
- Overall ratchet check: Ensure overall coverage does not decrease compared to a baseline
- New-file threshold: Require new files to meet a minimum coverage percentage
- Changed-file ratchet: Prevent per-file coverage from decreasing on modified files
- PR comments: Automatically post or update a coverage summary comment on pull requests
- Step summary: Write a markdown summary to
$GITHUB_STEP_SUMMARY
- name: Run tests with coverage
run: flutter test --coverage
- name: Check coverage
uses: pento/lcov-coverage-check@main
with:
new-file-minimum-coverage: 80
path: "lib/"
changed-file-no-decrease: true
ignore-patterns: |
*.g.dart
*.freezed.dart
github-token: ${{ secrets.GITHUB_TOKEN }}Note: When using
github-token, your workflow needsactions: readpermission. See Token permissions.
If your project has source code in multiple directories, specify multiple path prefixes (one per line):
- name: Check coverage
uses: pento/lcov-coverage-check@main
with:
path: |
lib/
src/
github-token: ${{ secrets.GITHUB_TOKEN }}- On main push: summary report + stores LCOV as
lcov-baselineartifact - On PR: auto-retrieves baseline, auto-detects git refs, runs full comparison, posts PR comment, stores
lcov-coverageartifact
Report coverage without enforcing any rules or managing artifacts. Always passes.
- name: Coverage summary
uses: pento/lcov-coverage-check@mainname: Coverage
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
coverage:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run tests with coverage
run: flutter test --coverage
- name: Check coverage
uses: pento/lcov-coverage-check@main
with:
new-file-minimum-coverage: 80
path: "lib/"
changed-file-no-decrease: true
ignore-patterns: |
*.g.dart
*.freezed.dart
github-token: ${{ secrets.GITHUB_TOKEN }}If your repo produces multiple LCOV files (e.g., Go backend + TypeScript frontend), use coverage-label to run the action multiple times without conflicts:
- name: Check Go coverage
uses: pento/lcov-coverage-check@main
with:
lcov-file: go-coverage.lcov
coverage-label: go
path: ""
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Check TypeScript coverage
uses: pento/lcov-coverage-check@main
with:
lcov-file: ts-coverage.lcov
coverage-label: frontend
path: "src/"
github-token: ${{ secrets.GITHUB_TOKEN }}All labels share a single consolidated PR comment, with each label occupying its own section that is independently updated. Each label also gets its own baseline artifact (e.g., lcov-baseline-go) and PR coverage artifact (e.g., lcov-coverage-frontend).
Labels are normalized to lowercase alphanumeric characters and hyphens.
| Input | Required | Default | Description |
|---|---|---|---|
lcov-file |
no | coverage/lcov.info |
Path to current LCOV coverage file |
new-file-minimum-coverage |
no | 80 |
Minimum coverage percentage for new files (0-100) |
path |
no | lib/ |
Path prefixes for file-level checks, one per line. Empty = all paths |
changed-file-no-decrease |
no | true |
Require that per-file coverage of modified files does not decrease vs baseline |
ignore-patterns |
no | '' |
File patterns to exclude from coverage checks (one glob pattern per line) |
coverage-label |
no | '' |
Label to distinguish multiple coverage checks. Each label gets its own section in the consolidated PR comment and its own baseline artifact |
github-token |
no | '' |
GitHub token for PR comments and artifact management. If empty, runs in summary-only mode |
| Output | Description |
|---|---|
overall-coverage |
Current overall coverage percentage (e.g., 87.50) |
baseline-coverage |
Baseline coverage percentage (empty if summary-only) |
passed |
'true' or 'false' |
coverage-label |
Sanitized coverage label (may differ from input) |
baseline-artifact-downloaded |
'true' if baseline was auto-retrieved from a previous run |
When github-token is provided, the action automatically manages baseline coverage artifacts:
- On pushes to the default branch (e.g.,
main): the current LCOV file is uploaded as anlcov-baselineartifact, overwriting any previous baseline. - On pull requests: the action retrieves the
lcov-baselineartifact from the latest successful default-branch run of the same workflow. It also extractsbase.shaandhead.shafrom the PR event payload forgit diffoperations. - On pull requests: the current LCOV file is also uploaded as an
lcov-coverageartifact.
When coverage-label is set, artifact names are suffixed (e.g., lcov-baseline-go, lcov-coverage-frontend). Each label tracks its own independent baseline.
If no baseline artifact is found (e.g., first run), the action falls back to summary-only mode gracefully.
The github-token needs the following permissions:
actions: read— to list workflow runs and download artifactspull-requests: write— to post/update PR comments
Baseline artifacts follow your repository's default artifact retention policy. You can configure this in your repository settings or workflow file.
- Parses the LCOV file and prints overall + per-file coverage
- Writes a markdown summary to
$GITHUB_STEP_SUMMARY - Always exits 0 and sets
passedtotrue
- Overall ratchet: Current overall coverage must be >= baseline overall coverage
- New-file check: New source files (filtered to file types found in the LCOV data, detected via
git diff --diff-filter=A) must meet thenew-file-minimum-coveragethreshold. Files with no instrumentable lines (LF:0) pass automatically. Files not found in the LCOV data are treated as 0% coverage. - Changed-file ratchet: If
changed-file-no-decreaseistrue, modified source files (filtered to file types found in the LCOV data) must not have decreased per-file coverage. Files not present in the baseline LCOV data are skipped.
When ignore-patterns is provided, matching files are excluded from all coverage checks:
- LCOV filtering: Records for matching files are removed from LCOV data before any calculations, affecting both overall and per-file coverage numbers.
- New-file check: New files matching a pattern are skipped.
- Changed-file ratchet: Modified files matching a pattern are skipped.
Patterns use standard glob syntax: * matches any characters (including path separators), ? matches a single character, and [...] matches character classes. One pattern per line.
Common examples:
*.g.dart— Dart code generation output*.freezed.dart— Freezed-generated files*.generated.go— Go generated codelib/generated/*— all files under a directory
When github-token is provided and the action runs in a pull request context, a markdown comment is posted (or updated) on the PR. All coverage checks share a single consolidated comment, identified by a hidden HTML marker. Each coverage-label occupies its own section within the comment, independently updated via a read-modify-write cycle with retry to handle concurrent updates. Old-format per-label comments (from previous versions) are automatically cleaned up on the first run.
For new/modified file detection, the action needs access to both the base and head commits. It will automatically attempt to fetch them from the remote, but if your checkout uses a very restrictive configuration (e.g., no remote access), you may need fetch-depth: 0 in your actions/checkout step. If the refs cannot be fetched or resolved, a ::warning:: annotation is emitted and the action continues without file-level checks rather than failing.
- Empty or missing LCOV files: Treated as 0% coverage (not an error)
- First run (no baseline): Runs in summary-only mode. Baseline stored for next PR.
- Expired artifact: Filtered by
expired == false. Graceful fallback to summary-only. - Fork PRs: Token may lack
actions: read. ERR trap handles graceful fallback. - New file not in LCOV data: Treated as 0% coverage, fails if below threshold
- New file with
LF:0: No instrumentable lines, passes automatically - Modified file not in baseline LCOV: Skipped (new to coverage tracking)
- Modified file not in current LCOV: Treated as 0% coverage
- Ignored files: Completely excluded from LCOV data, overall coverage, and per-file checks
- Changing a
coverage-label: Renaming a label leaves the old section in the consolidated comment (it won't be updated or removed until the comment is recreated). The old baseline artifact is no longer used. The new label starts fresh.
scripts/
lib/
common.sh # Shared helpers (write_output, append_summary)
lcov.sh # LCOV parsing and numeric helpers
filter.sh # File filtering / ignore-pattern logic
comment.sh # PR comment section management
check-coverage.sh # Main coverage checking logic
retrieve-baseline.sh # Baseline artifact retrieval
test/
helpers/
runner.sh # Test framework (pass/fail/run_test)
git-helpers.sh # Git repo setup/teardown for tests
tests/ # Test files, sourced by run-tests.sh
fixtures/ # LCOV fixture files
run-tests.sh # Test runner
action.yml # GitHub Actions composite action definitionThe script can be run locally without GitHub Actions:
INPUT_LCOV_FILE=coverage/lcov.info \
INPUT_LCOV_BASE=baseline/lcov.info \
INPUT_BASE_REF=main \
INPUT_HEAD_REF=HEAD \
INPUT_NEW_FILE_MINIMUM_COVERAGE=80 \
INPUT_PATH=lib/ \
INPUT_CHANGED_FILE_NO_DECREASE=true \
INPUT_IGNORE_PATTERNS="" \
INPUT_COVERAGE_LABEL="" \
INPUT_GITHUB_TOKEN="" \
./scripts/check-coverage.sh./test/run-tests.shMIT License. See LICENSE for details.