Skip to content

About

A CLI tool and GitHub Action to post Go code coverage reports as comment to your pull requests.

Topics

Resources

Contributing

Stars

126 stars

Watchers

2 watching

Forks

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

Go Coverage Report

A CLI tool and GitHub Action to post Go code coverage reports as comment to your pull requests.


go-coverage-report is a command line tool and a GitHub Action that parses code coverage results to create a tabular code coverage report. This markdown report is intended as pull request comment both to highlight the impact of the changes on the code coverage on all updated packages and to motivate the contributors to write tests for their code.

The provided tool and GitHub Action work without any third-party services which makes them fast, secure and very easy-to-setup in your own CI/CD pipeline.

Example

Example of a pull request comment created by go-coverage-report:

Example of a pull request comment created by go-coverage-report

Please note that by default, the "Coverage details" section is collapsed so the focus is more on the overall impact on the coverage per package.

There is no indicator of the total coverage of the project because the impact by changed package is typically a better indicator of the quality of the changes. Additionally, looking at coverage changes by package is more motivating as code coverage improvements are more pronounced (i.e. typically larger percentage values) when zooming into the package level, instead of comparing changes on the project level.

The "Impacted Packages" table lists all packages that contain changed files. If your tests also record coverage for other packages than their own (e.g. integration tests that you run with go test -coverpkg=./...), a change can affect the coverage of packages that contain no changed files. Those packages are listed as well, but only if their coverage actually changed. Packages without any statements that could be covered (e.g. packages that only contain tests) are never listed.

The last column is an emoji "score" that is based on the coverage change of the package. The following emojis are used:

  • 🌟 - The coverage of the package increased by > 20%
  • 🎉 - The coverage of the package increased by <= 20%
  • 👍 - The coverage of the package increased by <= 10%
  • 👎 - The coverage of the package decreased by <= 10%
  • 💀 - The coverage of the package decreased by > 10%, every 10% add another skull (up to five skulls)

Usage

The go-coverage-report tool ships with a GitHub Action that you can easily include in your own Workflows:

name: CI

# This setup assumes that you run the unit tests with code coverage in the same
# workflow that will also print the coverage report as comment to the pull request. 
# Therefore, you need to trigger this workflow when a pull request is (re)opened or
# when new code is pushed to the branch of the pull request. In addition, you also
# need to trigger this workflow when new code is pushed to the main branch because 
# we need to upload the code coverage results as artifact for the main branch as
# well since it will be the baseline code coverage.
# 
# We do not want to trigger the workflow for pushes to *any* branch because this
# would trigger our jobs twice on pull requests (once from "push" event and once
# from "pull_request->synchronize")
on:
  pull_request:
    types: [opened, reopened, synchronize]
  push:
    branches:
      - 'main'

jobs:
  unit_tests:
    name: "Unit tests"
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v4
        with:
          go-version: ^1.22

      # When you execute your unit tests, make sure to use the "-coverprofile" flag to write a 
      # coverage profile to a file. You will need the name of the file (e.g. "coverage.txt")
      # in the next step as well as the next job.
      - name: Test
        run: go test -cover -coverprofile=coverage.txt ./...

      - name: Archive code coverage results
        uses: actions/upload-artifact@v4
        with:
          name: code-coverage
          path: coverage.txt # Make sure to use the same file name you chose for the "-coverprofile" in the "Test" step

  code_coverage:
    name: "Code coverage report"
    if: github.event_name == 'pull_request' # Do not run when workflow is triggered by push to main branch
    runs-on: ubuntu-latest
    needs: unit_tests # Depends on the artifact uploaded by the "unit_tests" job
    permissions:
      contents:      read
      actions:       read  # to download code coverage results from "test" job
      pull-requests: write # write permission needed to comment on PR
    steps:
      - uses: fgrosse/go-coverage-report@v1.5.0 # Consider using a Git revision for maximum security
        with:
          coverage-artifact-name: "code-coverage" # can be omitted if you used this default value
          coverage-file-name: "coverage.txt" # can be omitted if you used this default value

Inputs

