Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
60 changes: 60 additions & 0 deletions .github/workflows/test-annotate-coverage.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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 ./...
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@
"actions/component-change-detection": "1.0.1",
"actions/socket-export-sbom": "0.1.1",
"actions/issues-update-project-status": "0.2.0",
"actions/go-flaky-tests": "0.1.0"
"actions/go-flaky-tests": "0.1.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[@]}"
96 changes: 96 additions & 0 deletions actions/annotate-coverage/cmd/annotate-coverage/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package main

import (
"context"
"fmt"
"os"

"github.com/grafana/shared-workflows/actions/annotate-coverage/internal/diff"
"github.com/grafana/shared-workflows/actions/annotate-coverage/internal/local"
"github.com/spf13/cobra"
)

var (
// Version information (set via ldflags during build)
version = "dev"
commit = "unknown"
date = "unknown"

// CLI flags
coveragePath string
format string
baseRef string
commitSHA string
)

func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

var rootCmd = &cobra.Command{
Use: "annotate-coverage",
Short: "annotate-coverage - Highlight uncovered lines in a diff",
Long: `annotate-coverage analyzes Go coverage data against the current git diff
to find uncovered lines in your changes.

Usage:
annotate-coverage [flags]

This command analyzes coverage files in the specified directory and compares them
against the current git diff to highlight uncovered lines in your changes.

Diff modes:
- Default: Compares against current working directory changes (git diff)
- --base <ref>: Compares base ref to HEAD (git diff <base>..HEAD)
- --base <ref> --commit <ref>: Compares two refs (git diff <base>..<commit>)
- --commit <sha>: Shows changes for a specific commit (git diff-tree <sha>)`,
RunE: run,
}

var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("annotate-coverage %s\n", version)
fmt.Printf(" commit: %s\n", commit)
fmt.Printf(" built: %s\n", date)
},
}

func init() {
// Add subcommands
rootCmd.AddCommand(versionCmd)

// Define flags
rootCmd.Flags().StringVar(&coveragePath, "coverage", ".coverage", "Directory containing coverage files")
rootCmd.Flags().StringVar(&format, "format", "Text", "Output format (Text, Markdown, GitHubAnnotations)")
rootCmd.Flags().StringVar(&baseRef, "base", "", "Base reference to compare from (can be combined with --commit)")
rootCmd.Flags().StringVar(&commitSHA, "commit", "", "Commit reference to compare to (defaults to HEAD when used with --base)")
}

func run(cmd *cobra.Command, args []string) error {
// Create appropriate DiffSource based on flags
var diffSource diff.DiffSource

if baseRef != "" {
// --base flag: compare base to commit (defaults to HEAD if commit not specified)
// Supports both: --base <ref> and --base <ref> --commit <ref>
diffSource = diff.NewGitBaseDiffSource(baseRef, commitSHA, "")
} else if commitSHA != "" {
// --commit flag only: single commit analysis
diffSource = diff.NewGitCommitDiffSource(commitSHA, "")
} else {
// Default: use local git diff (working directory changes)
diffSource = diff.NewLocalDiffSource("")
}

runner := local.NewRunner(local.Config{
CoveragePath: coveragePath,
Format: format,
}, local.WithDiffSource(diffSource))

return runner.Run(context.Background())
}
20 changes: 20 additions & 0 deletions actions/annotate-coverage/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module github.com/grafana/shared-workflows/actions/annotate-coverage

go 1.25.4

require (
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
golang.org/x/tools v0.42.0
)

require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
35 changes: 35 additions & 0 deletions actions/annotate-coverage/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading
Loading