Skip to content

Commit 1ed432f

Browse files
authored
Merge pull request #22 from target/agents.md
feat: Agents.md
2 parents 1bc382b + 2fb78d4 commit 1ed432f

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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

Comments
 (0)