Guidance for AI coding agents working in this repository. Humans may find it useful too.
pull-request-code-coverage is a CI plugin (a Go CLI shipped as a Docker image) that reports code
coverage for only the lines changed in a pull request — not whole files or the whole repo. It
reads a unified diff plus a coverage report, works out which changed lines your tests executed, and
writes the result to the CI console and (optionally) as a GitHub PR comment.
It is format-driven, not language-driven: support for a language is really support for that language's coverage report format.
coverage_type |
Language(s) | Format | Loader package |
|---|---|---|---|
jacoco |
JVM (Java/Kotlin/Scala) | JaCoCo XML | internal/plugin/coverage/jacoco |
cobertura |
Go | Cobertura XML | internal/plugin/coverage/cobertura |
python |
Python | coverage.py XML | internal/plugin/coverage/pythoncov |
lcov (aliases javascript, typescript) |
JS/TS | LCOV lcov.info |
internal/plugin/coverage/lcov |
- Go 1.26.3, module
github.com/target/pull-request-code-coverage. - Deps:
pkg/errors,sirupsen/logrus,stretchr/testify(seego.mod). Keep deps minimal. - Ships as a Docker image (
Dockerfile) published toghcr.io/target/pull-request-code-coverage.
go build ./... # compile everything
go test ./... # run all tests (do this before declaring done)
go test ./internal/plugin/coverage/lcov/... # run one package
go vet ./... # vet
make format # CHECK gofmt only — does NOT modify files; fails if anything is unformatted
gofmt -w . # actually auto-format (use this to fix what `make format` flags)
make lint # golangci-lint (downloads pinned v2.12.2 into ./bin on first run)Before finishing any change, all of these must pass: go test ./..., make format, make lint.
The CI (.github/workflows/test.yml) runs build, test, make format, and make lint on every PR.
Entry point main.go → plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout).
The runner (internal/plugin/runner.go) reads config from env vars (PARAMETER_*,
BUILD_PULL_REQUEST_NUMBER, REPOSITORY_ORG, REPOSITORY_NAME), the diff from stdin, and the
coverage report from the file at PARAMETER_COVERAGE_FILE.
stdin (unified diff) ──► sourcelines/unifieddiff ──► []domain.SourceLine
│ {Module,SrcDir,Pkg,FileName,LineNumber,LineValue}
coverage file ──► coverage.Loader.Load() ──► coverage.Report
│
calculator.DetermineCoverage(lines, report) ─────┘
└ for each line: report.GetCoverageData(...) ──► []domain.SourceLineCoverage
│
reporter.Forking{ Simple, GithubPullRequest }.Write(...)
├─ Simple → plain-text report to stdout (always)
└─ GithubPullRequest → Markdown PR comment (only if creds present)
Key packages:
internal/plugin/sourcelines/unifieddiff/changed_source_loader.go— parses the unified diff into changedSourceLines.PARAMETER_SOURCE_DIRScontrols how a path prefix is split intoSrcDir/Pkg.internal/plugin/coverage/—report.godefines the two interfaces every format implements:Loader.Load(file) (Report, error)andReport.GetCoverageData(module, sourceDir, pkg, fileName, lineNumber) (*CoverageData, bool).internal/plugin/calculator/calculator.go— joins changed lines to coverage data.internal/plugin/reporter/—simple.go(console),github_pr.go(PR comment markdown),forking.go(runs all reporters),utils.go(filePath,lineDescription). Per-file aggregation (collectFileCoverage) andcoverageStatusEmojilive ingithub_pr.goand are shared by both reporters (same package).internal/plugin/domain/domain.go— core types. Coverage is counted in instructions (CoveredInstructionCount/MissedInstructionCount), not lines (see below).
- Lines vs. instructions are deliberately different. For JaCoCo, one source line maps to several JVM bytecode instructions, so a line can be partly covered. For Go/Python/LCOV the loaders emit exactly 1 instruction per line. The reports surface both units on purpose — do not "fix" this as if it were a bug. The user has explicitly asked for this distinction to be clear.
- Two output formats, one dataset.
Simple(plain text, stdout) andGithubPullRequest(Markdown) render the same data differently. Change both if you change what's reported. - The PR comment is posted only when
gh_api_keyANDBUILD_PULL_REQUEST_NUMBERANDREPOSITORY_ORGANDREPOSITORY_NAMEare all present; otherwise console-only.GithubPullRequestalso returns early when there are zero changed lines with coverage data. - Path matching differs by loader.
jacoco/coberturamatch using the report's source root;python/lcovmatch on the repo-relative path (andlcovalso suffix-matches absoluteSF:paths). When adding/altering a loader, preserve its matching contract.
- Create
internal/plugin/coverage/<name>/report.goimplementingcoverage.Loaderandcoverage.Report. Mirror an existing loader; per-loader helpers are duplicated by convention (each loader has its ownsilentlyCall, path helpers, etc. — that's the established style here, not shared utilities). - Add a
caseingetCoverageReportLoaderininternal/plugin/runner.go(and the import). - Add a fixture under
internal/test/(e.g.example_<name>.<ext>) and a matching diff fixture. - Add a loader unit test
report_test.goand a full end-to-end test ininternal/plugin/runner_test.go(see golden-test note below). - Update
README.md: the supported-formats table, a usage section, and thecoverage_typeparameter values.
internal/plugin/runner_test.go contains golden-string assertions for the exact console output
(buf.String()) and the exact PR comment body. If you change anything the reporters print, these
goldens will fail and must be regenerated exactly — do not hand-edit them (emoji, box-drawing
chars, tabs, and trailing spaces all matter).
To regenerate, write a temporary internal/plugin/dump_test.go that runs NewRunner().Run(...) for
the affected scenarios against an httptest server, and prints strconv.Quote(output) for both the
console buffer and the captured request body. Copy those quoted strings into the goldens, then
delete the dump test. (This pattern has been used repeatedly here; it is the reliable way to keep
goldens byte-exact.)
Other test notes:
- Mocks live in
internal/test/mocks(MockPropertyGetter,WithMockGithubAPI).propGetteruses testify mock withAssertExpectations, so only stub the env vars the runner actually reads for that scenario. _test.gofiles are exempt fromfunlen,goconst,gosec(see.golangci.yml).
- Lint is strict (
.golangci.yml:gocyclo,gosec,unused,staticcheck,errcheck,unparam,goconst, etc.). Notably: preferfmt.Fprintf(&b, ...)overb.WriteString(fmt.Sprintf(...)); remove dead code (unused); file opens with user-supplied paths need a// nolint: gosecwith a reason (see existing loaders). make formatonly checks; rungofmt -w .to actually format.- This plugin dogfoods itself:
.github/workflows/pr-coverage.ymlruns it on this repo's own PRs, so reporter changes will visibly change the coverage comment on your PR. That's expected. release.ymlbuilds and pushes the Docker image to GHCR on GitHub release (uses the built-inGITHUB_TOKEN, no extra secrets).
- Do not add AI/agent co-author trailers or "Generated with…" lines to commits. Commits are attributed solely to the repository author. Write a normal, descriptive commit message.
- Only commit/push when explicitly asked. If on the default branch, create a feature branch first.
- A PR is ready only when
go test ./...,make format, andmake lintall pass.