inputs:
  version:
    description: 'The exact version tag of the go-coverage-report tool to use.'
    required: true
    default: "v1.5.0"

  sha256sum:
    description: 'Optional SHA256 checksum of the tarball when downloading the go-coverage-report binary.'
    required: false

  coverage-artifact-name:
    description: 'The name of the artifact containing the code coverage results.'
    required: true
    default: "code-coverage"

  coverage-file-name:
    description: 'The name of the file containing the code coverage results.'
    required: true
    default: "coverage.txt"

  root-package:
    description: |
      The Go import path of the tested repository to add as a prefix to all paths of the
      changed files. This is useful to map the changed files (e.g., ["foo/my_file.go"]
      to their coverage profile which uses the full import path to identify the files
      (e.g., "github.com/fgrosse/example/foo/my_file.go"). Set this explicitly if your
      module path differs from the repository path, e.g. for major version suffixes
      ("github.com/owner/repo/v2"), vanity import paths or modules in a subdirectory.
    required: false
    default: "github.com/${{ github.repository }}"

  skip-comment:
    description: |
      Skip creating or updating the pull request comment. This may be useful when you want
      to generate the coverage report and modify it in your own scripts.
    required: false
    default: 'false'

  trim:
    description: Trim a prefix in the "Impacted Packages" column of the markdown report.
    required: false

  exclude:
    description: |
      Exclude files matching the given regular expression from the report. The expression
      is applied to the file names in the coverage profiles as well as to the changed files
      (after the root-package prefix was added).
    required: false
    
  github-baseline-workflow-ref:
    description: |
      The ref of the GitHub actions Workflow that produces the baseline coverage.
      By default, the GitHub Actions Workflow ref is used
      (e.g. "octocat/hello-world/.github/workflows/my-workflow.yml@refs/heads/my_branch").
      You can aso just pass the name of the Workflow file directly (e.g. "my-workflow.yml").
    default: ${{ github.workflow_ref }}
    required: false

  event-name:
    description: "The event that triggered the workflow used to filter baseline runs."
    default: "push"
    required: false

  target-branch:
    description: |
      The base branch to compare the coverage results against.
      Defaults to github.base_ref, which is only set on pull_request events.
      For other event types (e.g. push, schedule), set this explicitly.
    default: ${{ github.base_ref }}
    required: false

  baseline-run-id:
    description: |
      The ID of the GitHub Actions workflow run whose coverage artifact should be used as
      baseline. If set, this exact run is used and no other lookup happens. This is intended
      for scripting, e.g. to feed the baseline run ID from another workflow into this action.
    default: ""
    required: false

  baseline-sha:
    description: |
      The commit whose coverage should be used as baseline. The action uses the latest
      successful run of the baseline workflow for this commit on the target branch. If there
      is no such run, or its coverage artifact cannot be downloaded, the nearest ancestor of
      this commit with a usable run is used instead. Must be a full commit SHA. Defaults to the
      base commit of the pull request.
      Set to "" to disable matching by commit and always use the latest run on the target branch.
    default: ${{ github.event.pull_request.base.sha }}
    required: false

  baseline-search-depth:
    description: |
      The number of commits in the first-parent history of baseline-sha (including itself) that
      are searched for a usable baseline run (at most 100). If none of them has one, the latest
      successful run on the target branch is used. The history is read from a single page of
      100 commits, so if it contains merged branches, fewer commits may be searched.
    default: "30"
    required: false

  baseline-max-downloads:
    description: |
      Stop searching the history of baseline-sha after this many coverage artifacts could not be
      downloaded, e.g. because they expired or the coverage-artifact-name is wrong. The latest
      run on the target branch is still tried afterwards.
    default: "5"
    required: false

Baseline selection

The coverage of the pull request is compared against the coverage artifact of a successful run of the baseline workflow (see github-baseline-workflow-ref and event-name). The run is selected as follows:

  1. If baseline-run-id is set, exactly this run is used.
  2. Otherwise, if baseline-sha is set (by default the base commit of the pull request), the latest successful run for this commit on the target-branch is used.
  3. If this commit has no successful run (e.g. because it failed, was cancelled or is still running) or its coverage artifact cannot be downloaded, the action walks back through the first-parent history of this commit (see baseline-search-depth) and uses the nearest commit with a usable run. This way, the baseline never includes changes that were made after the base commit. The commits of merged branches are skipped.
  4. If none of these commits has a usable run, or baseline-sha is empty, the latest successful run on the target-branch is used.

Whenever the action does not use the run of the baseline-sha itself (steps 3 and 4), it emits a warning, because the coverage changes may then include commits that are not part of the pull request. In steps 2 and 3, the runs of each commit are filtered by branch, event and conclusion by the action itself instead of by the GitHub API, since the filtered API has been observed to return stale results (see #109).

The selected run (ID, commit, age and URL) is logged, and the "Coverage details" section of the pull request comment names the baseline commit and run. If the report does not compare against the base commit of the pull request, a caution callout at the top of the comment explains why: either an ancestor or the latest run on the target-branch was used instead, no successful baseline run was found, or the coverage artifact of the selected run could not be used (e.g. because it expired). In the latter two cases, the comment only shows the current coverage of the changed files.

Outputs

This action provides the following outputs:

  • coverage_report: The generated coverage report in Markdown format.
  • total_coverage: New overall test coverage percentage (e.g. "85.23").
  • coverage_delta: Change in coverage vs. the baseline (e.g. "-2.13"). Positive = increase.
  • coverage_trend: Direction of change — "increased", "decreased", or "no change".
  • total_statements: Total number of statements in the new coverage profile.
  • covered_statements: Number of covered statements.
  • missed_statements: Number of missed (uncovered) statements.

Using outputs in downstream steps

- uses: fgrosse/go-coverage-report@v1
  id: coverage

- name: Fail if coverage drops
  if: ${{ steps.coverage.outputs.coverage_trend == 'decreased' }}
  run: echo "Coverage decreased!" && exit 1

- name: Enforce minimum coverage
  run: |
    COV="${{ steps.coverage.outputs.total_coverage }}"
    if (( $(echo "$COV < 80.0" | bc -l) )); then
      echo "Coverage $COV% is below 80%" && exit 1
    fi

Limitations

  • Currently, code coverage profiles are uploaded as GitHub artifacts which automatically expire after 90 days. In a repository which receives changes only infrequently, this might lead to issues when trying to compare the code coverage of a pull request with the code coverage of the main branch (see #5). In this case, the report only shows the current coverage of the changed files and a note explains why.
  • Support for forks is limited since the necessary GITHUB_TOKEN permissions don't allow to post comments to the pull request of the base repository (see #15). If forks are important for you, this action might not be the best solution.
  • The "Coverage details" section only lists changed files. Packages that are listed only because their coverage changed (e.g. when using go test -coverpkg) have no per-file breakdown.
  • Requires actions/upload-artifact >= v4 (see this issue).

Built With

Development

This repository uses mise to pin the versions of Go, golangci-lint and shellcheck that are used in CI, so local checks produce the same results as the pipeline. After installing and activating mise, run the following in the repository root to install the pinned tools:

mise install

The tools are then on your PATH and you can run them as usual:

go test ./...
golangci-lint run ./...
shellcheck scripts/*.sh

The pinned versions live in mise.toml. Our GitHub Actions workflows install the very same versions via mise-action, so bumping a tool in mise.toml is all that is needed to update it everywhere.

Contributing

Please read CONTRIBUTING.md for details on our code of conduct and on the process for submitting pull requests to this repository.

Versioning

We use SemVer for versioning. All significant (e.g. breaking) changes are documented in the CHANGELOG.md. A list of all available versions can be found at the releases page.

Authors

  • Friedrich Große - Initial work - fgrosse
  • See also the list of contributors who participated in this project.

License

This project is licensed under the BSD-3-Clause License - see the LICENSE file for details.

About

A CLI tool and GitHub Action to post Go code coverage reports as comment to your pull requests.

Topics

Resources

Contributing

Stars

126 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages