{"id":51440491,"url":"https://github.com/target/pull-request-code-coverage","last_synced_at":"2026-07-05T11:01:20.116Z","repository":{"id":44308701,"uuid":"451658853","full_name":"target/pull-request-code-coverage","owner":"target","description":"A continuous integration plugin to allow detecting code coverage for only the lines changed in a PR.","archived":false,"fork":false,"pushed_at":"2026-06-22T16:23:55.000Z","size":514,"stargazers_count":2,"open_issues_count":5,"forks_count":3,"subscribers_count":6,"default_branch":"master","last_synced_at":"2026-06-22T18:16:42.752Z","etag":null,"topics":["code-coverage","go","golang","jacoco","pull-request","vela","vela-plugin"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/target.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2022-01-24T22:45:49.000Z","updated_at":"2026-06-22T16:24:02.000Z","dependencies_parsed_at":"2024-06-21T15:41:10.696Z","dependency_job_id":"689772d8-edf8-49fa-ab8d-269ee1ee6186","html_url":"https://github.com/target/pull-request-code-coverage","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/target/pull-request-code-coverage","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/target%2Fpull-request-code-coverage","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/target%2Fpull-request-code-coverage/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/target%2Fpull-request-code-coverage/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/target%2Fpull-request-code-coverage/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/target","download_url":"https://codeload.github.com/target/pull-request-code-coverage/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/target%2Fpull-request-code-coverage/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35151638,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-coverage","go","golang","jacoco","pull-request","vela","vela-plugin"],"created_at":"2026-07-05T11:01:19.262Z","updated_at":"2026-07-05T11:01:20.110Z","avatar_url":"https://github.com/target.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pull-request-code-coverage\n\nA CI plugin that reports code coverage for **only the lines changed in a pull request** — not the whole file, not the whole repo.\n\nWhen you're working to raise a repo's coverage, a whole-repo percentage doesn't tell you whether *your* change is tested. This plugin looks at just the lines your PR adds or edits and reports coverage for those lines, so a reviewer can immediately see whether the new code is covered.\n\nIt supports **JVM, Go, and Python** projects, and works out of the box for [Vela](https://github.com/go-vela) (Target's open-source CI/CD) as well as any CI that can run a Docker container (e.g. GitHub Actions).\n\n---\n\n## Contents\n\n- [How it works](#how-it-works)\n- [Lines vs. instructions](#lines-vs-instructions)\n- [What you'll see](#what-youll-see)\n  - [In the CI/CD console](#in-the-cicd-console)\n  - [As a pull-request comment](#as-a-pull-request-comment)\n- [Supported coverage formats](#supported-coverage-formats)\n- [Usage](#usage)\n  - [Docker image](#docker-image)\n  - [JVM projects (jacoco)](#jvm-projects-jacoco)\n  - [Go projects (cobertura)](#go-projects-cobertura)\n  - [Python projects (python)](#python-projects-python)\n  - [JavaScript / TypeScript projects (lcov)](#javascript--typescript-projects-lcov)\n  - [Running outside Vela](#running-outside-vela)\n- [Parameters](#parameters)\n- [Development](#development)\n- [License](#license)\n\n---\n\n## How it works\n\n1. It reads the PR's unified diff to find the lines you changed.\n2. It reads your coverage report (JaCoCo / Cobertura / coverage.py).\n3. For each changed line, it checks whether your tests executed it.\n4. It reports the result in two places: the **CI/CD console** (always) and a **pull-request comment** (when GitHub credentials are provided).\n\n---\n\n## Lines vs. instructions\n\nThe report uses two different units, and they are **not** the same thing:\n\n- **Lines** — the source lines your PR changed.\n- **Instructions** — the smaller executable units the coverage tool counts *inside* those lines.\n\nFor **JaCoCo (JVM)**, a single source line compiles to several JVM bytecode instructions, so one line can be partly covered — e.g. `8 covered / 3 missed` instructions spread across only `2` measurable lines. For **Go (cobertura)** and **Python (coverage.py)**, the plugin counts one instruction per line, so there the two numbers line up.\n\nEvery changed line falls into one of these buckets:\n\n| Bucket | Meaning |\n|---|---|\n| 🟢 Covered instructions | changed code your tests executed |\n| 🔴 Missed instructions | changed code your tests never ran |\n| 📈 Tracked changed lines | changed lines the coverage tool could measure |\n| ⚪ Untracked changed lines | changed lines with no coverage data: comments, blanks, declarations |\n\n**Diff coverage** is the headline number: `covered ÷ (covered + missed)` instructions.\n\n---\n\n## What you'll see\n\nThe same data is rendered for two audiences: plain text for the build log, and Markdown for the PR.\n\n### In the CI/CD console\n\nEvery run prints a report to the step's console (stdout):\n\n```\n──────────────────────────────────────────────────────────────\n 📊 Patch Coverage Report — 73% 🟡 — 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```\n\nWhat each part shows:\n\n- **Diff coverage** — the headline: how much of your changed, *measurable* code ran.\n- **Summary** — the four buckets from [Lines vs. instructions](#lines-vs-instructions).\n- **Coverage by file** — per-file diff coverage, **lowest first** so the riskiest files surface at the top. Files whose only changed lines aren't measurable (config, docs, tests) are collapsed into a count.\n- **Uncovered lines** — each changed line your tests never ran, with the source line.\n\n### As a pull-request comment\n\nWhen GitHub credentials are set, the same report is posted as a PR comment. It renders like this:\n\n\u003e ### 🛡️ Patch Coverage Report — `73%` 🟡\n\u003e\n\u003e 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\u003e\n\u003e *Modules:* category-search\n\u003e\n\u003e **Diff coverage:** `73%` 🟡 — `8` of `11` changed instructions covered\n\u003e\n\u003e | Metric | Value | |\n\u003e | :-- | --: | :-- |\n\u003e | 🟢 Covered instructions | `8` (73%) | changed code your tests executed |\n\u003e | 🔴 Missed instructions | `3` (27%) | changed code your tests never ran |\n\u003e | 📈 Tracked changed lines | `2` (22%) | lines the coverage tool could measure |\n\u003e | ⚪ Untracked changed lines | `7` (78%) | comments, blanks, declarations |\n\u003e\n\u003e **Coverage by file**\n\u003e\n\u003e | File | Diff coverage | Covered / Missed |\n\u003e | :-- | :-: | :-: |\n\u003e | `category-search/src/main/java/com/tgt/CategorySearchApplication.java` | 🟡 73% | 8 / 3 |\n\u003e\n\u003e \u003cdetails\u003e\u003csummary\u003e🔍 Uncovered lines (1)\u003c/summary\u003e\n\u003e\n\u003e `category-search/.../CategorySearchApplication.java:52` → `System.out.print(\"Something\");`\n\u003e \u003c/details\u003e\n\nIt carries the same sections as the console — diff-coverage headline, summary table, per-file breakdown, and a collapsible list of uncovered lines.\n\n\u003e **Note:** the PR comment is posted only when `gh_api_key`, the PR number, the org, and the repo name are all available. Without them the plugin still prints the console report.\n\u003e\n\u003e The comment is **sticky**: on later pushes the plugin finds the comment it posted before (via a hidden marker) and updates it in place, so a PR keeps a single, always-current coverage comment instead of a new one per push.\n\n---\n\n## Supported coverage formats\n\n| `coverage_type` | Language(s) | Report format |\n|---|---|---|\n| `jacoco` | Java, Kotlin, Scala (JVM) | JaCoCo XML |\n| `cobertura` | Go | Cobertura XML via [gocov-xml](https://github.com/AlekSi/gocov-xml) |\n| `python` | Python | coverage.py XML (`coverage xml` / pytest-cov `--cov-report=xml`) |\n| `lcov` | JavaScript, TypeScript | LCOV `lcov.info` from Jest / nyc / Vitest / c8 (aliases: `javascript`, `typescript`) |\n\n---\n\n## Usage\n\n### Docker image\n\nThe plugin runs as a Docker container, published to the GitHub Container Registry (GHCR) on every release:\n\n```\nghcr.io/target/pull-request-code-coverage:latest\nghcr.io/target/pull-request-code-coverage:\u003crelease-tag\u003e\n```\n\n```\ndocker pull ghcr.io/target/pull-request-code-coverage:latest\n```\n\nThe examples below use [Vela](https://github.com/go-vela) step syntax. See [Running outside Vela](#running-outside-vela) for other CIs.\n\n### JVM projects (jacoco)\n\nYou need a JaCoCo XML report as input. Generating it (via Gradle/Maven) is outside the scope of this project. Once you have it, pass its path to `coverage_file`:\n\n```yaml\n- name: check-pr-code-coverage\n   image: ghcr.io/target/pull-request-code-coverage:latest\n   pull: true\n   ruleset:\n     event: [pull_request]\n   parameters:\n     coverage_type: jacoco\n     coverage_file: some-sub-module/build/reports/jacoco/test/jacocoTestReport.xml\n     source_dirs:\n       - src/main/java\n       - src/main/kotlin\n     # omit for public github.com (defaults to https://api.github.com)\n     # for GitHub Enterprise, use the full API root including /api/v3\n     gh_api_base_url: https://git.target.com/api/v3\n     module: some-sub-module\n   secrets:\n     - source: pull_request_api_key\n       target: plugin_gh_api_key\n```\n\n### Go projects (cobertura)\n\nUse [gocov-xml](https://github.com/AlekSi/gocov-xml) to convert Go's coverage profile to Cobertura XML:\n\n```\n - go get github.com/axw/gocov/gocov\n - go get github.com/AlekSi/gocov-xml\n - go test -v -coverpkg=./... -coverprofile=coverage.txt ./...\n - go tool cover -func=coverage.txt\n - gocov convert coverage.txt | gocov-xml \u003e ./coverage.xml\n```\n\nThen pass `coverage.xml` to the plugin:\n\n```yaml\n- name: check-pr-code-coverage\n   image: ghcr.io/target/pull-request-code-coverage:latest\n   pull: true\n   ruleset:\n     event: [pull_request]\n   parameters:\n     coverage_type: cobertura\n     # coverage.xml generated in the step above\n     coverage_file: coverage.xml\n     source_dirs:\n       - /vela/src/github.com/targetOSS/pull-request-code-coverage\n     gh_api_base_url: https://git.target.com/api/v3\n   secrets:\n     - source: pull_request_api_key\n       target: plugin_gh_api_key\n```\n\n\u003e For `cobertura`, `source_dirs` must match the `\u003csource\u003e` path in the generated XML (the directory the tests ran in), and only a single source dir is supported.\n\n### Python projects (python)\n\nGenerate the XML report with [coverage.py](https://coverage.readthedocs.io) or pytest-cov:\n\n```\n # with coverage.py\n - coverage run -m pytest\n - coverage xml          # writes coverage.xml\n\n # or directly with pytest-cov\n - pytest --cov=myapp --cov-report=xml\n```\n\nThen pass `coverage.xml` with `coverage_type: python`:\n\n```yaml\n- name: check-pr-code-coverage\n   image: ghcr.io/target/pull-request-code-coverage:latest\n   pull: true\n   ruleset:\n     event: [pull_request]\n   parameters:\n     coverage_type: python\n     # coverage.xml generated in the step above\n     coverage_file: coverage.xml\n     source_dirs:\n       # repo root; use e.g. \"src\" if your package lives under src/\n       - .\n     gh_api_base_url: https://git.target.com/api/v3\n   secrets:\n     - source: pull_request_api_key\n       target: plugin_gh_api_key\n```\n\n\u003e Unlike `cobertura`, the `python` type matches files by their **repo-relative path**, so `source_dirs` does **not** need to be an absolute build path. Run from the repo root with `source_dirs: [\".\"]`, or set it to your source folder (e.g. `src`) if your code lives under one.\n\n### JavaScript / TypeScript projects (lcov)\n\nMost JS/TS coverage tools (Jest, nyc, Vitest, c8 — all built on Istanbul) emit an `lcov.info` file. Generate it with the `lcov` reporter:\n\n```\n # Jest\n - jest --coverage --coverageReporters=lcov\n # nyc\n - nyc --reporter=lcov npm test\n # Vitest\n - vitest run --coverage --coverage.reporter=lcov\n```\n\nThen pass the report (commonly `coverage/lcov.info`) with `coverage_type: lcov`:\n\n```yaml\n- name: check-pr-code-coverage\n   image: ghcr.io/target/pull-request-code-coverage:latest\n   pull: true\n   ruleset:\n     event: [pull_request]\n   parameters:\n     coverage_type: lcov            # aliases: javascript, typescript\n     # lcov.info generated in the step above\n     coverage_file: coverage/lcov.info\n     source_dirs:\n       # repo root; use e.g. \"src\" if your code lives under src/\n       - .\n     gh_api_base_url: https://git.target.com/api/v3\n   secrets:\n     - source: pull_request_api_key\n       target: plugin_gh_api_key\n```\n\n\u003e Like `python`, the `lcov` type matches files by their **repo-relative path**, and it also handles the absolute `SF:` paths Istanbul commonly writes (e.g. `/home/runner/work/app/app/src/x.ts`) by suffix-matching. Set `source_dirs` to `.` (repo root) or to your source folder.\n\n### Running outside Vela\n\nOn other CIs (e.g. GitHub Actions), run the same image and pass the inputs as environment variables instead of Vela `parameters:`. Each parameter maps to a `PARAMETER_\u003cNAME\u003e` env var, and the build context maps to `BUILD_PULL_REQUEST_NUMBER`, `REPOSITORY_ORG`, and `REPOSITORY_NAME` (see the table below). Pipe the PR's unified diff to the container on stdin:\n\n```\ngit --no-pager diff --unified=0 \"origin/$BASE_REF\" -- '*.go' | docker run --rm -i \\\n  -e PARAMETER_COVERAGE_TYPE -e PARAMETER_COVERAGE_FILE -e PARAMETER_SOURCE_DIRS \\\n  -e PARAMETER_GH_API_KEY -e BUILD_PULL_REQUEST_NUMBER -e REPOSITORY_ORG -e REPOSITORY_NAME \\\n  ghcr.io/target/pull-request-code-coverage:latest\n```\n\nA working GitHub Actions example lives in [`.github/workflows/pr-coverage.yml`](.github/workflows/pr-coverage.yml).\n\n---\n\n## Parameters\n\n**Plugin inputs** — set via Vela `parameters:` / `secrets:`, or as `PARAMETER_*` env vars on other CIs.\n\n| Parameter | Env var | Required | Default | Description |\n|---|---|---|---|---|\n| `coverage_type` | `PARAMETER_COVERAGE_TYPE` | yes | | coverage format: `jacoco`, `cobertura`, `python`, or `lcov` (aliases `javascript`/`typescript`) |\n| `coverage_file` | `PARAMETER_COVERAGE_FILE` | yes | | path to the coverage report, relative to the working dir |\n| `source_dirs` | `PARAMETER_SOURCE_DIRS` | yes | | array of source dirs, relative to the working dir (see per-language notes above) |\n| `module` | `PARAMETER_MODULE` | no | _(empty)_ | sub-module path prefix to strip, for multi-module projects (e.g. a Gradle multi-project build) |\n| `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) |\n| `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` |\n| `enabled` | `PARAMETER_ENABLED` | no | `true` | master on/off switch. Set to `false` to make the plugin do nothing and exit 0, so it can stay wired into a pipeline and be toggled off without removing the step. Any other value (or unset) means enabled |\n| `min_coverage` | `PARAMETER_MIN_COVERAGE` | no | _(empty)_ | diff-coverage gate: a percentage (e.g. `80`). When set, the step **fails** (exit 1) if diff coverage falls below it. Leave unset/empty to disable the gate (report only). A PR that changed no measurable lines counts as 100% and never fails |\n| `debug` | `PARAMETER_DEBUG` | no | `false` | enable debug logging |\n\n**Build context** — provided automatically by Vela; set these yourself on other CIs to enable the PR comment.\n\n| Env var | Description |\n|---|---|\n| `BUILD_PULL_REQUEST_NUMBER` | the PR number to comment on |\n| `REPOSITORY_ORG` | repository owner / org |\n| `REPOSITORY_NAME` | repository name |\n\n\u003e 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.\n\n---\n\n## Development\n\nThis project needs go (\u003e= 1.26.3) installed. Before submitting a PR, run:\n\n* `make format`\n* `make lint`\n\n---\n\n## License\n\nThis project is licensed under the Apache License, Version 2.0.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftarget%2Fpull-request-code-coverage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftarget%2Fpull-request-code-coverage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftarget%2Fpull-request-code-coverage/lists"}