From e0e48d12c8d45e2c196bca0e7bfaa9fcdfe973ca Mon Sep 17 00:00:00 2001 From: Reithan Date: Fri, 14 Aug 2026 02:40:33 -0700 Subject: [PATCH] Enforce branch-coverage gate on pre-push (mirror CI locally) (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- .pre-commit-config.yaml | 4 ++-- CONTRIBUTING.md | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f0fb1e..6fd27f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cec4207..139410e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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