Skip to main content

Compare Coverage Action

The bffless/compare-coverage action compares test coverage reports against a baseline stored in BFFless, detecting coverage regressions in pull requests.

Example PR comment from compare-coverage action showing Coverage Report with metrics comparison table

See example PR comment

Multiple Format Support

This action supports LCOV, Istanbul, Cobertura, Clover, and JaCoCo coverage formats with automatic detection.

Use Cases

  • Detecting coverage regressions in pull requests
  • Enforcing minimum coverage thresholds
  • Tracking coverage trends across commits
  • Automated coverage reporting in CI/CD pipelines

Quick Start

- uses: bffless/compare-coverage@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path: ./coverage
baseline-alias: coverage-production
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}

The action will:

  1. Download baseline coverage from the specified alias
  2. Parse your local coverage report (auto-detecting format)
  3. Compare metrics: statements, branches, functions, and lines
  4. Upload results to BFFless
  5. Post a comment on the PR with comparison results

Full Workflow Example

name: Coverage Comparison

on:
push:
branches: [main]
pull_request:

permissions:
contents: read
pull-requests: write

jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Run tests with coverage
run: npm test -- --coverage

- name: Compare coverage
uses: bffless/compare-coverage@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path: ./coverage
baseline-alias: coverage-production
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}

# On main branch, update the baseline
- name: Update baseline
if: github.ref == 'refs/heads/main'
uses: bffless/upload-artifact@v1
with:
path: ./coverage
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}
alias: coverage-production

Inputs

InputRequiredDefaultDescription
pathYes-Path to coverage report file or directory
baseline-aliasYes-BFFless alias containing baseline coverage
api-urlYes-BFFless platform URL
api-keyYes-API key for authentication
formatNoautoCoverage format: lcov, istanbul, cobertura, clover, jacoco, or auto
thresholdNo0Allowed regression percentage (0 = any regression fails)
upload-resultsNotrueUpload current coverage to BFFless
aliasNopreviewAlias for uploaded results
repositoryNoCurrent repoRepository in owner/repo format
fail-on-regressionNotrueFail the action if coverage regresses
summaryNotrueGenerate GitHub step summary
commentNotruePost a comment on the PR with results
comment-headerNo## Coverage ReportHeader text for PR comment
note

The action requires GITHUB_TOKEN environment variable to post PR comments.

Outputs

OutputDescription
statementsStatement coverage percentage
branchesBranch coverage percentage
functionsFunction coverage percentage
linesLine coverage percentage
statements-deltaChange vs baseline
branches-deltaChange vs baseline
functions-deltaChange vs baseline
lines-deltaChange vs baseline
resultOverall result: pass, fail, or improved
reportJSON report contents
baseline-commit-shaCommit SHA of the baseline coverage
upload-urlURL to uploaded results

Using Outputs

- uses: bffless/compare-coverage@v1
id: coverage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path: ./coverage
baseline-alias: coverage-production
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}
fail-on-regression: false

- name: Check results
run: |
echo "Lines: ${{ steps.coverage.outputs.lines }}%"
echo "Delta: ${{ steps.coverage.outputs.lines-delta }}%"
echo "Result: ${{ steps.coverage.outputs.result }}"
if [ "${{ steps.coverage.outputs.result }}" == "fail" ]; then
echo "::warning::Coverage regressed!"
fi

Supported Coverage Formats

FormatFile TypesUsed By
lcov.info, .lcovJest, Vitest, c8, nyc, gcov
istanbulcoverage-final.jsonJest, nyc, Istanbul
cobertura.xmlPython (coverage.py), .NET, PHPUnit
clover.xmlPHP (PHPUnit), Java
jacoco.xmlJava, Kotlin, Scala

Path Resolution

The path input accepts either a file or directory:

# Direct file path
path: ./coverage/lcov.info

# Directory - action will find the coverage file
path: ./coverage

When a directory is provided, the action searches for these files (in order):

  • lcov.info, coverage.lcov
  • coverage-final.json, coverage.json
  • cobertura.xml, cobertura-coverage.xml, coverage.xml
  • clover.xml
  • jacoco.xml, jacocoTestReport.xml

Examples

Jest / Vitest (LCOV)

name: Coverage

on:
push:
branches: [main]
pull_request:

permissions:
contents: read
pull-requests: write

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'

- run: npm ci

- name: Run tests
run: npm test -- --coverage

- name: Compare coverage
if: github.event_name == 'pull_request'
uses: bffless/compare-coverage@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path: ./coverage/lcov.info
baseline-alias: coverage-production
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}

- name: Update baseline
if: github.ref == 'refs/heads/main'
uses: bffless/upload-artifact@v1
with:
path: ./coverage
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}
alias: coverage-production

Python (Cobertura)

- name: Run tests
run: pytest --cov=src --cov-report=xml

- name: Compare coverage
uses: bffless/compare-coverage@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path: ./coverage.xml
format: cobertura
baseline-alias: coverage-production
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}

Java (JaCoCo)

- name: Run tests
run: ./gradlew test jacocoTestReport

- name: Compare coverage
uses: bffless/compare-coverage@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path: ./build/reports/jacoco/test/jacocoTestReport.xml
format: jacoco
baseline-alias: coverage-production
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}

Allow Minor Regression

Allow up to 1% coverage regression without failing:

- uses: bffless/compare-coverage@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path: ./coverage
baseline-alias: coverage-production
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}
threshold: 1

Don't Fail on Regression

Report regressions but don't fail the build:

- uses: bffless/compare-coverage@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path: ./coverage
baseline-alias: coverage-production
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}
fail-on-regression: false

Skip PR Comment

Only generate step summary, no PR comment:

- uses: bffless/compare-coverage@v1
with:
path: ./coverage
baseline-alias: coverage-production
api-url: ${{ vars.BFFLESS_URL }}
api-key: ${{ secrets.BFFLESS_API_KEY }}
comment: false

How It Works

  1. Downloads baseline coverage from BFFless using the specified alias
  2. Parses both baseline and current coverage reports (auto-detecting format)
  3. Compares metrics: statements, branches, functions, and lines
  4. Calculates deltas and determines if coverage regressed
  5. Uploads current coverage to BFFless
  6. Posts a comment on the PR with comparison results
  7. Writes a GitHub step summary with detailed metrics

Coverage Metrics

MetricDescription
StatementsLines of code executed
BranchesConditional branches taken (if/else, switch)
FunctionsFunctions/methods called
LinesPhysical lines hit

Result Categories

ResultDescription
passNo regression (or within threshold)
failCoverage regressed beyond threshold
improvedCoverage increased

PR Comment Format

The action posts a comment on the PR with:

  • Overall coverage change summary
  • Metrics table comparing baseline vs current
  • Baseline and current commit information
  • Collapsible section with file-level changes

Troubleshooting

"No baseline found"

  • Verify the baseline-alias exists in BFFless
  • Upload an initial baseline using bffless/upload-artifact
  • Use continue-on-error: true for the first PR

"Unable to detect coverage format"

  • Specify the format explicitly using the format input
  • Ensure your coverage file has a standard name/extension
  • Check that the coverage file is not empty

PR Comment Not Posted

  • Ensure GITHUB_TOKEN is set in the environment
  • Verify pull-requests: write permission is granted
  • Check that the workflow is triggered by pull_request event

Coverage Metrics Don't Match

  • Different tools may calculate metrics differently
  • Ensure baseline and current use the same coverage tool
  • Check for differences in test configuration between runs