From 2fb78d41d76fa993a576003ac9559e17d9ec5319 Mon Sep 17 00:00:00 2001 From: Vishal Vaibhav Date: Sun, 24 May 2026 11:48:15 +0530 Subject: [PATCH] feat: Agents.md --- AGENTS.md | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bb47610 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,142 @@ +# AGENTS.md + +Guidance for AI coding agents working in this repository. Humans may find it useful too. + +## What this project is + +`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` | + +## Tech stack + +- **Go 1.26.3**, module `github.com/target/pull-request-code-coverage`. +- Deps: `pkg/errors`, `sirupsen/logrus`, `stretchr/testify` (see `go.mod`). Keep deps minimal. +- Ships as a Docker image (`Dockerfile`) published to `ghcr.io/target/pull-request-code-coverage`. + +## Commands (run from repo root) + +```bash +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. + +## Architecture & data flow + +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 + changed `SourceLine`s. `PARAMETER_SOURCE_DIRS` controls how a path prefix is split into `SrcDir`/`Pkg`. +- `internal/plugin/coverage/` — `report.go` defines the two interfaces every format implements: + `Loader.Load(file) (Report, error)` and `Report.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`) and `coverageStatusEmoji` live in `github_pr.go` and 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). + +## Concepts you must not "simplify" away + +- **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) and `GithubPullRequest` + (Markdown) render the same data differently. Change both if you change what's reported. +- **The PR comment is posted only** when `gh_api_key` AND `BUILD_PULL_REQUEST_NUMBER` AND + `REPOSITORY_ORG` AND `REPOSITORY_NAME` are all present; otherwise console-only. `GithubPullRequest` + also returns early when there are zero changed lines with coverage data. +- **Path matching differs by loader.** `jacoco`/`cobertura` match using the report's source root; + `python`/`lcov` match on the **repo-relative path** (and `lcov` also suffix-matches absolute + `SF:` paths). When adding/altering a loader, preserve its matching contract. + +## How to add a new coverage format + +1. Create `internal/plugin/coverage//report.go` implementing `coverage.Loader` and + `coverage.Report`. Mirror an existing loader; per-loader helpers are duplicated by convention + (each loader has its own `silentlyCall`, path helpers, etc. — that's the established style here, + not shared utilities). +2. Add a `case` in `getCoverageReportLoader` in `internal/plugin/runner.go` (and the import). +3. Add a fixture under `internal/test/` (e.g. `example_.`) and a matching diff fixture. +4. Add a loader unit test `report_test.go` and a full end-to-end test in + `internal/plugin/runner_test.go` (see golden-test note below). +5. Update `README.md`: the supported-formats table, a usage section, and the `coverage_type` + parameter values. + +## Testing conventions (read before editing reporter output) + +`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`). `propGetter` + uses testify mock with `AssertExpectations`, so only stub the env vars the runner actually reads + for that scenario. +- `_test.go` files are exempt from `funlen`, `goconst`, `gosec` (see `.golangci.yml`). + +## Conventions & gotchas + +- **Lint is strict** (`.golangci.yml`: `gocyclo`, `gosec`, `unused`, `staticcheck`, `errcheck`, + `unparam`, `goconst`, etc.). Notably: prefer `fmt.Fprintf(&b, ...)` over + `b.WriteString(fmt.Sprintf(...))`; remove dead code (`unused`); file opens with user-supplied + paths need a `// nolint: gosec` with a reason (see existing loaders). +- `make format` only *checks*; run `gofmt -w .` to actually format. +- This plugin **dogfoods itself**: `.github/workflows/pr-coverage.yml` runs it on this repo's own + PRs, so reporter changes will visibly change the coverage comment on your PR. That's expected. +- `release.yml` builds and pushes the Docker image to GHCR on GitHub release (uses the built-in + `GITHUB_TOKEN`, no extra secrets). + +## Commits & PRs + +- **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`, and `make lint` all pass.