Enforce branch-coverage gate on pre-push (mirror CI locally) (#40)

## What
Makes the local **pre-push** git hook enforce the same branch-coverage
gate as CI, so contributors catch coverage regressions before they push
(not just in CI). Two files change: the hook (`.pre-commit-config.yaml`)
and a doc note (`CONTRIBUTING.md`).

## The hook (`.pre-commit-config.yaml`, `run-tests` pre-push hook)
Mirrors `.github/workflows/test.yml`:
1. Runs the full suite with **real torch** + branch coverage: `pytest
--cov=NRS --cov-branch --cov-report=xml`.
2. Then the diff gate: `diff-cover coverage.xml
--compare-branch=origin/main --branch-coverage --fail-under=90` —
**blocks the push if changed code drops below 90% branch coverage**.
3. **Graceful skip** with a warning if `uv` isn't installed (unchanged
behavior); **fails loudly** if `origin/main` can't be resolved (nothing
to diff against).
4. Cleans up `coverage.xml`/`.coverage` on both success and failure
paths, while preserving the exit code so a failing gate still blocks.

All existing hooks are untouched: `prevent-push-to-main`,
`prevent-commit-to-main`, ruff, and the standard hooks. Only the
`run-tests` entry/name changed. Coverage stays on the **pre-push** stage
only — commits remain fast.

### Uses `uvx`, not `uv run` (important)
The invocations use `uvx --with torch --with pytest-cov --with
diff-cover ...`. An earlier `uv run` version was verified to **break the
push**: `uv run` syncs the project env and mutates `uv.lock` mid-hook,
which conflicts with pre-commit's stash/restore and aborts the push
(`Stashed changes conflicted with hook auto-fixes... Rolling back`).
`uvx` runs in an ephemeral env and never touches `uv.lock` — matching
how the project runs tests locally everywhere else.

## Docs (`CONTRIBUTING.md`)
Added a short note right after the `pre-commit install --hook-type
pre-push` instruction explaining the gate, the `uv` requirement (skips
with a warning otherwise), and that `origin/main` must be fetched for
the diff.

## Validation
- Ran the hook's exact command: **45 tests pass**, diff-cover passes;
`git status` after confirms `uv.lock` is **not** mutated (the
stash-conflict path can't recur).
- `uvx pre-commit run --hook-stage pre-push` completes with no
stash-conflict rollback.
- **This very PR's push exercised the hook live** — it ran the gate and
pushed cleanly.
- Only `.pre-commit-config.yaml` and `CONTRIBUTING.md` changed;
`pyproject.toml`/`uv.lock`/tests/`NRS/` untouched.

## Out-of-scope observation
The `ruff-format` hook has no `stages:` restriction, so a manual
`--all-files` dry-run reformats files broadly. Pre-existing; not
addressed here.
This commit is contained in:
Reithan
2026-08-14 02:40:33 -07:00
committed by GitHub
parent 950b55df5f
commit e0e48d12c8
2 changed files with 8 additions and 2 deletions
+2 -2
View File
@@ -36,8 +36,8 @@ repos:
- repo: local
hooks:
- id: run-tests
name: Run pytest tests
entry: bash -c 'if command -v uv > /dev/null 2>&1; then uv run pytest tests/ || exit 1; else echo "WARNING - uv not found, skipping tests"; fi'
name: Run pytest with branch-coverage gate
entry: bash -c 'if ! command -v uv > /dev/null 2>&1; then echo "WARNING - uv not found, skipping tests and coverage gate"; exit 0; fi; if ! git rev-parse --verify --quiet origin/main > /dev/null 2>&1; then echo "ERROR - origin/main not resolvable locally; fetch origin main and retry"; exit 1; fi; uvx --with torch --with pytest-cov --with diff-cover pytest --cov=NRS --cov-branch --cov-report=xml --cov-report=term-missing tests/; status=$?; if [ $status -ne 0 ]; then rm -f coverage.xml .coverage; exit $status; fi; uvx --with diff-cover diff-cover coverage.xml --compare-branch=origin/main --branch-coverage --fail-under=90; cov_status=$?; rm -f coverage.xml .coverage; exit $cov_status'
language: system
stages: [pre-push]
always_run: true
+6
View File
@@ -54,6 +54,12 @@ Thank you for your interest in contributing! This document provides guidelines f
pre-commit install --hook-type pre-push
```
The pre-push hook runs the full test suite with branch coverage and blocks
the push if changed code drops below 90% branch coverage (via pytest-cov +
diff-cover, mirroring CI). It needs `uv` installed — if `uv` isn't found,
the check is skipped with a warning — and `origin/main` fetched locally so
there's something to diff against.
6. **Verify setup**:
```bash