|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +Guidance for AI coding agents working in this repository. Humans may find it useful too. |
| 4 | + |
| 5 | +## What this project is |
| 6 | + |
| 7 | +`pull-request-code-coverage` is a CI plugin (a Go CLI shipped as a Docker image) that reports code |
| 8 | +coverage for **only the lines changed in a pull request** — not whole files or the whole repo. It |
| 9 | +reads a unified diff plus a coverage report, works out which changed lines your tests executed, and |
| 10 | +writes the result to the CI console and (optionally) as a GitHub PR comment. |
| 11 | + |
| 12 | +It is **format-driven, not language-driven**: support for a language is really support for that |
| 13 | +language's coverage report format. |
| 14 | + |
| 15 | +| `coverage_type` | Language(s) | Format | Loader package | |
| 16 | +|---|---|---|---| |
| 17 | +| `jacoco` | JVM (Java/Kotlin/Scala) | JaCoCo XML | `internal/plugin/coverage/jacoco` | |
| 18 | +| `cobertura` | Go | Cobertura XML | `internal/plugin/coverage/cobertura` | |
| 19 | +| `python` | Python | coverage.py XML | `internal/plugin/coverage/pythoncov` | |
| 20 | +| `lcov` (aliases `javascript`, `typescript`) | JS/TS | LCOV `lcov.info` | `internal/plugin/coverage/lcov` | |
| 21 | + |
| 22 | +## Tech stack |
| 23 | + |
| 24 | +- **Go 1.26.3**, module `github.com/target/pull-request-code-coverage`. |
| 25 | +- Deps: `pkg/errors`, `sirupsen/logrus`, `stretchr/testify` (see `go.mod`). Keep deps minimal. |
| 26 | +- Ships as a Docker image (`Dockerfile`) published to `ghcr.io/target/pull-request-code-coverage`. |
| 27 | + |
| 28 | +## Commands (run from repo root) |
| 29 | + |
| 30 | +```bash |
| 31 | +go build ./... # compile everything |
| 32 | +go test ./... # run all tests (do this before declaring done) |
| 33 | +go test ./internal/plugin/coverage/lcov/... # run one package |
| 34 | +go vet ./... # vet |
| 35 | + |
| 36 | +make format # CHECK gofmt only — does NOT modify files; fails if anything is unformatted |
| 37 | +gofmt -w . # actually auto-format (use this to fix what `make format` flags) |
| 38 | + |
| 39 | +make lint # golangci-lint (downloads pinned v2.12.2 into ./bin on first run) |
| 40 | +``` |
| 41 | + |
| 42 | +Before finishing any change, all of these must pass: `go test ./...`, `make format`, `make lint`. |
| 43 | +The CI (`.github/workflows/test.yml`) runs build, test, `make format`, and `make lint` on every PR. |
| 44 | + |
| 45 | +## Architecture & data flow |
| 46 | + |
| 47 | +Entry point `main.go` → `plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout)`. |
| 48 | +The runner (`internal/plugin/runner.go`) reads **config from env vars** (`PARAMETER_*`, |
| 49 | +`BUILD_PULL_REQUEST_NUMBER`, `REPOSITORY_ORG`, `REPOSITORY_NAME`), the **diff from stdin**, and the |
| 50 | +**coverage report from the file** at `PARAMETER_COVERAGE_FILE`. |
| 51 | + |
| 52 | +``` |
| 53 | +stdin (unified diff) ──► sourcelines/unifieddiff ──► []domain.SourceLine |
| 54 | + │ {Module,SrcDir,Pkg,FileName,LineNumber,LineValue} |
| 55 | +coverage file ──► coverage.Loader.Load() ──► coverage.Report |
| 56 | + │ |
| 57 | + calculator.DetermineCoverage(lines, report) ─────┘ |
| 58 | + └ for each line: report.GetCoverageData(...) ──► []domain.SourceLineCoverage |
| 59 | + │ |
| 60 | + reporter.Forking{ Simple, GithubPullRequest }.Write(...) |
| 61 | + ├─ Simple → plain-text report to stdout (always) |
| 62 | + └─ GithubPullRequest → Markdown PR comment (only if creds present) |
| 63 | +``` |
| 64 | + |
| 65 | +Key packages: |
| 66 | +- `internal/plugin/sourcelines/unifieddiff/changed_source_loader.go` — parses the unified diff into |
| 67 | + changed `SourceLine`s. `PARAMETER_SOURCE_DIRS` controls how a path prefix is split into `SrcDir`/`Pkg`. |
| 68 | +- `internal/plugin/coverage/` — `report.go` defines the two interfaces every format implements: |
| 69 | + `Loader.Load(file) (Report, error)` and `Report.GetCoverageData(module, sourceDir, pkg, fileName, lineNumber) (*CoverageData, bool)`. |
| 70 | +- `internal/plugin/calculator/calculator.go` — joins changed lines to coverage data. |
| 71 | +- `internal/plugin/reporter/` — `simple.go` (console), `github_pr.go` (PR comment markdown), |
| 72 | + `forking.go` (runs all reporters), `utils.go` (`filePath`, `lineDescription`). Per-file |
| 73 | + aggregation (`collectFileCoverage`) and `coverageStatusEmoji` live in `github_pr.go` and are |
| 74 | + shared by both reporters (same package). |
| 75 | +- `internal/plugin/domain/domain.go` — core types. Coverage is counted in **instructions** |
| 76 | + (`CoveredInstructionCount`/`MissedInstructionCount`), not lines (see below). |
| 77 | + |
| 78 | +## Concepts you must not "simplify" away |
| 79 | + |
| 80 | +- **Lines vs. instructions are deliberately different.** For JaCoCo, one source line maps to several |
| 81 | + JVM bytecode *instructions*, so a line can be partly covered. For Go/Python/LCOV the loaders emit |
| 82 | + exactly 1 instruction per line. The reports surface both units on purpose — do not "fix" this as if |
| 83 | + it were a bug. The user has explicitly asked for this distinction to be clear. |
| 84 | +- **Two output formats, one dataset.** `Simple` (plain text, stdout) and `GithubPullRequest` |
| 85 | + (Markdown) render the same data differently. Change both if you change what's reported. |
| 86 | +- **The PR comment is posted only** when `gh_api_key` AND `BUILD_PULL_REQUEST_NUMBER` AND |
| 87 | + `REPOSITORY_ORG` AND `REPOSITORY_NAME` are all present; otherwise console-only. `GithubPullRequest` |
| 88 | + also returns early when there are zero changed lines with coverage data. |
| 89 | +- **Path matching differs by loader.** `jacoco`/`cobertura` match using the report's source root; |
| 90 | + `python`/`lcov` match on the **repo-relative path** (and `lcov` also suffix-matches absolute |
| 91 | + `SF:` paths). When adding/altering a loader, preserve its matching contract. |
| 92 | + |
| 93 | +## How to add a new coverage format |
| 94 | + |
| 95 | +1. Create `internal/plugin/coverage/<name>/report.go` implementing `coverage.Loader` and |
| 96 | + `coverage.Report`. Mirror an existing loader; per-loader helpers are duplicated by convention |
| 97 | + (each loader has its own `silentlyCall`, path helpers, etc. — that's the established style here, |
| 98 | + not shared utilities). |
| 99 | +2. Add a `case` in `getCoverageReportLoader` in `internal/plugin/runner.go` (and the import). |
| 100 | +3. Add a fixture under `internal/test/` (e.g. `example_<name>.<ext>`) and a matching diff fixture. |
| 101 | +4. Add a loader unit test `report_test.go` and a full end-to-end test in |
| 102 | + `internal/plugin/runner_test.go` (see golden-test note below). |
| 103 | +5. Update `README.md`: the supported-formats table, a usage section, and the `coverage_type` |
| 104 | + parameter values. |
| 105 | + |
| 106 | +## Testing conventions (read before editing reporter output) |
| 107 | + |
| 108 | +`internal/plugin/runner_test.go` contains **golden-string assertions** for the exact console output |
| 109 | +(`buf.String()`) and the exact PR comment body. If you change anything the reporters print, these |
| 110 | +goldens will fail and must be regenerated **exactly** — do not hand-edit them (emoji, box-drawing |
| 111 | +chars, tabs, and trailing spaces all matter). |
| 112 | + |
| 113 | +To regenerate, write a temporary `internal/plugin/dump_test.go` that runs `NewRunner().Run(...)` for |
| 114 | +the affected scenarios against an `httptest` server, and prints `strconv.Quote(output)` for both the |
| 115 | +console buffer and the captured request body. Copy those quoted strings into the goldens, then |
| 116 | +**delete the dump test**. (This pattern has been used repeatedly here; it is the reliable way to keep |
| 117 | +goldens byte-exact.) |
| 118 | + |
| 119 | +Other test notes: |
| 120 | +- Mocks live in `internal/test/mocks` (`MockPropertyGetter`, `WithMockGithubAPI`). `propGetter` |
| 121 | + uses testify mock with `AssertExpectations`, so only stub the env vars the runner actually reads |
| 122 | + for that scenario. |
| 123 | +- `_test.go` files are exempt from `funlen`, `goconst`, `gosec` (see `.golangci.yml`). |
| 124 | + |
| 125 | +## Conventions & gotchas |
| 126 | + |
| 127 | +- **Lint is strict** (`.golangci.yml`: `gocyclo`, `gosec`, `unused`, `staticcheck`, `errcheck`, |
| 128 | + `unparam`, `goconst`, etc.). Notably: prefer `fmt.Fprintf(&b, ...)` over |
| 129 | + `b.WriteString(fmt.Sprintf(...))`; remove dead code (`unused`); file opens with user-supplied |
| 130 | + paths need a `// nolint: gosec` with a reason (see existing loaders). |
| 131 | +- `make format` only *checks*; run `gofmt -w .` to actually format. |
| 132 | +- This plugin **dogfoods itself**: `.github/workflows/pr-coverage.yml` runs it on this repo's own |
| 133 | + PRs, so reporter changes will visibly change the coverage comment on your PR. That's expected. |
| 134 | +- `release.yml` builds and pushes the Docker image to GHCR on GitHub release (uses the built-in |
| 135 | + `GITHUB_TOKEN`, no extra secrets). |
| 136 | + |
| 137 | +## Commits & PRs |
| 138 | + |
| 139 | +- **Do not add AI/agent co-author trailers or "Generated with…" lines to commits.** Commits are |
| 140 | + attributed solely to the repository author. Write a normal, descriptive commit message. |
| 141 | +- Only commit/push when explicitly asked. If on the default branch, create a feature branch first. |
| 142 | +- A PR is ready only when `go test ./...`, `make format`, and `make lint` all pass. |
0 commit comments