Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions .github/workflows/test-annotate-coverage.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: Test annotate-coverage action

on:
push:
branches:
- main
paths:
- "actions/annotate-coverage/**"
- ".github/workflows/test-annotate-coverage.yaml"

pull_request:
paths:
- "actions/annotate-coverage/**"
- ".github/workflows/test-annotate-coverage.yaml"
types:
- edited
- opened
- ready_for_review
- synchronize

merge_group:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
actions: write # needed for cache

steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Setup go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
check-latest: true
go-version-file: "actions/annotate-coverage/go.mod"
cache-dependency-path: "actions/annotate-coverage/go.sum"

- name: golangci-lint
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
with:
version: latest
working-directory: actions/annotate-coverage

- name: Test Go code
shell: bash
working-directory: actions/annotate-coverage
run: go test -race -short ./...

# End-to-end: run the action against this module's own coverage and assert it
# annotates the intentionally-uncovered demo function. This exercises the full
# wiring (inputs -> flags -> git diff -> coverage intersection -> ::notice) and
# doubles as a live signal that the action still detects uncovered code.
e2e:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
actions: write # needed for cache

steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Setup go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
check-latest: true
go-version-file: "actions/annotate-coverage/go.mod"
cache-dependency-path: "actions/annotate-coverage/go.sum"

- name: Generate coverage for this module
shell: bash
working-directory: actions/annotate-coverage
env:
COVERAGE_DIR: ${{ runner.temp }}/coverage
run: |
mkdir -p "${COVERAGE_DIR}"
go test ./... -covermode=set -coverprofile="${COVERAGE_DIR}/coverage.out"

- name: Present the demo fixture as an added file
shell: bash
# annotate-coverage only flags lines *added* in the diff. uncovered_demo.go
# is committed, so un-stage it to make it appear as a new file in the
# working-tree diff, mirroring a PR that adds uncovered code.
run: git rm --cached --quiet actions/annotate-coverage/internal/coverage/uncovered_demo.go

- name: Run annotate-coverage on its own coverage
uses: ./actions/annotate-coverage
with:
coverage-path: ${{ runner.temp }}/coverage
format: GitHubAnnotations

- name: Assert the uncovered demo function was annotated
shell: bash
env:
COVERAGE_DIR: ${{ runner.temp }}/coverage
# A composite action's stdout is not available to the caller, so re-run
# the binary the previous step built (action.yaml builds it into
# RUNNER_TEMP) and assert it flagged the deliberately-uncovered demo.
run: |
output="$("${RUNNER_TEMP}/annotate-coverage" --coverage "${COVERAGE_DIR}" --format GitHubAnnotations)"
echo "${output}"
if ! grep -q 'uncovered_demo.go' <<<"${output}"; then
echo "::error::annotate-coverage did not report uncovered_demo.go as uncovered"
exit 1
fi
echo "annotate-coverage reported the demo function as uncovered, as expected"
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@
"actions/socket-export-sbom": "0.1.2",
"actions/issues-update-project-status": "0.2.1",
"actions/go-flaky-tests": "0.1.1",
"actions/wait-for-docker-publish": "0.2.0"
"actions/wait-for-docker-publish": "0.2.0",
"actions/annotate-coverage": "0.1.0"
}
20 changes: 20 additions & 0 deletions actions/annotate-coverage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Built binary at the action root
/annotate-coverage
*.exe
*.exe~
*.dll
*.so
*.dylib

# Go test binaries
*.test

# Coverage output for local runs (testdata/ fixtures are still tracked)
/.coverage/

