Skip to content

Commit 78c98ae

Browse files
ci: gate PRs on 100% patch coverage + 95% project floor (#149)
* ci(coverage): gate PRs on 100% patch coverage + 95% project floor Adds diff-cover patch-coverage enforcement to the coverage workflow: every changed line in a PR must be covered by a test (--fail-under=100), and total project coverage must stay >=95%. Go coverage is converted to Cobertura via gocover-cobertura so diff-cover can read it. fetch-depth: 0 lets diff-cover resolve origin/<base_ref>; gated to pull_request events. New org mandate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: measure 95% project floor over production code only The >=95% project floor was computed over the full coverage.out, which includes non-shippable packages (internal/testhelpers ~5%, cmd/smoke-buildinfo, e2e, generated *_pb.go). That dilutes the denominator and turns a coverage gate into noise from test scaffolding. Filter those package classes out of the profile before go tool cover -func so the floor reflects real production code. Correct measurement, not a waiver — no internal/<domain> package is excluded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5909f59 commit 78c98ae

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

.github/workflows/coverage.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ jobs:
9696
- uses: actions/checkout@v4
9797
with:
9898
path: api
99+
# Full history so diff-cover can resolve origin/<base_ref> for the
100+
# patch-coverage gate below (shallow clones lack the base commit).
101+
fetch-depth: 0
99102

100103
- name: Checkout proto sibling (for go.mod replace ../proto)
101104
uses: actions/checkout@v4
@@ -147,3 +150,59 @@ jobs:
147150
files: api/coverage.out
148151
flags: api
149152
fail_ci_if_error: false
153+
154+
# ------------------------------------------------------------------
155+
# Org patch-coverage mandate: every changed line in a PR diff must be
156+
# covered by a test (100%), and the project floor stays >=95%.
157+
# Tool: diff-cover (https://github.com/Bachmann1234/diff-cover) reads a
158+
# Cobertura report + the git diff vs the base branch. The "Generate
159+
# coverage" step above is continue-on-error, so it still produces
160+
# coverage.out even if a flaky test trips — the gate reads that file.
161+
# ------------------------------------------------------------------
162+
- uses: actions/setup-python@v5
163+
if: github.event_name == 'pull_request'
164+
with:
165+
python-version: '3.12'
166+
- name: Install diff-cover + cobertura converter
167+
if: github.event_name == 'pull_request'
168+
run: |
169+
pip install diff-cover
170+
go install github.com/boumenot/gocover-cobertura@latest
171+
- name: Convert coverage to Cobertura
172+
if: github.event_name == 'pull_request'
173+
working-directory: api
174+
run: $(go env GOPATH)/bin/gocover-cobertura < coverage.out > coverage.xml
175+
- name: Patch coverage gate (100% of changed lines)
176+
if: github.event_name == 'pull_request'
177+
working-directory: api
178+
run: |
179+
git fetch origin "${{ github.base_ref }}" --depth=1 || true
180+
diff-cover coverage.xml \
181+
--compare-branch="origin/${{ github.base_ref }}" \
182+
--fail-under=100
183+
- name: Project coverage floor (>=95% production code)
184+
if: github.event_name == 'pull_request'
185+
working-directory: api
186+
# The >=95% floor is measured over PRODUCTION code only. We drop
187+
# genuinely-non-shippable packages from the coverage profile before
188+
# computing the total — this is correct measurement, NOT a waiver.
189+
# No internal/<domain> production package is ever excluded here.
190+
#
191+
# Excluded (and why):
192+
# internal/testhelpers — test-DB/setup harness, imported only by
193+
# tests; never runs in prod (sits ~5%).
194+
# cmd/smoke-buildinfo — diagnostic/smoke binary, not shipped logic.
195+
# cmd/* — pure diagnostic/smoke binaries.
196+
# e2e/ — black-box E2E suite (//go:build e2e).
197+
# proto/gen, *_pb.go — generated protobuf code.
198+
# Build-tag-gated files (//go:build e2e|integration|chaos|loadtest)
199+
# are not compiled into the `-short` run, so they never appear in
200+
# coverage.out — the path filter below is belt-and-suspenders.
201+
run: |
202+
# Keep the `mode:` header line; drop excluded package paths.
203+
grep -vE '(/internal/testhelpers/|/cmd/|/e2e/|/proto/gen/|_pb\.go:)' \
204+
coverage.out > coverage.prod.out
205+
total=$(go tool cover -func=coverage.prod.out | tail -1 | awk '{print $3}' | tr -d '%')
206+
echo "Total project coverage: ${total}%"
207+
awk -v t="$total" 'BEGIN { exit (t+0 >= 95) ? 0 : 1 }' \
208+
|| { echo "::error::Production coverage ${total}% is below the 95% floor"; exit 1; }

0 commit comments

Comments
 (0)