diff --git a/.gitignore b/.gitignore index ac22b0c9..1ced00ab 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Go backend backend/data/ *.db +backend/coverage*.out # Node frontend frontend/node_modules/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9c9fc718..902627fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,6 +40,12 @@ repos: language: system types: [go] + - id: go-test-coverage + name: go test (with coverage enforcement) + entry: bash scripts/go-test-coverage.sh + language: system + pass_filenames: false + - id: golangci-lint name: golangci-lint (project linter) entry: golangci-lint run diff --git a/README.md b/README.md index 3c98a56d..a61776ff 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ See `VERSION.md` for complete versioning documentation. - **Branching model**: `development` is the integration branch; open PRs from `feature/**` - **CI**: `.github/workflows/ci.yml` runs Go tests, ESLint, and frontend builds - **Docker**: Multi-stage build with Node (frontend) → Go (backend) → Alpine runtime +- **Pre-commit**: `.pre-commit-config.yaml` runs formatters, linters, and now `go test` with coverage enforcement (`CPM_MIN_COVERAGE=75` by default) ## Contributing - See `CONTRIBUTING.md` (coming soon) for contribution guidelines. diff --git a/scripts/go-test-coverage.sh b/scripts/go-test-coverage.sh new file mode 100755 index 00000000..68ea8a5d --- /dev/null +++ b/scripts/go-test-coverage.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BACKEND_DIR="$ROOT_DIR/backend" +COVERAGE_FILE="$BACKEND_DIR/coverage.pre-commit.out" +MIN_COVERAGE="${CPM_MIN_COVERAGE:-75}" + +cd "$BACKEND_DIR" + +go test -coverprofile="$COVERAGE_FILE" ./... + +go tool cover -func="$COVERAGE_FILE" | tail -n 1 +TOTAL_LINE=$(go tool cover -func="$COVERAGE_FILE" | grep total) +TOTAL_PERCENT=$(echo "$TOTAL_LINE" | awk '{print substr($3, 1, length($3)-1)}') + +echo "Computed coverage: ${TOTAL_PERCENT}% (minimum required ${MIN_COVERAGE}%)" + +export TOTAL_PERCENT +export MIN_COVERAGE + +python3 - <<'PY' +import os, sys +from decimal import Decimal + +total = Decimal(os.environ['TOTAL_PERCENT']) +minimum = Decimal(os.environ['MIN_COVERAGE']) +if total < minimum: + print(f"Coverage {total}% is below required {minimum}% (set CPM_MIN_COVERAGE to override)", file=sys.stderr) + sys.exit(1) +PY + +rm -f "$COVERAGE_FILE" + +echo "Coverage requirement met"