# Re-allow tracked source/fixture directories named `coverage/` (repo root
# .gitignore excludes `coverage/` to ignore coverage-output artifacts).
!internal/coverage/
!internal/coverage/**
!testdata/coverage/
!testdata/coverage/**
1 change: 1 addition & 0 deletions actions/annotate-coverage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
86 changes: 86 additions & 0 deletions actions/annotate-coverage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# annotate-coverage

Highlights uncovered lines in a PR diff using Go coverage data. Parses Go
coverage files from a directory, intersects them with a git diff, and prints
the uncovered lines as text, Markdown, or GitHub Actions workflow commands
(`::notice file=...,line=...::...`) that GitHub Actions renders as PR
annotations on the changed lines.

<!-- x-release-please-start-version -->

```yaml
name: Coverage
on:
pull_request:

permissions:
contents: read

jobs:
annotate-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-go@v6
with:
go-version: "1.25"

- name: Run tests with coverage
run: |
mkdir -p .coverage
go test ./... -coverprofile=.coverage/coverage.out

- name: Annotate uncovered lines in PR diff
uses: grafana/shared-workflows/actions/annotate-coverage@annotate-coverage/v0.1.0
with:
coverage-path: .coverage
base-ref: ${{ github.event.pull_request.base.sha }}
commit-sha: ${{ github.event.pull_request.head.sha }}
```

<!-- x-release-please-end-version -->

## Inputs

| Input | Description | Required | Default |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------- |
| `coverage-path` | Directory containing Go coverage files (`*.out`) | No | `.coverage` |
| `format` | Output format: `Text`, `Markdown`, or `GitHubAnnotations` | No | `GitHubAnnotations` |
| `base-ref` | Base ref to compare against (e.g., the PR base SHA). When set, diff is `<base-ref>..<commit-sha or HEAD>`. | No | - |
| `commit-sha` | Commit ref to compare to. With `base-ref`, diff is `<base-ref>..<commit-sha>`. Without `base-ref`, diff is the changes introduced by `<commit-sha>` alone. | No | - |
| `repository-directory` | Path to the git repository to analyze | No | `${{ github.workspace }}` |
| `go-version` | Go version used to build the action binary | No | `1.25` |

## Diff modes

- **PR diff (recommended in CI):** set `base-ref` to the PR base SHA and
`commit-sha` to the head SHA. The diff is `<base-ref>..<commit-sha>`.
- **Branch vs HEAD:** set only `base-ref`. The diff is `<base-ref>..HEAD`.
- **Single commit:** set only `commit-sha`. The diff is the changes introduced
by that commit.
- **Working tree:** leave both empty. The diff is `git diff` against the
working tree (useful for local runs).

## Output formats

- `Text` — human-readable output for logs.
- `Markdown` — table-formatted output for PR comments or summaries.
- `GitHubAnnotations` — `::notice file=...,line=...::...` workflow commands
that GitHub Actions renders as PR annotations on the changed lines. No
GitHub API client is involved — annotations are emitted via workflow
commands on stdout.

## Notes

- Coverage files are merged at the block level using the `gocovmerge`
algorithm, so multiple `*.out` files in `coverage-path` are combined before
analysis.
- Only Go files (`.go`) are considered. Binary files, deleted files, and
non-Go files are skipped.
- Lines that are not in any coverage block (comments, blank lines, package
declarations, etc.) are excluded from the uncovered count.
- For PR runs, check out the repository with `fetch-depth: 0` so the base ref
is available locally.
63 changes: 63 additions & 0 deletions actions/annotate-coverage/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: "Annotate Coverage"
description: "Annotate uncovered lines in a PR diff using Go coverage data"
author: "Grafana Labs"

inputs:
coverage-path:
description: "Directory containing Go coverage files (*.out)"
required: false
default: ".coverage"
format:
description: "Output format: Text, Markdown, or GitHubAnnotations"
required: false
default: "GitHubAnnotations"
base-ref:
description: "Base ref to compare against (e.g., the PR base branch). When set, diff is computed as <base-ref>..<commit-sha or HEAD>."
required: false
default: ""
commit-sha:
description: "Commit ref to compare to. With base-ref, diff is <base-ref>..<commit-sha>. Without base-ref, diff is the changes introduced by <commit-sha>. Defaults to HEAD when used with base-ref."
required: false
default: ""
repository-directory:
description: "Path to the git repository to analyze"
required: false
default: ${{ github.workspace }}
go-version:
description: "Go version to use when building the binary"
required: false
default: "1.25"

runs:
using: "composite"
steps:
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ inputs.go-version }}

- name: Build annotate-coverage
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
run: |
cd "${ACTION_PATH}"
go build -o "${RUNNER_TEMP}/annotate-coverage" ./cmd/annotate-coverage

- name: Run annotate-coverage
shell: bash
working-directory: ${{ inputs.repository-directory }}
env:
COVERAGE_PATH: ${{ inputs.coverage-path }}
FORMAT: ${{ inputs.format }}
BASE_REF: ${{ inputs.base-ref }}
COMMIT_SHA: ${{ inputs.commit-sha }}
run: |
args=(--coverage "${COVERAGE_PATH}" --format "${FORMAT}")
if [[ -n "${BASE_REF}" ]]; then
args+=(--base "${BASE_REF}")
fi
if [[ -n "${COMMIT_SHA}" ]]; then
args+=(--commit "${COMMIT_SHA}")
fi
"${RUNNER_TEMP}/annotate-coverage" "${args[@]}"
Loading
Loading