From 9e9506f385a21e21fbb46be6dd44c781382823a3 Mon Sep 17 00:00:00 2001 From: webkitez <22659755+webkitez@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:03:23 +0530 Subject: [PATCH 1/2] Revert "last pr regression (#25)" This reverts commit 75de7d1090b4cfcec791b310e0f2175517e024a1. --- internal/plugin/gitdiff/diff.go | 56 ----------------------- internal/plugin/runner.go | 32 ++----------- internal/plugin/runner_diffsource_test.go | 46 +------------------ 3 files changed, 4 insertions(+), 130 deletions(-) delete mode 100644 internal/plugin/gitdiff/diff.go diff --git a/internal/plugin/gitdiff/diff.go b/internal/plugin/gitdiff/diff.go deleted file mode 100644 index bc6277b..0000000 --- a/internal/plugin/gitdiff/diff.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package gitdiff produces a unified diff by running git locally, reproducing -// the behaviour of the old scripts/start.sh that was removed in v1.0.1. -package gitdiff - -import ( - "bytes" - "io" - "os/exec" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// Loader fetches the PR diff by running `git fetch` + `git diff` in the -// current working directory. git must be on $PATH and the remote "origin" must -// be reachable (credentials via .netrc, SSH key, or the git credential helper -// already configured in the calling environment). -type Loader struct { - baseBranch string - module string -} - -func NewLoader(baseBranch, module string) *Loader { - return &Loader{baseBranch: baseBranch, module: module} -} - -// Load fetches origin/ and returns a unified=0 diff against HEAD. -// When module is non-empty it is passed as a path filter to git diff, matching -// the behaviour of the old start.sh for Gradle multi-module projects. -func (l *Loader) Load() (io.Reader, error) { - if l.baseBranch == "" { - return nil, errors.New("base branch is required for PARAMETER_DIFF_SOURCE=git (set PARAMETER_BASE_BRANCH or VELA_PULL_REQUEST_TARGET)") - } - - fetchArgs := []string{"fetch", "--no-tags", "origin", l.baseBranch} - logrus.Infof("running git %v", fetchArgs) - fetchCmd := exec.Command("git", fetchArgs...) //nolint:gosec // args are controlled internally, not user input - if out, err := fetchCmd.CombinedOutput(); err != nil { - return nil, errors.Wrapf(err, "git fetch failed: %s", string(out)) - } - - diffArgs := []string{"--no-pager", "diff", "--unified=0", "origin/" + l.baseBranch} - if l.module != "" { - diffArgs = append(diffArgs, l.module) - } - logrus.Infof("running git %v", diffArgs) - diffCmd := exec.Command("git", diffArgs...) //nolint:gosec // args are controlled internally, not user input - var stdout, stderr bytes.Buffer - diffCmd.Stdout = &stdout - diffCmd.Stderr = &stderr - if err := diffCmd.Run(); err != nil { - return nil, errors.Wrapf(err, "git diff failed: %s", stderr.String()) - } - - return &stdout, nil -} diff --git a/internal/plugin/runner.go b/internal/plugin/runner.go index 078377b..023561b 100644 --- a/internal/plugin/runner.go +++ b/internal/plugin/runner.go @@ -15,7 +15,6 @@ import ( "github.com/target/pull-request-code-coverage/internal/plugin/coverage/jacoco" "github.com/target/pull-request-code-coverage/internal/plugin/coverage/lcov" "github.com/target/pull-request-code-coverage/internal/plugin/coverage/pythoncov" - "github.com/target/pull-request-code-coverage/internal/plugin/gitdiff" "github.com/target/pull-request-code-coverage/internal/plugin/githubdiff" "github.com/target/pull-request-code-coverage/internal/plugin/pluginhttp" "github.com/target/pull-request-code-coverage/internal/plugin/pluginjson" @@ -102,39 +101,14 @@ func (*DefaultRunner) Run(propertyGetter func(string) (string, bool), changedSou diffSource, found := propertyGetter("PARAMETER_DIFF_SOURCE") if !found || diffSource == "" { - // Auto-detect: when running in Vela CI (VELA_PULL_REQUEST_TARGET is set by - // the Vela runtime) fall back to the git diff mode so pipelines that relied - // on the old start.sh entrypoint continue to work without any config change. - if velaTarget, hasVelaTarget := propertyGetter("VELA_PULL_REQUEST_TARGET"); hasVelaTarget && velaTarget != "" { - logrus.Info("PARAMETER_DIFF_SOURCE was missing but VELA_PULL_REQUEST_TARGET is set, defaulting to git") - diffSource = "git" - } else { - logrus.Info("PARAMETER_DIFF_SOURCE was missing, defaulting to stdin") - diffSource = "stdin" - } + logrus.Info("PARAMETER_DIFF_SOURCE was missing, defaulting to stdin") + diffSource = "stdin" } switch diffSource { case "stdin": // changedSourceLinesSource already points at the piped-in diff (stdin); // nothing to do. This is the original, default behavior. - case "git": - // Runs git fetch + git diff locally, reproducing what start.sh did before - // it was removed in v1.0.1. The base branch is read from PARAMETER_BASE_BRANCH - // first, then VELA_PULL_REQUEST_TARGET for backward compatibility. - baseBranch, hasBranch := propertyGetter("PARAMETER_BASE_BRANCH") - if !hasBranch || baseBranch == "" { - baseBranch, _ = propertyGetter("VELA_PULL_REQUEST_TARGET") - } - - logrus.Infof("PARAMETER_DIFF_SOURCE is git, diffing against origin/%s", baseBranch) - - diffReader, fetchErr := gitdiff.NewLoader(baseBranch, module).Load() - if fetchErr != nil { - return errors.Wrap(fetchErr, "Failed fetching diff via git") - } - - changedSourceLinesSource = diffReader case "github": if !ghAPIKeyFound || !repoPRFound || !repoOwnerFound || !repoNameFound { return errors.New("PARAMETER_DIFF_SOURCE=github requires a GitHub API key (PARAMETER_GH_API_KEY), BUILD_PULL_REQUEST_NUMBER, REPOSITORY_ORG and REPOSITORY_NAME") @@ -149,7 +123,7 @@ func (*DefaultRunner) Run(propertyGetter func(string) (string, bool), changedSou changedSourceLinesSource = diffReader default: - return errors.Errorf("Unknown PARAMETER_DIFF_SOURCE %q (expected \"stdin\", \"git\", or \"github\")", diffSource) + return errors.Errorf("Unknown PARAMETER_DIFF_SOURCE %q (expected \"stdin\" or \"github\")", diffSource) } changedLines, changedLinesErr := unifieddiff.NewChangedSourceLinesLoader(module, sourceDirs).Load(changedSourceLinesSource) diff --git a/internal/plugin/runner_diffsource_test.go b/internal/plugin/runner_diffsource_test.go index dac1c89..aa79324 100644 --- a/internal/plugin/runner_diffsource_test.go +++ b/internal/plugin/runner_diffsource_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/target/pull-request-code-coverage/internal/test/mocks" ) @@ -57,7 +56,7 @@ func TestDefaultRunner_Run_DiffSourceUnknown(t *testing.T) { propGetter.On("GetProperty", "PARAMETER_DIFF_SOURCE").Return("banana", true) err := NewRunner().Run(propGetter.GetProperty, strings.NewReader(""), os.Stdout) - assert.EqualError(t, err, "Unknown PARAMETER_DIFF_SOURCE \"banana\" (expected \"stdin\", \"git\", or \"github\")") + assert.EqualError(t, err, "Unknown PARAMETER_DIFF_SOURCE \"banana\" (expected \"stdin\" or \"github\")") propGetter.AssertExpectations(t) } @@ -118,46 +117,3 @@ func TestDefaultRunner_Run_DiffSourceGithub_FetchesDiff(t *testing.T) { propGetter.AssertExpectations(t) } - -// When PARAMETER_DIFF_SOURCE is absent but VELA_PULL_REQUEST_TARGET is set the -// runner should auto-select the "git" path. We verify this by using a branch -// name that is guaranteed to not exist on any remote, so git fetch always fails -// and the error message proves the "git" path was taken (not the silent "stdin" -// path that would return 0 lines with no error). -func TestDefaultRunner_Run_DiffSourceAutodetect_VelaTarget(t *testing.T) { - propGetter := mocks.NewMockPropertyGetter() - - propGetter.On("GetProperty", "PARAMETER_COVERAGE_FILE").Return("../test/jacocoTestReport.xml", true) - propGetter.On("GetProperty", "PARAMETER_COVERAGE_TYPE").Return("jacoco", true) - propGetter.On("GetProperty", "PARAMETER_SOURCE_DIRS").Return("src/main/java", true) - // No PARAMETER_DIFF_SOURCE — auto-detect should kick in. - // Use a branch that cannot exist on any remote so git fetch always fails. - propGetter.On("GetProperty", "VELA_PULL_REQUEST_TARGET").Return("this-branch-does-not-exist-xyzzy-99999", true) - propGetter.On("GetProperty", "PARAMETER_BASE_BRANCH").Return("", false) - - err := NewRunner().Run(propGetter.GetProperty, strings.NewReader(""), os.Stdout) - // require (not assert) so the test stops here on nil — prevents a nil-deref - // panic on the Contains check below. - require.Error(t, err) - assert.Contains(t, err.Error(), "Failed fetching diff via git") - - propGetter.AssertExpectations(t) -} - -// PARAMETER_DIFF_SOURCE=git with an explicit PARAMETER_BASE_BRANCH should also -// take the git path and surface a meaningful error when the branch is empty. -func TestDefaultRunner_Run_DiffSourceGit_MissingBaseBranch(t *testing.T) { - propGetter := mocks.NewMockPropertyGetter() - - propGetter.On("GetProperty", "PARAMETER_COVERAGE_FILE").Return("../test/jacocoTestReport.xml", true) - propGetter.On("GetProperty", "PARAMETER_COVERAGE_TYPE").Return("jacoco", true) - propGetter.On("GetProperty", "PARAMETER_SOURCE_DIRS").Return("src/main/java", true) - propGetter.On("GetProperty", "PARAMETER_DIFF_SOURCE").Return("git", true) - propGetter.On("GetProperty", "PARAMETER_BASE_BRANCH").Return("", false) - propGetter.On("GetProperty", "VELA_PULL_REQUEST_TARGET").Return("", false) - - err := NewRunner().Run(propGetter.GetProperty, strings.NewReader(""), os.Stdout) - assert.EqualError(t, err, "Failed fetching diff via git: base branch is required for PARAMETER_DIFF_SOURCE=git (set PARAMETER_BASE_BRANCH or VELA_PULL_REQUEST_TARGET)") - - propGetter.AssertExpectations(t) -} From 318681659c95f3e206a4b8d41e08fc3f1e6d129d Mon Sep 17 00:00:00 2001 From: webkitez <22659755+webkitez@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:03:23 +0530 Subject: [PATCH 2/2] Revert "sticky comment (#24)" This reverts commit 6913878d2dc9ad2955738a23e157143b1db658c7. --- .github/workflows/pr-coverage.yml | 54 +++----- AGENTS.md | 30 ++--- Dockerfile | 6 +- README.md | 14 -- internal/plugin/githubdiff/diff.go | 74 ----------- internal/plugin/githubdiff/diff_test.go | 88 ------------- internal/plugin/pluginjson/client.go | 5 - internal/plugin/pluginjson/mocks.go | 6 - internal/plugin/reporter/github_pr.go | 121 +++--------------- internal/plugin/reporter/github_pr_test.go | 114 ++++++++--------- internal/plugin/reporter/step_summary.go | 38 ------ internal/plugin/reporter/step_summary_test.go | 43 ------- internal/plugin/runner.go | 46 ------- internal/plugin/runner_diffsource_test.go | 119 ----------------- internal/plugin/runner_test.go | 16 +-- .../unifieddiff/changed_source_loader.go | 12 -- .../unifieddiff/changed_source_loader_test.go | 83 ------------ internal/test/mocks/mock_gh_api.go | 14 +- internal/test/mocks/property_getter.go | 24 ---- scripts/start.sh | 30 +++++ 20 files changed, 143 insertions(+), 794 deletions(-) delete mode 100644 internal/plugin/githubdiff/diff.go delete mode 100644 internal/plugin/githubdiff/diff_test.go delete mode 100644 internal/plugin/reporter/step_summary.go delete mode 100644 internal/plugin/reporter/step_summary_test.go delete mode 100644 internal/plugin/runner_diffsource_test.go delete mode 100644 internal/plugin/sourcelines/unifieddiff/changed_source_loader_test.go create mode 100755 scripts/start.sh diff --git a/.github/workflows/pr-coverage.yml b/.github/workflows/pr-coverage.yml index dc2978a..b3cbf7e 100644 --- a/.github/workflows/pr-coverage.yml +++ b/.github/workflows/pr-coverage.yml @@ -14,17 +14,14 @@ permissions: jobs: coverage: runs-on: ubuntu-latest - # Runs on fork PRs too. Fork PRs only get a read-only GITHUB_TOKEN and no - # secrets, so the plugin can't post the PR comment there — it just prints - # coverage to the job log (the reporter no-ops the comment when the token / - # PR context is missing). + # Fork PRs only get a read-only GITHUB_TOKEN (can't comment) and no secrets, + # so restrict to same-repo PRs to avoid a guaranteed failure on forks. + if: github.event.pull_request.head.repo.full_name == github.repository steps: - name: Check out the repo uses: actions/checkout@v4 - # No fetch-depth needed: the plugin now fetches the PR diff from the - # GitHub API (PARAMETER_DIFF_SOURCE=github), so we don't diff against the - # base branch locally. Checkout is only here to build the image and run - # the tests that produce the coverage report. + with: + fetch-depth: 0 # need the base branch present to diff against it - name: Set up Go uses: actions/setup-go@v5 @@ -46,16 +43,7 @@ jobs: run: docker build -t pr-code-coverage:ci . - name: Report coverage on changed lines - # On fork PRs the GITHUB_TOKEN is read-only: the plugin can read the diff - # but the PR-comment POST gets a 403 and errors. Tolerate that on forks so - # the run still goes green with coverage printed to the log; same-repo PRs - # keep failing loudly on real errors. - continue-on-error: ${{ github.event.pull_request.head.repo.full_name != github.repository }} env: - # Fetch the PR diff straight from the GitHub API instead of piping in a - # local `git diff`. Note the API diff covers ALL changed files, not just - # *.go, so non-Go edits (yml/md) show up as "untracked changed lines". - PARAMETER_DIFF_SOURCE: github PARAMETER_COVERAGE_TYPE: cobertura PARAMETER_COVERAGE_FILE: coverage.xml # Must equal the cobertura path (the dir go test ran in). @@ -65,22 +53,18 @@ jobs: BUILD_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} REPOSITORY_ORG: ${{ github.repository_owner }} REPOSITORY_NAME: ${{ github.event.repository.name }} - # The container mounts the workspace so it can read coverage.xml, and the - # GITHUB_STEP_SUMMARY file so the plugin can render coverage on the run's - # summary page (visible even on fork PRs where the comment can't post). - # The diff no longer comes from stdin, so `-i` and the pipe are gone. run: | - docker run --rm \ - -e PARAMETER_DIFF_SOURCE \ - -e PARAMETER_COVERAGE_TYPE \ - -e PARAMETER_COVERAGE_FILE \ - -e PARAMETER_SOURCE_DIRS \ - -e PARAMETER_GH_API_KEY \ - -e BUILD_PULL_REQUEST_NUMBER \ - -e REPOSITORY_ORG \ - -e REPOSITORY_NAME \ - -e GITHUB_STEP_SUMMARY \ - -v "${{ github.workspace }}:${{ github.workspace }}" \ - -v "$GITHUB_STEP_SUMMARY:$GITHUB_STEP_SUMMARY" \ - -w "${{ github.workspace }}" \ - pr-code-coverage:ci + git fetch --no-tags origin "${{ github.base_ref }}" + git --no-pager diff --unified=0 "origin/${{ github.base_ref }}" -- '*.go' \ + | docker run --rm -i \ + -e PARAMETER_COVERAGE_TYPE \ + -e PARAMETER_COVERAGE_FILE \ + -e PARAMETER_SOURCE_DIRS \ + -e PARAMETER_GH_API_KEY \ + -e BUILD_PULL_REQUEST_NUMBER \ + -e REPOSITORY_ORG \ + -e REPOSITORY_NAME \ + -v "${{ github.workspace }}:${{ github.workspace }}" \ + -w "${{ github.workspace }}" \ + --entrypoint /plugin \ + pr-code-coverage:ci diff --git a/AGENTS.md b/AGENTS.md index 41e660a..bb47610 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,43 +46,32 @@ The CI (`.github/workflows/test.yml`) runs build, test, `make format`, and `make 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 by -default, or fetched from the GitHub API — see `PARAMETER_DIFF_SOURCE` below), and the +`BUILD_PULL_REQUEST_NUMBER`, `REPOSITORY_ORG`, `REPOSITORY_NAME`), the **diff from stdin**, and the **coverage report from the file** at `PARAMETER_COVERAGE_FILE`. ``` -diff (unified) ──► sourcelines/unifieddiff ──► []domain.SourceLine +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, StepSummary }.Write(...) + reporter.Forking{ Simple, GithubPullRequest }.Write(...) ├─ Simple → plain-text report to stdout (always) - ├─ GithubPullRequest → Markdown PR comment (only if creds present) - └─ StepSummary → Markdown to $GITHUB_STEP_SUMMARY (only if set) + └─ 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`. - Handles both `--unified=0` diffs (the stdin/Vela path, no context lines) and diffs that carry context - lines (e.g. from the GitHub API) — context lines advance the new-file line counter but aren't recorded. -- `internal/plugin/githubdiff/diff.go` — alternative diff source. When `PARAMETER_DIFF_SOURCE=github`, - the runner fetches the PR diff from `GET /repos/{owner}/{repo}/pulls/{n}` with the - `application/vnd.github.v3.diff` media type instead of reading stdin. Default is `stdin` - (unchanged behavior). The `github` mode requires `PARAMETER_GH_API_KEY` + the three build-context vars. - `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), - `step_summary.go` (writes the same markdown to `$GITHUB_STEP_SUMMARY`), `forking.go` (runs all - reporters), `utils.go` (`filePath`, `lineDescription`). Per-file aggregation - (`collectFileCoverage`), `coverageStatusEmoji`, and the shared markdown builder - (`buildMarkdownReport`, used by both `github_pr.go` and `step_summary.go`) live in `github_pr.go`. - The PR comment is **sticky**: `github_pr.go` first GETs the PR's comments, and if it finds the one - carrying the hidden `commentMarker` it PATCHes that comment instead of POSTing a new one. + `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). @@ -92,9 +81,8 @@ Key packages: 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) renders one way; - `GithubPullRequest` and `StepSummary` both render the shared `buildMarkdownReport` output. Change - `Simple` and `buildMarkdownReport` if you change what's reported. +- **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. diff --git a/Dockerfile b/Dockerfile index 40c8679..5214a90 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,5 @@ FROM alpine:latest COPY --from=builder /go/src/github.com/target/pull-request-code-coverage/bin/plugin / RUN apk --no-cache add ca-certificates git bash openssh-client WORKDIR /root/ -# Run the plugin directly. With PARAMETER_DIFF_SOURCE=github it fetches the PR -# diff from the GitHub API; for the stdin path, pipe a `git diff` into the -# container (docker run -i ... | git diff ...). -ENTRYPOINT ["/plugin"] \ No newline at end of file +COPY scripts/start.sh / +CMD ["/start.sh"] \ No newline at end of file diff --git a/README.md b/README.md index edb1899..8fe364b 100644 --- a/README.md +++ b/README.md @@ -312,16 +312,6 @@ git --no-pager diff --unified=0 "origin/$BASE_REF" -- '*.go' | docker run --rm - A working GitHub Actions example lives in [`.github/workflows/pr-coverage.yml`](.github/workflows/pr-coverage.yml). -**No git checkout?** Set `PARAMETER_DIFF_SOURCE=github` and the plugin fetches the PR's diff straight from the GitHub API instead of reading stdin — so you don't need the repo checked out or `git` available, just the coverage file and a token. This uses the same diff GitHub shows reviewers (computed against the merge base), so it can differ slightly from a local `git diff origin/`. It requires `PARAMETER_GH_API_KEY`, `BUILD_PULL_REQUEST_NUMBER`, `REPOSITORY_ORG`, and `REPOSITORY_NAME`. - -``` -docker run --rm \ - -e PARAMETER_DIFF_SOURCE=github \ - -e PARAMETER_COVERAGE_TYPE -e PARAMETER_COVERAGE_FILE -e PARAMETER_SOURCE_DIRS \ - -e PARAMETER_GH_API_KEY -e BUILD_PULL_REQUEST_NUMBER -e REPOSITORY_ORG -e REPOSITORY_NAME \ - ghcr.io/target/pull-request-code-coverage:latest -``` - --- ## Parameters @@ -336,7 +326,6 @@ docker run --rm \ | `module` | `PARAMETER_MODULE` | no | _(empty)_ | sub-module path prefix to strip, for multi-module projects (e.g. a Gradle multi-project build) | | `gh_api_key` | `PARAMETER_GH_API_KEY` (or `PLUGIN_GH_API_KEY`) | no | | token used to post the PR comment. If unset, no comment is posted (console only) | | `gh_api_base_url` | `PARAMETER_GH_API_BASE_URL` | no | `https://api.github.com` | GitHub API root. For GitHub Enterprise, use the full root including `/api/v3` | -| `diff_source` | `PARAMETER_DIFF_SOURCE` | no | `stdin` | where the PR diff comes from: `stdin` (pipe a `git diff` in, the default) or `github` (fetch the PR diff from the GitHub API — needs no git checkout; requires `gh_api_key` and the three build-context values) | | `debug` | `PARAMETER_DEBUG` | no | `false` | enable debug logging | **Build context** — provided automatically by Vela; set these yourself on other CIs to enable the PR comment. @@ -346,11 +335,8 @@ docker run --rm \ | `BUILD_PULL_REQUEST_NUMBER` | the PR number to comment on | | `REPOSITORY_ORG` | repository owner / org | | `REPOSITORY_NAME` | repository name | -| `GITHUB_STEP_SUMMARY` | set automatically by GitHub Actions. When present, the plugin also writes the report to the [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary), so coverage shows on the run page even on fork PRs that can't be commented on | > The PR comment is posted only when `gh_api_key` **and** all three build-context values are present. Otherwise the plugin prints to the console and exits successfully. -> -> The comment is **sticky**: on later pushes the plugin updates its existing comment in place instead of posting a new one each time. In GitHub Actions, mount the summary file (`-v "$GITHUB_STEP_SUMMARY:$GITHUB_STEP_SUMMARY" -e GITHUB_STEP_SUMMARY`) to get the job-summary output — see [`.github/workflows/pr-coverage.yml`](.github/workflows/pr-coverage.yml). --- diff --git a/internal/plugin/githubdiff/diff.go b/internal/plugin/githubdiff/diff.go deleted file mode 100644 index 43ad137..0000000 --- a/internal/plugin/githubdiff/diff.go +++ /dev/null @@ -1,74 +0,0 @@ -// Package githubdiff fetches a pull request's unified diff directly from the -// GitHub REST API, so the plugin can determine what a PR changed without a git -// checkout. It is an alternative to reading the diff piped in on stdin. -package githubdiff - -import ( - "bytes" - "fmt" - "io" - "strings" - - "github.com/pkg/errors" - "github.com/target/pull-request-code-coverage/internal/plugin/pluginhttp" -) - -const httpResponseOK = 200 - -// Loader retrieves the diff for a single pull request from the GitHub API. -type Loader struct { - apiKey string - apiBaseURL string - pr string - owner string - repo string - httpClient pluginhttp.Client -} - -func NewLoader(apiKey string, apiBaseURL string, pr string, owner string, repo string, httpClient pluginhttp.Client) *Loader { - return &Loader{ - apiKey: apiKey, - apiBaseURL: apiBaseURL, - pr: pr, - owner: owner, - repo: repo, - httpClient: httpClient, - } -} - -// Load requests the pull request diff using the `application/vnd.github.v3.diff` -// media type. GitHub returns the same unified diff it shows reviewers — computed -// against the merge base and carrying context lines — which the unified-diff -// parser handles. The whole response is read into memory and returned as a -// reader so the caller can treat it exactly like the stdin diff. -func (l *Loader) Load() (io.Reader, error) { - url := fmt.Sprintf("%v/repos/%v/%v/pulls/%v", strings.TrimRight(l.apiBaseURL, "/"), l.owner, l.repo, l.pr) - - req, newErr := l.httpClient.NewRequest("GET", url, nil) - if newErr != nil { - return nil, errors.Wrap(newErr, "Failed creating request to github") - } - - req.Header.Add("Authorization", "token "+l.apiKey) - req.Header.Add("Accept", "application/vnd.github.v3.diff") - - resp, doErr := l.httpClient.Do(req) - if doErr != nil { - return nil, errors.Wrap(doErr, "Failed calling github") - } - - defer func() { - _ = resp.Body.Close() - }() - - if resp.StatusCode != httpResponseOK { - return nil, errors.Errorf("Failed calling github: bad status code: %v", resp.StatusCode) - } - - body, readErr := io.ReadAll(resp.Body) - if readErr != nil { - return nil, errors.Wrap(readErr, "Failed reading diff response from github") - } - - return bytes.NewReader(body), nil -} diff --git a/internal/plugin/githubdiff/diff_test.go b/internal/plugin/githubdiff/diff_test.go deleted file mode 100644 index ae30bd5..0000000 --- a/internal/plugin/githubdiff/diff_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package githubdiff - -import ( - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/target/pull-request-code-coverage/internal/plugin/pluginhttp" -) - -func TestLoader_Load_BuildsRequestAndReturnsDiff(t *testing.T) { - mockClient := &pluginhttp.MockClient{} - request := httptest.NewRequest("GET", "http://anywhere", nil) - - mockClient. - On("NewRequest", "GET", "https://api.github.com/repos/some_org/some_repo/pulls/123", nil). - Return(request, nil) - mockClient. - On("Do", request). - Return(&http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("THE DIFF"))}, nil) - - reader, err := NewLoader("SOME_API_KEY", "https://api.github.com", "123", "some_org", "some_repo", mockClient).Load() - - assert.NoError(t, err) - - body, _ := io.ReadAll(reader) - assert.Equal(t, "THE DIFF", string(body)) - - assert.Equal(t, "token SOME_API_KEY", request.Header.Get("Authorization")) - assert.Equal(t, "application/vnd.github.v3.diff", request.Header.Get("Accept")) - - mockClient.AssertExpectations(t) -} - -func TestLoader_Load_TrimsTrailingSlashOnBaseURL(t *testing.T) { - mockClient := &pluginhttp.MockClient{} - request := httptest.NewRequest("GET", "http://anywhere", nil) - - mockClient. - On("NewRequest", "GET", "https://git.example.com/api/v3/repos/o/r/pulls/9", nil). - Return(request, nil) - mockClient. - On("Do", request). - Return(&http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(""))}, nil) - - _, err := NewLoader("k", "https://git.example.com/api/v3/", "9", "o", "r", mockClient).Load() - - assert.NoError(t, err) - mockClient.AssertExpectations(t) -} - -func TestLoader_Load_FailedNewRequest(t *testing.T) { - mockClient := &pluginhttp.MockClient{} - mockClient.On("NewRequest", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("boom")) - - _, err := NewLoader("k", "https://api.github.com", "1", "o", "r", mockClient).Load() - - assert.EqualError(t, err, "Failed creating request to github: boom") -} - -func TestLoader_Load_FailedDo(t *testing.T) { - mockClient := &pluginhttp.MockClient{} - request := httptest.NewRequest("GET", "http://anywhere", nil) - - mockClient.On("NewRequest", mock.Anything, mock.Anything, mock.Anything).Return(request, nil) - mockClient.On("Do", request).Return(nil, errors.New("boom")) - - _, err := NewLoader("k", "https://api.github.com", "1", "o", "r", mockClient).Load() - - assert.EqualError(t, err, "Failed calling github: boom") -} - -func TestLoader_Load_BadStatus(t *testing.T) { - mockClient := &pluginhttp.MockClient{} - request := httptest.NewRequest("GET", "http://anywhere", nil) - - mockClient.On("NewRequest", mock.Anything, mock.Anything, mock.Anything).Return(request, nil) - mockClient.On("Do", request).Return(&http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader(""))}, nil) - - _, err := NewLoader("k", "https://api.github.com", "1", "o", "r", mockClient).Load() - - assert.EqualError(t, err, "Failed calling github: bad status code: 404") -} diff --git a/internal/plugin/pluginjson/client.go b/internal/plugin/pluginjson/client.go index 1990b1b..27efa5b 100644 --- a/internal/plugin/pluginjson/client.go +++ b/internal/plugin/pluginjson/client.go @@ -4,7 +4,6 @@ import "encoding/json" type Client interface { Marshal(data interface{}) ([]byte, error) - Unmarshal(data []byte, v interface{}) error } type DefaultClient struct{} @@ -12,7 +11,3 @@ type DefaultClient struct{} func (c *DefaultClient) Marshal(data interface{}) ([]byte, error) { return json.Marshal(data) } - -func (c *DefaultClient) Unmarshal(data []byte, v interface{}) error { - return json.Unmarshal(data, v) -} diff --git a/internal/plugin/pluginjson/mocks.go b/internal/plugin/pluginjson/mocks.go index ca5132b..6ef6c1e 100644 --- a/internal/plugin/pluginjson/mocks.go +++ b/internal/plugin/pluginjson/mocks.go @@ -19,9 +19,3 @@ func (m *MockClient) Marshal(data interface{}) ([]byte, error) { } return r.([]byte), e } - -func (m *MockClient) Unmarshal(data []byte, v interface{}) error { - args := m.Called(data, v) - - return args.Error(0) -} diff --git a/internal/plugin/reporter/github_pr.go b/internal/plugin/reporter/github_pr.go index 22b82b0..6633aab 100644 --- a/internal/plugin/reporter/github_pr.go +++ b/internal/plugin/reporter/github_pr.go @@ -24,15 +24,9 @@ type GithubPullRequest struct { } const ( - HTTPResponseOK = 200 HTTPResponseCreated = 201 ) -// commentMarker is an HTML comment embedded at the top of the report. It renders -// invisibly on GitHub but lets a later run find the comment it posted earlier so -// it can update that one in place instead of posting a new comment every push. -const commentMarker = "" - func NewGithubPullRequest(apiKey string, apiBaseURL string, pr string, owner string, repo string, httpClient pluginhttp.Client, jsonClient pluginjson.Client) *GithubPullRequest { return &GithubPullRequest{ apiKey: apiKey, @@ -57,32 +51,12 @@ func (s *GithubPullRequest) Write(changedLinesWithCoverage domain.SourceLineCove return errors.Wrap(bodyErr, "Failed creating payload for github") } - existingID, findErr := s.findExistingCommentID() - if findErr != nil { - return findErr - } - - // Update the comment from a previous run when we find one; otherwise post a - // fresh comment. This keeps a single, always-current coverage comment on the - // PR instead of a new one per push. - if existingID != 0 { - url := fmt.Sprintf("%v/repos/%v/%v/issues/comments/%v", s.baseURL(), s.owner, s.repo, existingID) - return s.send("PATCH", url, body, HTTPResponseOK) - } - - url := fmt.Sprintf("%v/repos/%v/%v/issues/%v/comments", s.baseURL(), s.owner, s.repo, s.pr) - return s.send("POST", url, body, HTTPResponseCreated) -} - -// baseURL returns the configured GitHub API root without a trailing slash. -func (s *GithubPullRequest) baseURL() string { - return strings.TrimRight(s.apiBaseURL, "/") -} + url := fmt.Sprintf("%v/repos/%v/%v/issues/%v/comments", strings.TrimRight(s.apiBaseURL, "/"), s.owner, s.repo, s.pr) -// send issues a write request (POST/PATCH) carrying the comment payload and -// verifies the response status. -func (s *GithubPullRequest) send(method string, url string, body io.Reader, wantStatus int) error { - req, newErr := s.httpClient.NewRequest(method, url, body) + req, newErr := s.httpClient.NewRequest( + "POST", + url, + body) if newErr != nil { return errors.Wrap(newErr, "Failed creating request to github") @@ -101,89 +75,19 @@ func (s *GithubPullRequest) send(method string, url string, body io.Reader, want _ = resp.Body.Close() }() - if resp.StatusCode != wantStatus { + if resp.StatusCode != HTTPResponseCreated { return errors.Errorf("Failed calling github: bad status code: %v", resp.StatusCode) } return nil } -// findExistingCommentID looks for a coverage comment this plugin posted on an -// earlier run, identified by the hidden commentMarker. It returns 0 when none is -// found. Only the first page of comments is checked (per_page=100), which covers -// any realistic PR. The GET only needs read access, so it also works on fork PRs -// even though the follow-up write may not. -func (s *GithubPullRequest) findExistingCommentID() (int64, error) { - url := fmt.Sprintf("%v/repos/%v/%v/issues/%v/comments?per_page=100", s.baseURL(), s.owner, s.repo, s.pr) - - req, newErr := s.httpClient.NewRequest("GET", url, nil) - if newErr != nil { - return 0, errors.Wrap(newErr, "Failed creating request to github") - } - - req.Header.Add("Authorization", "token "+s.apiKey) - - resp, doErr := s.httpClient.Do(req) - if doErr != nil { - return 0, errors.Wrap(doErr, "Failed calling github") - } - - defer func() { - _ = resp.Body.Close() - }() - - if resp.StatusCode != HTTPResponseOK { - return 0, errors.Errorf("Failed listing github comments: bad status code: %v", resp.StatusCode) - } - - respBody, readErr := io.ReadAll(resp.Body) - if readErr != nil { - return 0, errors.Wrap(readErr, "Failed reading github comments response") - } - - var comments []struct { - ID int64 `json:"id"` - Body string `json:"body"` - } - - if unmarshalErr := s.jsonClient.Unmarshal(respBody, &comments); unmarshalErr != nil { - return 0, errors.Wrap(unmarshalErr, "Failed parsing github comments response") - } - - for _, c := range comments { - if strings.Contains(c.Body, commentMarker) { - return c.ID, nil - } - } - - return 0, nil -} - func (s *GithubPullRequest) GetName() string { return "github pull request reporter" } func (s *GithubPullRequest) createCommentBody(changedLinesWithCoverage domain.SourceLineCoverageReport) (io.Reader, error) { - data := map[string]string{ - "body": buildMarkdownReport(changedLinesWithCoverage), - } - - dataBytes, marshalErr := s.jsonClient.Marshal(data) - - if marshalErr != nil { - return nil, errors.Wrap(marshalErr, "Failed marshalling payload to json") - } - - return bytes.NewBuffer(dataBytes), nil -} - -// buildMarkdownReport renders the changed-line coverage report as GitHub-flavored -// Markdown. It is shared by the PR-comment reporter and the job-summary reporter -// so both show identical output. The leading commentMarker is invisible when -// rendered and lets the PR reporter find and update its own comment. -func buildMarkdownReport(changedLinesWithCoverage domain.SourceLineCoverageReport) string { - modules := collectModules(changedLinesWithCoverage) covered := changedLinesWithCoverage.TotalCoveredInstructions() @@ -201,7 +105,6 @@ func buildMarkdownReport(changedLinesWithCoverage domain.SourceLineCoverageRepor var b strings.Builder - b.WriteString(commentMarker + "\n") b.WriteString("## 🛡️ Patch Coverage Report\n\n") b.WriteString("> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. ") b.WriteString("It answers one thing — *did your tests run the code you just touched?*\n\n") @@ -226,7 +129,17 @@ func buildMarkdownReport(changedLinesWithCoverage domain.SourceLineCoverageRepor b.WriteString(missedInstructionsSection(changedLinesWithCoverage)) b.WriteString("\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n") - return b.String() + data := map[string]string{ + "body": b.String(), + } + + dataBytes, marshalErr := s.jsonClient.Marshal(data) + + if marshalErr != nil { + return nil, errors.Wrap(marshalErr, "Failed marshalling payload to json") + } + + return bytes.NewBuffer(dataBytes), nil } // fileCoverage holds the aggregated changed-line coverage for a single file. diff --git a/internal/plugin/reporter/github_pr_test.go b/internal/plugin/reporter/github_pr_test.go index 8d25e1a..be239c1 100644 --- a/internal/plugin/reporter/github_pr_test.go +++ b/internal/plugin/reporter/github_pr_test.go @@ -15,16 +15,6 @@ import ( "github.com/target/pull-request-code-coverage/internal/plugin/pluginjson" ) -func sampleReport() domain.SourceLineCoverageReport { - return domain.SourceLineCoverageReport{ - domain.SourceLineCoverage{ - CoverageData: domain.CoverageData{ - CoveredInstructionCount: 1, - }, - }, - } -} - func TestGithubPullRequest_Write_FailedNewRequest(t *testing.T) { mockClient := &pluginhttp.MockClient{} @@ -36,7 +26,13 @@ func TestGithubPullRequest_Write_FailedNewRequest(t *testing.T) { jsonClient: &pluginjson.DefaultClient{}, } - e := writer.Write(sampleReport()) + e := writer.Write(domain.SourceLineCoverageReport{ + domain.SourceLineCoverage{ + CoverageData: domain.CoverageData{ + CoveredInstructionCount: 1, + }, + }, + }) assert.EqualError(t, e, "Failed creating request to github: something bad happened") } @@ -55,63 +51,59 @@ func TestGithubPullRequest_Write_FailedDo(t *testing.T) { jsonClient: &pluginjson.DefaultClient{}, } - e := writer.Write(sampleReport()) + e := writer.Write(domain.SourceLineCoverageReport{ + domain.SourceLineCoverage{ + CoverageData: domain.CoverageData{ + CoveredInstructionCount: 1, + }, + }, + }) assert.EqualError(t, e, "Failed calling github: something bad happened") } -func TestGithubPullRequest_Write_FailedListingComments_BadStatus(t *testing.T) { +func TestGithubPullRequest_Write_FailedDo_BadStatus(t *testing.T) { mockClient := &pluginhttp.MockClient{} request := httptest.NewRequest("GET", "http://anywhere", nil) - mockClient.On("NewRequest", "GET", mock.Anything, mock.Anything).Return(request, nil) + mockClient.On("NewRequest", mock.Anything, mock.Anything, mock.Anything).Return(request, nil) mockClient.On("Do", request).Return(&http.Response{StatusCode: 400, Body: io.NopCloser(strings.NewReader(""))}, nil) - writer := NewGithubPullRequest("KEY", "https://api.github.com", "42", "some_owner", "some_repo", mockClient, &pluginjson.DefaultClient{}) - - e := writer.Write(sampleReport()) - - assert.EqualError(t, e, "Failed listing github comments: bad status code: 400") -} - -func TestGithubPullRequest_Write_CreatesCommentWhenNoneExists(t *testing.T) { - - mockClient := &pluginhttp.MockClient{} - listReq := httptest.NewRequest("GET", "http://list", nil) - postReq := httptest.NewRequest("POST", "http://create", nil) - - mockClient.On("NewRequest", "GET", "https://api.github.com/repos/some_owner/some_repo/issues/42/comments?per_page=100", mock.Anything).Return(listReq, nil) - mockClient.On("Do", listReq).Return(&http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("[]"))}, nil) - - mockClient.On("NewRequest", "POST", "https://api.github.com/repos/some_owner/some_repo/issues/42/comments", mock.Anything).Return(postReq, nil) - mockClient.On("Do", postReq).Return(&http.Response{StatusCode: 201, Body: io.NopCloser(strings.NewReader(""))}, nil) - - writer := NewGithubPullRequest("KEY", "https://api.github.com", "42", "some_owner", "some_repo", mockClient, &pluginjson.DefaultClient{}) + writer := &GithubPullRequest{ + apiBaseURL: "anything", + httpClient: mockClient, + jsonClient: &pluginjson.DefaultClient{}, + } - e := writer.Write(sampleReport()) + e := writer.Write(domain.SourceLineCoverageReport{ + domain.SourceLineCoverage{ + CoverageData: domain.CoverageData{ + CoveredInstructionCount: 1, + }, + }, + }) - assert.NoError(t, e) - mockClient.AssertExpectations(t) + assert.EqualError(t, e, "Failed calling github: bad status code: 400") } -func TestGithubPullRequest_Write_UpdatesExistingComment(t *testing.T) { +func TestGithubPullRequest_Write_BuildsPublicGithubURL(t *testing.T) { mockClient := &pluginhttp.MockClient{} - listReq := httptest.NewRequest("GET", "http://list", nil) - patchReq := httptest.NewRequest("PATCH", "http://update", nil) + request := httptest.NewRequest("POST", "http://anywhere", nil) - existing := `[{"id": 7, "body": "stale"}, {"id": 99, "body": "old report ` + commentMarker + ` here"}]` - - mockClient.On("NewRequest", "GET", "https://api.github.com/repos/some_owner/some_repo/issues/42/comments?per_page=100", mock.Anything).Return(listReq, nil) - mockClient.On("Do", listReq).Return(&http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(existing))}, nil) - - mockClient.On("NewRequest", "PATCH", "https://api.github.com/repos/some_owner/some_repo/issues/comments/99", mock.Anything).Return(patchReq, nil) - mockClient.On("Do", patchReq).Return(&http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(""))}, nil) + mockClient.On("NewRequest", "POST", "https://api.github.com/repos/some_owner/some_repo/issues/42/comments", mock.Anything).Return(request, nil) + mockClient.On("Do", request).Return(&http.Response{StatusCode: 201, Body: io.NopCloser(strings.NewReader(""))}, nil) writer := NewGithubPullRequest("KEY", "https://api.github.com", "42", "some_owner", "some_repo", mockClient, &pluginjson.DefaultClient{}) - e := writer.Write(sampleReport()) + e := writer.Write(domain.SourceLineCoverageReport{ + domain.SourceLineCoverage{ + CoverageData: domain.CoverageData{ + CoveredInstructionCount: 1, + }, + }, + }) assert.NoError(t, e) mockClient.AssertExpectations(t) @@ -120,18 +112,20 @@ func TestGithubPullRequest_Write_UpdatesExistingComment(t *testing.T) { func TestGithubPullRequest_Write_TrimsTrailingSlashFromEnterpriseURL(t *testing.T) { mockClient := &pluginhttp.MockClient{} - listReq := httptest.NewRequest("GET", "http://list", nil) - postReq := httptest.NewRequest("POST", "http://create", nil) + request := httptest.NewRequest("POST", "http://anywhere", nil) - mockClient.On("NewRequest", "GET", "https://git.target.com/api/v3/repos/some_owner/some_repo/issues/42/comments?per_page=100", mock.Anything).Return(listReq, nil) - mockClient.On("Do", listReq).Return(&http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("[]"))}, nil) - - mockClient.On("NewRequest", "POST", "https://git.target.com/api/v3/repos/some_owner/some_repo/issues/42/comments", mock.Anything).Return(postReq, nil) - mockClient.On("Do", postReq).Return(&http.Response{StatusCode: 201, Body: io.NopCloser(strings.NewReader(""))}, nil) + mockClient.On("NewRequest", "POST", "https://git.target.com/api/v3/repos/some_owner/some_repo/issues/42/comments", mock.Anything).Return(request, nil) + mockClient.On("Do", request).Return(&http.Response{StatusCode: 201, Body: io.NopCloser(strings.NewReader(""))}, nil) writer := NewGithubPullRequest("KEY", "https://git.target.com/api/v3/", "42", "some_owner", "some_repo", mockClient, &pluginjson.DefaultClient{}) - e := writer.Write(sampleReport()) + e := writer.Write(domain.SourceLineCoverageReport{ + domain.SourceLineCoverage{ + CoverageData: domain.CoverageData{ + CoveredInstructionCount: 1, + }, + }, + }) assert.NoError(t, e) mockClient.AssertExpectations(t) @@ -149,7 +143,13 @@ func TestGithubPullRequest_Write_FailedJsonMarshal(t *testing.T) { jsonClient: mockClient, } - e := writer.Write(sampleReport()) + e := writer.Write(domain.SourceLineCoverageReport{ + domain.SourceLineCoverage{ + CoverageData: domain.CoverageData{ + CoveredInstructionCount: 1, + }, + }, + }) assert.EqualError(t, e, "Failed creating payload for github: Failed marshalling payload to json: something bad happened") } diff --git a/internal/plugin/reporter/step_summary.go b/internal/plugin/reporter/step_summary.go deleted file mode 100644 index a138a4b..0000000 --- a/internal/plugin/reporter/step_summary.go +++ /dev/null @@ -1,38 +0,0 @@ -package reporter - -import ( - "io" - - "github.com/pkg/errors" - "github.com/target/pull-request-code-coverage/internal/plugin/domain" -) - -// StepSummary writes the Markdown coverage report to a writer backed by the -// GitHub Actions job summary file ($GITHUB_STEP_SUMMARY). The summary shows on -// the workflow run page, so coverage is visible even on fork PRs where the -// GITHUB_TOKEN is read-only and the PR comment cannot be posted. -type StepSummary struct { - out io.Writer -} - -func NewStepSummary(out io.Writer) *StepSummary { - return &StepSummary{ - out: out, - } -} - -func (s *StepSummary) Write(changedLinesWithCoverage domain.SourceLineCoverageReport) error { - if changedLinesWithCoverage.TotalLinesWithData() == 0 { - return nil - } - - if _, err := io.WriteString(s.out, buildMarkdownReport(changedLinesWithCoverage)); err != nil { - return errors.Wrap(err, "Failed writing job summary") - } - - return nil -} - -func (s *StepSummary) GetName() string { - return "github step summary reporter" -} diff --git a/internal/plugin/reporter/step_summary_test.go b/internal/plugin/reporter/step_summary_test.go deleted file mode 100644 index f0a70e2..0000000 --- a/internal/plugin/reporter/step_summary_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package reporter - -import ( - "bytes" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/target/pull-request-code-coverage/internal/plugin/domain" -) - -func TestStepSummary_Write_RendersMarkdownReport(t *testing.T) { - - var buf bytes.Buffer - - e := NewStepSummary(&buf).Write(domain.SourceLineCoverageReport{ - domain.SourceLineCoverage{ - CoverageData: domain.CoverageData{ - CoveredInstructionCount: 1, - }, - }, - }) - - assert.NoError(t, e) - - out := buf.String() - assert.Contains(t, out, commentMarker) - assert.Contains(t, out, "Patch Coverage Report") -} - -func TestStepSummary_Write_NoDataWritesNothing(t *testing.T) { - - var buf bytes.Buffer - - e := NewStepSummary(&buf).Write(domain.SourceLineCoverageReport{}) - - assert.NoError(t, e) - assert.Empty(t, buf.String()) -} - -func TestStepSummary_GetName(t *testing.T) { - assert.Equal(t, "github step summary reporter", NewStepSummary(&strings.Builder{}).GetName()) -} diff --git a/internal/plugin/runner.go b/internal/plugin/runner.go index 023561b..6f7fd53 100644 --- a/internal/plugin/runner.go +++ b/internal/plugin/runner.go @@ -3,7 +3,6 @@ package plugin import ( "fmt" "io" - "os" "strconv" "strings" @@ -15,7 +14,6 @@ import ( "github.com/target/pull-request-code-coverage/internal/plugin/coverage/jacoco" "github.com/target/pull-request-code-coverage/internal/plugin/coverage/lcov" "github.com/target/pull-request-code-coverage/internal/plugin/coverage/pythoncov" - "github.com/target/pull-request-code-coverage/internal/plugin/githubdiff" "github.com/target/pull-request-code-coverage/internal/plugin/pluginhttp" "github.com/target/pull-request-code-coverage/internal/plugin/pluginjson" "github.com/target/pull-request-code-coverage/internal/plugin/reporter" @@ -99,33 +97,6 @@ func (*DefaultRunner) Run(propertyGetter func(string) (string, bool), changedSou return errors.Wrap(loadCoverageErr, "Failed loading coverage report") } - diffSource, found := propertyGetter("PARAMETER_DIFF_SOURCE") - if !found || diffSource == "" { - logrus.Info("PARAMETER_DIFF_SOURCE was missing, defaulting to stdin") - diffSource = "stdin" - } - - switch diffSource { - case "stdin": - // changedSourceLinesSource already points at the piped-in diff (stdin); - // nothing to do. This is the original, default behavior. - case "github": - if !ghAPIKeyFound || !repoPRFound || !repoOwnerFound || !repoNameFound { - return errors.New("PARAMETER_DIFF_SOURCE=github requires a GitHub API key (PARAMETER_GH_API_KEY), BUILD_PULL_REQUEST_NUMBER, REPOSITORY_ORG and REPOSITORY_NAME") - } - - logrus.Info("PARAMETER_DIFF_SOURCE is github, fetching diff from the GitHub API") - - diffReader, fetchErr := githubdiff.NewLoader(ghAPIKey, ghAPIBaseURL, repoPR, repoOwner, repoName, &pluginhttp.DefaultClient{}).Load() - if fetchErr != nil { - return errors.Wrap(fetchErr, "Failed fetching diff from github") - } - - changedSourceLinesSource = diffReader - default: - return errors.Errorf("Unknown PARAMETER_DIFF_SOURCE %q (expected \"stdin\" or \"github\")", diffSource) - } - changedLines, changedLinesErr := unifieddiff.NewChangedSourceLinesLoader(module, sourceDirs).Load(changedSourceLinesSource) if changedLinesErr != nil { return errors.Wrap(changedLinesErr, "Failed loading changed lines") @@ -154,23 +125,6 @@ func (*DefaultRunner) Run(propertyGetter func(string) (string, bool), changedSou if ghAPIKeyFound && repoPRFound && repoOwnerFound && repoNameFound { reporters = append(reporters, reporter.NewGithubPullRequest(ghAPIKey, ghAPIBaseURL, repoPR, repoOwner, repoName, &pluginhttp.DefaultClient{}, &pluginjson.DefaultClient{})) } - - // GitHub Actions sets GITHUB_STEP_SUMMARY to a file whose Markdown is rendered - // on the run's summary page. Writing there surfaces coverage even when no PR - // comment can be posted (e.g. fork PRs with a read-only token). - if summaryPath, found := propertyGetter("GITHUB_STEP_SUMMARY"); found && summaryPath != "" { - // nolint: gosec // path comes from the trusted GitHub Actions runner env - summaryFile, openErr := os.OpenFile(summaryPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if openErr != nil { - return errors.Wrap(openErr, "Failed opening GITHUB_STEP_SUMMARY file") - } - defer func() { - _ = summaryFile.Close() - }() - - reporters = append(reporters, reporter.NewStepSummary(summaryFile)) - } - logrus.Info("enabled reporters are ") for _, eachOne := range reporters { logrus.Info(eachOne.GetName()) diff --git a/internal/plugin/runner_diffsource_test.go b/internal/plugin/runner_diffsource_test.go deleted file mode 100644 index aa79324..0000000 --- a/internal/plugin/runner_diffsource_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package plugin - -import ( - "bytes" - "net/http" - "net/http/httptest" - "os" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/target/pull-request-code-coverage/internal/test/mocks" -) - -// When PARAMETER_DIFF_SOURCE is unset the runner must behave exactly as before: -// read the diff from the reader it was given (stdin). Covered implicitly by the -// existing golden tests; this asserts the explicit "stdin" value is equivalent. -func TestDefaultRunner_Run_DiffSourceStdin_Explicit(t *testing.T) { - propGetter := mocks.NewMockPropertyGetter() - - propGetter.On("GetProperty", "PARAMETER_COVERAGE_FILE").Return("../test/jacocoTestReport.xml", true) - propGetter.On("GetProperty", "PARAMETER_COVERAGE_TYPE").Return("jacoco", true) - propGetter.On("GetProperty", "PARAMETER_SOURCE_DIRS").Return("src/main/java", true) - propGetter.On("GetProperty", "PARAMETER_MODULE").Return("category-search", true) - propGetter.On("GetProperty", "PARAMETER_DIFF_SOURCE").Return("stdin", true) - - var buf bytes.Buffer - - err := NewRunner().Run(propGetter.GetProperty, MustOpen(t, "../test/sample_unified.diff"), &buf) - assert.NoError(t, err) - assert.Contains(t, buf.String(), "Patch Coverage Report") - - propGetter.AssertExpectations(t) -} - -func TestDefaultRunner_Run_DiffSourceGithub_MissingCreds(t *testing.T) { - propGetter := mocks.NewMockPropertyGetter() - - propGetter.On("GetProperty", "PARAMETER_COVERAGE_FILE").Return("../test/jacocoTestReport.xml", true) - propGetter.On("GetProperty", "PARAMETER_COVERAGE_TYPE").Return("jacoco", true) - propGetter.On("GetProperty", "PARAMETER_SOURCE_DIRS").Return("src/main/java", true) - propGetter.On("GetProperty", "PARAMETER_DIFF_SOURCE").Return("github", true) - - err := NewRunner().Run(propGetter.GetProperty, strings.NewReader(""), os.Stdout) - assert.EqualError(t, err, "PARAMETER_DIFF_SOURCE=github requires a GitHub API key (PARAMETER_GH_API_KEY), BUILD_PULL_REQUEST_NUMBER, REPOSITORY_ORG and REPOSITORY_NAME") - - propGetter.AssertExpectations(t) -} - -func TestDefaultRunner_Run_DiffSourceUnknown(t *testing.T) { - propGetter := mocks.NewMockPropertyGetter() - - propGetter.On("GetProperty", "PARAMETER_COVERAGE_FILE").Return("../test/jacocoTestReport.xml", true) - propGetter.On("GetProperty", "PARAMETER_COVERAGE_TYPE").Return("jacoco", true) - propGetter.On("GetProperty", "PARAMETER_SOURCE_DIRS").Return("src/main/java", true) - propGetter.On("GetProperty", "PARAMETER_DIFF_SOURCE").Return("banana", true) - - err := NewRunner().Run(propGetter.GetProperty, strings.NewReader(""), os.Stdout) - assert.EqualError(t, err, "Unknown PARAMETER_DIFF_SOURCE \"banana\" (expected \"stdin\" or \"github\")") - - propGetter.AssertExpectations(t) -} - -// End-to-end: with PARAMETER_DIFF_SOURCE=github the runner fetches the diff from -// the GitHub API instead of stdin (an empty reader here) and produces the same -// report. The mock server serves the same diff fixture for the PR-diff GET and -// accepts the PR-comment POST, so the output matches the stdin golden exactly. -func TestDefaultRunner_Run_DiffSourceGithub_FetchesDiff(t *testing.T) { - diff, readErr := os.ReadFile("../test/example_go_unified.diff") - assert.NoError(t, readErr) - - var diffRequests int - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet && r.URL.Path == "/repos/some_org/some_repo/pulls/123" { - diffRequests++ - assert.Equal(t, "application/vnd.github.v3.diff", r.Header.Get("Accept")) - assert.Equal(t, "token SOME_API_KEY", r.Header.Get("Authorization")) - w.WriteHeader(200) - _, _ = w.Write(diff) - return - } - - // The sticky-comment reporter lists existing comments first; no prior - // comment exists, so it then POSTs a new one. - if r.Method == http.MethodGet && r.URL.Path == "/repos/some_org/some_repo/issues/123/comments" { - w.WriteHeader(200) - _, _ = w.Write([]byte("[]")) - return - } - - // The PR-comment POST from the github reporter. - w.WriteHeader(201) - })) - defer ts.Close() - - propGetter := mocks.NewMockPropertyGetter() - - propGetter.On("GetProperty", "PARAMETER_COVERAGE_FILE").Return("../test/example_go_coverage_with_source_dir.xml", true) - propGetter.On("GetProperty", "PARAMETER_COVERAGE_TYPE").Return("cobertura", true) - propGetter.On("GetProperty", "PARAMETER_SOURCE_DIRS").Return("/go/github.com/target/pull-request-code-coverage", true) - propGetter.On("GetProperty", "PARAMETER_GH_API_KEY").Return("SOME_API_KEY", true) - propGetter.On("GetProperty", "PARAMETER_GH_API_BASE_URL").Return(ts.URL, true) - propGetter.On("GetProperty", "BUILD_PULL_REQUEST_NUMBER").Return("123", true) - propGetter.On("GetProperty", "REPOSITORY_ORG").Return("some_org", true) - propGetter.On("GetProperty", "REPOSITORY_NAME").Return("some_repo", true) - propGetter.On("GetProperty", "PARAMETER_DIFF_SOURCE").Return("github", true) - - var buf bytes.Buffer - - // stdin is intentionally empty — the diff must come from the GitHub API. - err := NewRunner().Run(propGetter.GetProperty, strings.NewReader(""), &buf) - assert.NoError(t, err) - - assert.Equal(t, 1, diffRequests, "expected exactly one PR-diff fetch") - assert.Equal(t, "──────────────────────────────────────────────────────────────\n 📊 Patch Coverage Report — changed lines only\n──────────────────────────────────────────────────────────────\n\n Diff coverage: 97% 🟢 — 177 of 182 changed instructions covered\n\n Summary\n Covered instructions 97% (177)\n Missed instructions 3% (5)\n Tracked changed lines 8% (182)\n Untracked changed lines 92% (2216)\n\n Note: \"lines\" are the source lines you changed; \"instructions\" are the\n executable units the coverage tool counts inside them (one line can hold\n several, e.g. JaCoCo bytecode), so the two counts differ.\n\n Coverage by file (lowest coverage first)\n 0% 0 cov / 4 miss main.go\n 96% 27 cov / 1 miss internal/plugin/runner.go\n 100% 10 cov / 0 miss internal/plugin/calculator/calculator.go\n 100% 29 cov / 0 miss internal/plugin/coverage/jacoco/report.go\n 100% 19 cov / 0 miss internal/plugin/domain/domain.go\n 100% 25 cov / 0 miss internal/plugin/reporter/reporter.go\n 100% 64 cov / 0 miss internal/plugin/sourcelines/unifieddiff/changed_source_loader.go\n 100% 3 cov / 0 miss internal/test/mocks/property_getter.go\n (25 file(s) with no measurable lines omitted)\n\n Uncovered lines (5)\n - internal/plugin/runner.go:72\n func GetCoverageReportLoader(coverageType string, sourceDir string) coverage.Loader {\n - main.go:10\n \terr := plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout)\n - main.go:12\n \tif err != nil {\n - main.go:13\n \t\tlog.WithFields(log.Fields{\n - main.go:17\n \t\tos.Exit(1)\n\n──────────────────────────────────────────────────────────────\n", buf.String()) - - propGetter.AssertExpectations(t) -} diff --git a/internal/plugin/runner_test.go b/internal/plugin/runner_test.go index 98e15dd..917934c 100644 --- a/internal/plugin/runner_test.go +++ b/internal/plugin/runner_test.go @@ -91,7 +91,7 @@ func TestDefaultRunner_Run_GoExample_WithSourceDir(t *testing.T) { assert.Equal(t, "──────────────────────────────────────────────────────────────\n 📊 Patch Coverage Report — changed lines only\n──────────────────────────────────────────────────────────────\n\n Diff coverage: 97% 🟢 — 177 of 182 changed instructions covered\n\n Summary\n Covered instructions 97% (177)\n Missed instructions 3% (5)\n Tracked changed lines 8% (182)\n Untracked changed lines 92% (2216)\n\n Note: \"lines\" are the source lines you changed; \"instructions\" are the\n executable units the coverage tool counts inside them (one line can hold\n several, e.g. JaCoCo bytecode), so the two counts differ.\n\n Coverage by file (lowest coverage first)\n 0% 0 cov / 4 miss main.go\n 96% 27 cov / 1 miss internal/plugin/runner.go\n 100% 10 cov / 0 miss internal/plugin/calculator/calculator.go\n 100% 29 cov / 0 miss internal/plugin/coverage/jacoco/report.go\n 100% 19 cov / 0 miss internal/plugin/domain/domain.go\n 100% 25 cov / 0 miss internal/plugin/reporter/reporter.go\n 100% 64 cov / 0 miss internal/plugin/sourcelines/unifieddiff/changed_source_loader.go\n 100% 3 cov / 0 miss internal/test/mocks/property_getter.go\n (25 file(s) with no measurable lines omitted)\n\n Uncovered lines (5)\n - internal/plugin/runner.go:72\n func GetCoverageReportLoader(coverageType string, sourceDir string) coverage.Loader {\n - main.go:10\n \terr := plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout)\n - main.go:12\n \tif err != nil {\n - main.go:13\n \t\tlog.WithFields(log.Fields{\n - main.go:17\n \t\tos.Exit(1)\n\n──────────────────────────────────────────────────────────────\n", buf.String()) requestAsserter.AssertRequestWasMade(t, "/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{ - "body": "\n## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n**Diff coverage:** `97%` 🟢 — `177` of `182` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `177` (97%) | changed code your tests executed |\n| 🔴 Missed instructions | `5` (3%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `182` (8%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `2216` (92%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `main.go` | 🔴 0% | 0 / 4 |\n| `internal/plugin/runner.go` | 🟢 96% | 27 / 1 |\n| `internal/plugin/calculator/calculator.go` | 🟢 100% | 10 / 0 |\n| `internal/plugin/coverage/jacoco/report.go` | 🟢 100% | 29 / 0 |\n| `internal/plugin/domain/domain.go` | 🟢 100% | 19 / 0 |\n| `internal/plugin/reporter/reporter.go` | 🟢 100% | 25 / 0 |\n| `internal/plugin/sourcelines/unifieddiff/changed_source_loader.go` | 🟢 100% | 64 / 0 |\n| `internal/test/mocks/property_getter.go` | 🟢 100% | 3 / 0 |\n\n25 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (5)\n\n```\n--- internal/plugin/runner.go:72\nfunc GetCoverageReportLoader(coverageType string, sourceDir string) coverage.Loader {\n--- main.go:10\n\terr := plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout)\n--- main.go:12\n\tif err != nil {\n--- main.go:13\n\t\tlog.WithFields(log.Fields{\n--- main.go:17\n\t\tos.Exit(1)\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", + "body": "## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n**Diff coverage:** `97%` 🟢 — `177` of `182` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `177` (97%) | changed code your tests executed |\n| 🔴 Missed instructions | `5` (3%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `182` (8%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `2216` (92%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `main.go` | 🔴 0% | 0 / 4 |\n| `internal/plugin/runner.go` | 🟢 96% | 27 / 1 |\n| `internal/plugin/calculator/calculator.go` | 🟢 100% | 10 / 0 |\n| `internal/plugin/coverage/jacoco/report.go` | 🟢 100% | 29 / 0 |\n| `internal/plugin/domain/domain.go` | 🟢 100% | 19 / 0 |\n| `internal/plugin/reporter/reporter.go` | 🟢 100% | 25 / 0 |\n| `internal/plugin/sourcelines/unifieddiff/changed_source_loader.go` | 🟢 100% | 64 / 0 |\n| `internal/test/mocks/property_getter.go` | 🟢 100% | 3 / 0 |\n\n25 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (5)\n\n```\n--- internal/plugin/runner.go:72\nfunc GetCoverageReportLoader(coverageType string, sourceDir string) coverage.Loader {\n--- main.go:10\n\terr := plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout)\n--- main.go:12\n\tif err != nil {\n--- main.go:13\n\t\tlog.WithFields(log.Fields{\n--- main.go:17\n\t\tos.Exit(1)\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", }) propGetter.AssertExpectations(t) @@ -124,7 +124,7 @@ func TestDefaultRunner_Run_GoExample(t *testing.T) { assert.Equal(t, "──────────────────────────────────────────────────────────────\n 📊 Patch Coverage Report — changed lines only\n──────────────────────────────────────────────────────────────\n\n Diff coverage: 97% 🟢 — 177 of 182 changed instructions covered\n\n Summary\n Covered instructions 97% (177)\n Missed instructions 3% (5)\n Tracked changed lines 8% (182)\n Untracked changed lines 92% (2216)\n\n Note: \"lines\" are the source lines you changed; \"instructions\" are the\n executable units the coverage tool counts inside them (one line can hold\n several, e.g. JaCoCo bytecode), so the two counts differ.\n\n Coverage by file (lowest coverage first)\n 0% 0 cov / 4 miss main.go\n 96% 27 cov / 1 miss internal/plugin/runner.go\n 100% 10 cov / 0 miss internal/plugin/calculator/calculator.go\n 100% 29 cov / 0 miss internal/plugin/coverage/jacoco/report.go\n 100% 19 cov / 0 miss internal/plugin/domain/domain.go\n 100% 25 cov / 0 miss internal/plugin/reporter/reporter.go\n 100% 64 cov / 0 miss internal/plugin/sourcelines/unifieddiff/changed_source_loader.go\n 100% 3 cov / 0 miss internal/test/mocks/property_getter.go\n (25 file(s) with no measurable lines omitted)\n\n Uncovered lines (5)\n - internal/plugin/runner.go:72\n func GetCoverageReportLoader(coverageType string, sourceDir string) coverage.Loader {\n - main.go:10\n \terr := plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout)\n - main.go:12\n \tif err != nil {\n - main.go:13\n \t\tlog.WithFields(log.Fields{\n - main.go:17\n \t\tos.Exit(1)\n\n──────────────────────────────────────────────────────────────\n", buf.String()) requestAsserter.AssertRequestWasMade(t, "/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{ - "body": "\n## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n**Diff coverage:** `97%` 🟢 — `177` of `182` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `177` (97%) | changed code your tests executed |\n| 🔴 Missed instructions | `5` (3%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `182` (8%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `2216` (92%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `main.go` | 🔴 0% | 0 / 4 |\n| `internal/plugin/runner.go` | 🟢 96% | 27 / 1 |\n| `internal/plugin/calculator/calculator.go` | 🟢 100% | 10 / 0 |\n| `internal/plugin/coverage/jacoco/report.go` | 🟢 100% | 29 / 0 |\n| `internal/plugin/domain/domain.go` | 🟢 100% | 19 / 0 |\n| `internal/plugin/reporter/reporter.go` | 🟢 100% | 25 / 0 |\n| `internal/plugin/sourcelines/unifieddiff/changed_source_loader.go` | 🟢 100% | 64 / 0 |\n| `internal/test/mocks/property_getter.go` | 🟢 100% | 3 / 0 |\n\n25 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (5)\n\n```\n--- internal/plugin/runner.go:72\nfunc GetCoverageReportLoader(coverageType string, sourceDir string) coverage.Loader {\n--- main.go:10\n\terr := plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout)\n--- main.go:12\n\tif err != nil {\n--- main.go:13\n\t\tlog.WithFields(log.Fields{\n--- main.go:17\n\t\tos.Exit(1)\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", + "body": "## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n**Diff coverage:** `97%` 🟢 — `177` of `182` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `177` (97%) | changed code your tests executed |\n| 🔴 Missed instructions | `5` (3%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `182` (8%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `2216` (92%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `main.go` | 🔴 0% | 0 / 4 |\n| `internal/plugin/runner.go` | 🟢 96% | 27 / 1 |\n| `internal/plugin/calculator/calculator.go` | 🟢 100% | 10 / 0 |\n| `internal/plugin/coverage/jacoco/report.go` | 🟢 100% | 29 / 0 |\n| `internal/plugin/domain/domain.go` | 🟢 100% | 19 / 0 |\n| `internal/plugin/reporter/reporter.go` | 🟢 100% | 25 / 0 |\n| `internal/plugin/sourcelines/unifieddiff/changed_source_loader.go` | 🟢 100% | 64 / 0 |\n| `internal/test/mocks/property_getter.go` | 🟢 100% | 3 / 0 |\n\n25 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (5)\n\n```\n--- internal/plugin/runner.go:72\nfunc GetCoverageReportLoader(coverageType string, sourceDir string) coverage.Loader {\n--- main.go:10\n\terr := plugin.NewRunner().Run(os.LookupEnv, os.Stdin, os.Stdout)\n--- main.go:12\n\tif err != nil {\n--- main.go:13\n\t\tlog.WithFields(log.Fields{\n--- main.go:17\n\t\tos.Exit(1)\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", }) propGetter.AssertExpectations(t) @@ -155,7 +155,7 @@ func TestDefaultRunner_Run_PythonExample(t *testing.T) { assert.Equal(t, "──────────────────────────────────────────────────────────────\n 📊 Patch Coverage Report — changed lines only\n──────────────────────────────────────────────────────────────\n\n Diff coverage: 71% 🟡 — 5 of 7 changed instructions covered\n\n Summary\n Covered instructions 71% (5)\n Missed instructions 29% (2)\n Tracked changed lines 78% (7)\n Untracked changed lines 22% (2)\n\n Note: \"lines\" are the source lines you changed; \"instructions\" are the\n executable units the coverage tool counts inside them (one line can hold\n several, e.g. JaCoCo bytecode), so the two counts differ.\n\n Coverage by file (lowest coverage first)\n 71% 5 cov / 2 miss myapp/calculator.py\n\n Uncovered lines (2)\n - myapp/calculator.py:6\n return wrong_name\n - myapp/calculator.py:9\n return a / b\n\n──────────────────────────────────────────────────────────────\n", buf.String()) requestAsserter.AssertRequestWasMade(t, "/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{ - "body": "\n## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n**Diff coverage:** `71%` 🟡 — `5` of `7` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `5` (71%) | changed code your tests executed |\n| 🔴 Missed instructions | `2` (29%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `7` (78%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `2` (22%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `myapp/calculator.py` | 🟡 71% | 5 / 2 |\n\n\n
🔍 Uncovered lines (2)\n\n```\n--- myapp/calculator.py:6\n return wrong_name\n--- myapp/calculator.py:9\n return a / b\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", + "body": "## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n**Diff coverage:** `71%` 🟡 — `5` of `7` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `5` (71%) | changed code your tests executed |\n| 🔴 Missed instructions | `2` (29%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `7` (78%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `2` (22%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `myapp/calculator.py` | 🟡 71% | 5 / 2 |\n\n\n
🔍 Uncovered lines (2)\n\n```\n--- myapp/calculator.py:6\n return wrong_name\n--- myapp/calculator.py:9\n return a / b\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", }) propGetter.AssertExpectations(t) @@ -186,7 +186,7 @@ func TestDefaultRunner_Run_LcovExample(t *testing.T) { assert.Equal(t, "──────────────────────────────────────────────────────────────\n 📊 Patch Coverage Report — changed lines only\n──────────────────────────────────────────────────────────────\n\n Diff coverage: 71% 🟡 — 5 of 7 changed instructions covered\n\n Summary\n Covered instructions 71% (5)\n Missed instructions 29% (2)\n Tracked changed lines 78% (7)\n Untracked changed lines 22% (2)\n\n Note: \"lines\" are the source lines you changed; \"instructions\" are the\n executable units the coverage tool counts inside them (one line can hold\n several, e.g. JaCoCo bytecode), so the two counts differ.\n\n Coverage by file (lowest coverage first)\n 71% 5 cov / 2 miss src/calculator.ts\n\n Uncovered lines (2)\n - src/calculator.ts:6\n return wrongName;\n - src/calculator.ts:9\n return a / b;\n\n──────────────────────────────────────────────────────────────\n", buf.String()) requestAsserter.AssertRequestWasMade(t, "/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{ - "body": "\n## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n**Diff coverage:** `71%` 🟡 — `5` of `7` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `5` (71%) | changed code your tests executed |\n| 🔴 Missed instructions | `2` (29%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `7` (78%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `2` (22%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `src/calculator.ts` | 🟡 71% | 5 / 2 |\n\n\n
🔍 Uncovered lines (2)\n\n```\n--- src/calculator.ts:6\n return wrongName;\n--- src/calculator.ts:9\n return a / b;\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", + "body": "## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n**Diff coverage:** `71%` 🟡 — `5` of `7` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `5` (71%) | changed code your tests executed |\n| 🔴 Missed instructions | `2` (29%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `7` (78%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `2` (22%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `src/calculator.ts` | 🟡 71% | 5 / 2 |\n\n\n
🔍 Uncovered lines (2)\n\n```\n--- src/calculator.ts:6\n return wrongName;\n--- src/calculator.ts:9\n return a / b;\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", }) propGetter.AssertExpectations(t) @@ -217,7 +217,7 @@ func TestDefaultRunner_Run(t *testing.T) { assert.Equal(t, "──────────────────────────────────────────────────────────────\n 📊 Patch Coverage Report — changed lines only\n──────────────────────────────────────────────────────────────\n Modules: category-search\n\n Diff coverage: 73% 🟡 — 8 of 11 changed instructions covered\n\n Summary\n Covered instructions 73% (8)\n Missed instructions 27% (3)\n Tracked changed lines 22% (2)\n Untracked changed lines 78% (7)\n\n Note: \"lines\" are the source lines you changed; \"instructions\" are the\n executable units the coverage tool counts inside them (one line can hold\n several, e.g. JaCoCo bytecode), so the two counts differ.\n\n Coverage by file (lowest coverage first)\n 73% 8 cov / 3 miss category-search/src/main/java/com/tgt/CategorySearchApplication.java\n (3 file(s) with no measurable lines omitted)\n\n Uncovered lines (1)\n - category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n\n──────────────────────────────────────────────────────────────\n", buf.String()) requestAsserter.AssertRequestWasMade(t, "/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{ - "body": "\n## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n*Modules:* category-search\n\n**Diff coverage:** `73%` 🟡 — `8` of `11` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `8` (73%) | changed code your tests executed |\n| 🔴 Missed instructions | `3` (27%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `2` (22%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `7` (78%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `category-search/src/main/java/com/tgt/CategorySearchApplication.java` | 🟡 73% | 8 / 3 |\n\n3 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (1)\n\n```\n--- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", + "body": "## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n*Modules:* category-search\n\n**Diff coverage:** `73%` 🟡 — `8` of `11` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `8` (73%) | changed code your tests executed |\n| 🔴 Missed instructions | `3` (27%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `2` (22%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `7` (78%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `category-search/src/main/java/com/tgt/CategorySearchApplication.java` | 🟡 73% | 8 / 3 |\n\n3 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (1)\n\n```\n--- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", }) propGetter.AssertExpectations(t) @@ -249,7 +249,7 @@ func TestDefaultRunner_Run_Vela(t *testing.T) { assert.Equal(t, "──────────────────────────────────────────────────────────────\n 📊 Patch Coverage Report — changed lines only\n──────────────────────────────────────────────────────────────\n Modules: category-search\n\n Diff coverage: 73% 🟡 — 8 of 11 changed instructions covered\n\n Summary\n Covered instructions 73% (8)\n Missed instructions 27% (3)\n Tracked changed lines 22% (2)\n Untracked changed lines 78% (7)\n\n Note: \"lines\" are the source lines you changed; \"instructions\" are the\n executable units the coverage tool counts inside them (one line can hold\n several, e.g. JaCoCo bytecode), so the two counts differ.\n\n Coverage by file (lowest coverage first)\n 73% 8 cov / 3 miss category-search/src/main/java/com/tgt/CategorySearchApplication.java\n (3 file(s) with no measurable lines omitted)\n\n Uncovered lines (1)\n - category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n\n──────────────────────────────────────────────────────────────\n", buf.String()) requestAsserter.AssertRequestWasMade(t, "/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{ - "body": "\n## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n*Modules:* category-search\n\n**Diff coverage:** `73%` 🟡 — `8` of `11` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `8` (73%) | changed code your tests executed |\n| 🔴 Missed instructions | `3` (27%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `2` (22%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `7` (78%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `category-search/src/main/java/com/tgt/CategorySearchApplication.java` | 🟡 73% | 8 / 3 |\n\n3 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (1)\n\n```\n--- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", + "body": "## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n*Modules:* category-search\n\n**Diff coverage:** `73%` 🟡 — `8` of `11` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `8` (73%) | changed code your tests executed |\n| 🔴 Missed instructions | `3` (27%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `2` (22%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `7` (78%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `category-search/src/main/java/com/tgt/CategorySearchApplication.java` | 🟡 73% | 8 / 3 |\n\n3 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (1)\n\n```\n--- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", }) propGetter.AssertExpectations(t) @@ -281,7 +281,7 @@ func TestDefaultRunner_Run_2_Source_Dirs(t *testing.T) { assert.Equal(t, "──────────────────────────────────────────────────────────────\n 📊 Patch Coverage Report — changed lines only\n──────────────────────────────────────────────────────────────\n Modules: category-search\n\n Diff coverage: 88% 🟢 — 42 of 48 changed instructions covered\n\n Summary\n Covered instructions 88% (42)\n Missed instructions 12% (6)\n Tracked changed lines 53% (8)\n Untracked changed lines 47% (7)\n\n Note: \"lines\" are the source lines you changed; \"instructions\" are the\n executable units the coverage tool counts inside them (one line can hold\n several, e.g. JaCoCo bytecode), so the two counts differ.\n\n Coverage by file (lowest coverage first)\n 73% 8 cov / 3 miss category-search/src/main/java/com/tgt/CategorySearchApplication.java\n 92% 34 cov / 3 miss category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt\n (3 file(s) with no measurable lines omitted)\n\n Uncovered lines (2)\n - category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n - category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt:12\n System.out.print(\"Something2\");\n\n──────────────────────────────────────────────────────────────\n", buf.String()) requestAsserter.AssertRequestWasMade(t, "/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{ - "body": "\n## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n*Modules:* category-search\n\n**Diff coverage:** `88%` 🟢 — `42` of `48` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `42` (88%) | changed code your tests executed |\n| 🔴 Missed instructions | `6` (12%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `8` (53%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `7` (47%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `category-search/src/main/java/com/tgt/CategorySearchApplication.java` | 🟡 73% | 8 / 3 |\n| `category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt` | 🟢 92% | 34 / 3 |\n\n3 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (2)\n\n```\n--- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n--- category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt:12\n System.out.print(\"Something2\");\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", + "body": "## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n*Modules:* category-search\n\n**Diff coverage:** `88%` 🟢 — `42` of `48` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `42` (88%) | changed code your tests executed |\n| 🔴 Missed instructions | `6` (12%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `8` (53%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `7` (47%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `category-search/src/main/java/com/tgt/CategorySearchApplication.java` | 🟡 73% | 8 / 3 |\n| `category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt` | 🟢 92% | 34 / 3 |\n\n3 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (2)\n\n```\n--- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n--- category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt:12\n System.out.print(\"Something2\");\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", }) propGetter.AssertExpectations(t) @@ -314,7 +314,7 @@ func TestDefaultRunner_Run_2_Source_Dirs_Vela(t *testing.T) { assert.Equal(t, "──────────────────────────────────────────────────────────────\n 📊 Patch Coverage Report — changed lines only\n──────────────────────────────────────────────────────────────\n Modules: category-search\n\n Diff coverage: 88% 🟢 — 42 of 48 changed instructions covered\n\n Summary\n Covered instructions 88% (42)\n Missed instructions 12% (6)\n Tracked changed lines 53% (8)\n Untracked changed lines 47% (7)\n\n Note: \"lines\" are the source lines you changed; \"instructions\" are the\n executable units the coverage tool counts inside them (one line can hold\n several, e.g. JaCoCo bytecode), so the two counts differ.\n\n Coverage by file (lowest coverage first)\n 73% 8 cov / 3 miss category-search/src/main/java/com/tgt/CategorySearchApplication.java\n 92% 34 cov / 3 miss category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt\n (3 file(s) with no measurable lines omitted)\n\n Uncovered lines (2)\n - category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n - category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt:12\n System.out.print(\"Something2\");\n\n──────────────────────────────────────────────────────────────\n", buf.String()) requestAsserter.AssertRequestWasMade(t, "/repos/some_org/some_repo/issues/123/comments", "SOME_API_KEY", map[string]interface{}{ - "body": "\n## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n*Modules:* category-search\n\n**Diff coverage:** `88%` 🟢 — `42` of `48` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `42` (88%) | changed code your tests executed |\n| 🔴 Missed instructions | `6` (12%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `8` (53%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `7` (47%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `category-search/src/main/java/com/tgt/CategorySearchApplication.java` | 🟡 73% | 8 / 3 |\n| `category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt` | 🟢 92% | 34 / 3 |\n\n3 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (2)\n\n```\n--- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n--- category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt:12\n System.out.print(\"Something2\");\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", + "body": "## 🛡️ Patch Coverage Report\n\n> Scope: **changed lines only** — the code this PR adds or edits, not whole files or the repo. It answers one thing — *did your tests run the code you just touched?*\n\n*Modules:* category-search\n\n**Diff coverage:** `88%` 🟢 — `42` of `48` changed instructions covered\n\n| Metric | Value | |\n| :-- | --: | :-- |\n| 🟢 Covered instructions | `42` (88%) | changed code your tests executed |\n| 🔴 Missed instructions | `6` (12%) | changed code your tests never ran |\n| 📈 Tracked changed lines | `8` (53%) | lines the coverage tool could measure |\n| ⚪ Untracked changed lines | `7` (47%) | comments, blanks, declarations |\n\n**Lines** = the source lines you changed. **Instructions** = the executable units the coverage tool counts inside those lines — one line can hold several (e.g. JaCoCo bytecode), so the two counts differ.\n\n### Coverage by file\n\n| File | Diff coverage | Covered / Missed |\n| :-- | :-: | :-: |\n| `category-search/src/main/java/com/tgt/CategorySearchApplication.java` | 🟡 73% | 8 / 3 |\n| `category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt` | 🟢 92% | 34 / 3 |\n\n3 changed file(s) with no measurable lines (config, docs, generated, or test-only) omitted.\n\n\n
🔍 Uncovered lines (2)\n\n```\n--- category-search/src/main/java/com/tgt/CategorySearchApplication.java:52\n System.out.print(\"Something\");\n--- category-search/src/main/kotlin/com/tgt/SomeOtherClass.kt:12\n System.out.print(\"Something2\");\n```\n
\n\n🤖 Generated by pull-request-code-coverage — coverage for changed lines only.\n", }) propGetter.AssertExpectations(t) diff --git a/internal/plugin/sourcelines/unifieddiff/changed_source_loader.go b/internal/plugin/sourcelines/unifieddiff/changed_source_loader.go index 0302871..a485761 100644 --- a/internal/plugin/sourcelines/unifieddiff/changed_source_loader.go +++ b/internal/plugin/sourcelines/unifieddiff/changed_source_loader.go @@ -39,7 +39,6 @@ func (l *Loader) Load(inReader io.Reader) ([]domain.SourceLine, error) { var changedFileLine = regexp.MustCompile("^[+][+][+][ ]b?[/](.*)") var changedLineCounts = regexp.MustCompile("^[@][@][ ][-].*?[ ][+](.*?)[ ][@][@].*") var addedLine = regexp.MustCompile("^[+].*") -var contextLine = regexp.MustCompile("^[ ].*") var emptyStr = "" // nolint: gocyclo @@ -140,17 +139,6 @@ func getChangedLinesFromUnifiedDiff(unifiedDiffLines []string, module string, so Module: *currentModule, }) - currentRelativeLine++ - linesLeftInBlock-- - } else if linesLeftInBlock > 0 && contextLine.MatchString(line) { - - // A context line is unchanged code the diff shows for orientation. We - // don't record it (the PR didn't change it), but it still occupies a - // line in the new file and counts against the hunk's line budget, so - // advance both counters to keep subsequent changed-line numbers - // correct. Diffs produced with --unified=0 (the Vela/stdin path) have - // no context lines, so this branch is inert there; it only matters for - // diffs that carry context, such as those fetched from the GitHub API. currentRelativeLine++ linesLeftInBlock-- } diff --git a/internal/plugin/sourcelines/unifieddiff/changed_source_loader_test.go b/internal/plugin/sourcelines/unifieddiff/changed_source_loader_test.go deleted file mode 100644 index 54f6c91..0000000 --- a/internal/plugin/sourcelines/unifieddiff/changed_source_loader_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package unifieddiff - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -// unified=0 is what the git-diff/stdin path produces: no context lines, every -// hunk line is an addition. This pins the original behavior. -func TestLoad_Unified0_NoContextLines(t *testing.T) { - diff := strings.Join([]string{ - "diff --git a/foo.go b/foo.go", - "--- a/foo.go", - "+++ b/foo.go", - "@@ -10,0 +11,2 @@ func foo() {", - "+\ta := 1", - "+\tb := 2", - }, "\n") - - lines, err := NewChangedSourceLinesLoader("", []string{""}).Load(strings.NewReader(diff)) - - assert.NoError(t, err) - assert.Len(t, lines, 2) - assert.Equal(t, 11, lines[0].LineNumber) - assert.Equal(t, "\ta := 1", lines[0].LineValue) - assert.Equal(t, 12, lines[1].LineNumber) - assert.Equal(t, "\tb := 2", lines[1].LineValue) -} - -// unified=3 is what the GitHub API returns: changed lines surrounded by context -// lines. Only the added lines should be recorded, and their line numbers must -// account for the context lines that precede them. -func TestLoad_Unified3_WithContextLines(t *testing.T) { - diff := strings.Join([]string{ - "diff --git a/foo.go b/foo.go", - "index 1234567..89abcde 100644", - "--- a/foo.go", - "+++ b/foo.go", - "@@ -8,7 +8,8 @@ func foo() {", - " \tline8", - " \tline9", - " \tline10", - "-\told", - "+\tnewA", - "+\tnewB", - " \tline12", - " \tline13", - " \tline14", - }, "\n") - - lines, err := NewChangedSourceLinesLoader("", []string{""}).Load(strings.NewReader(diff)) - - assert.NoError(t, err) - assert.Len(t, lines, 2) - // Hunk starts at new-file line 8; three context lines (8,9,10) precede the - // additions, so the first added line is 11. - assert.Equal(t, 11, lines[0].LineNumber) - assert.Equal(t, "\tnewA", lines[0].LineValue) - assert.Equal(t, 12, lines[1].LineNumber) - assert.Equal(t, "\tnewB", lines[1].LineValue) -} - -// A blank context line is emitted as a single space; it must still be counted so -// later line numbers stay correct. -func TestLoad_Unified3_BlankContextLine(t *testing.T) { - diff := strings.Join([]string{ - "+++ b/foo.go", - "@@ -1,3 +1,4 @@", - " first", - " ", - "+added", - " last", - }, "\n") - - lines, err := NewChangedSourceLinesLoader("", []string{""}).Load(strings.NewReader(diff)) - - assert.NoError(t, err) - assert.Len(t, lines, 1) - assert.Equal(t, 3, lines[0].LineNumber) - assert.Equal(t, "added", lines[0].LineValue) -} diff --git a/internal/test/mocks/mock_gh_api.go b/internal/test/mocks/mock_gh_api.go index 5b66d9b..ae99bf2 100644 --- a/internal/test/mocks/mock_gh_api.go +++ b/internal/test/mocks/mock_gh_api.go @@ -11,10 +11,7 @@ import ( "github.com/stretchr/testify/assert" ) -const ( - HTTPResponseOK = 200 - HTTPResponseCreated = 201 -) +const HTTPResponseCreated = 201 type CapturedRequest struct { req *http.Request @@ -31,15 +28,6 @@ func WithMockGithubAPI(doer func(mockServerURL string, requestAsserter GithubAPI body: mustReadAll(r.Body), }) - // The sticky-comment reporter first GETs existing comments to decide - // whether to update or create. Return an empty list so it falls - // through to creating (POST) a new comment. - if r.Method == http.MethodGet { - w.WriteHeader(HTTPResponseOK) - _, _ = w.Write([]byte("[]")) - return - } - w.WriteHeader(HTTPResponseCreated) }), ) diff --git a/internal/test/mocks/property_getter.go b/internal/test/mocks/property_getter.go index da646b2..708966b 100644 --- a/internal/test/mocks/property_getter.go +++ b/internal/test/mocks/property_getter.go @@ -11,30 +11,6 @@ func NewMockPropertyGetter() *MockPropertyGetter { } func (m *MockPropertyGetter) GetProperty(s string) (string, bool) { - // Properties a test did not explicitly stub resolve to ("", false), the same - // as os.LookupEnv for an unset variable. This keeps each test focused on the - // properties it cares about and means looking up a newer optional property - // (e.g. PARAMETER_DIFF_SOURCE) does not panic in tests written before it - // existed. Stubbed properties still go through testify so AssertExpectations - // continues to verify them. - if !m.hasExpectedCall(s) { - return "", false - } - args := m.Called(s) return args.Get(0).(string), args.Bool(1) } - -func (m *MockPropertyGetter) hasExpectedCall(s string) bool { - for _, c := range m.ExpectedCalls { - if c.Method != "GetProperty" || len(c.Arguments) != 1 { - continue - } - - if name, ok := c.Arguments[0].(string); ok && name == s { - return true - } - } - - return false -} diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 0000000..d87678a --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + + + +if [[ ! -f ~/.netrc ]] +then + echo "~/.netrc does not exist, creating..." + + cat >~/.netrc <