1+ name : Code Coverage
2+
3+ permissions :
4+ contents : read
5+
6+ on :
7+ pull_request :
8+
9+ jobs :
10+ coverage :
11+ runs-on : ubuntu-latest
12+ steps :
13+ # ----------------------------------------------
14+ # check-out repo and set-up python
15+ # ----------------------------------------------
16+ - name : Check out repository
17+ uses : actions/checkout@v4
18+ with :
19+ fetch-depth : 0 # Needed for coverage comparison
20+ ref : ${{ github.event.pull_request.head.ref || github.ref_name }}
21+ repository : ${{ github.event.pull_request.head.repo.full_name || github.repository }}
22+ - name : Set up python
23+ id : setup-python
24+ uses : actions/setup-python@v5
25+ with :
26+ python-version : " 3.10"
27+ # ----------------------------------------------
28+ # ----- install & configure poetry -----
29+ # ----------------------------------------------
30+ - name : Install Poetry
31+ uses : snok/install-poetry@v1
32+ with :
33+ virtualenvs-create : true
34+ virtualenvs-in-project : true
35+ installer-parallel : true
36+
37+ # ----------------------------------------------
38+ # load cached venv if cache exists
39+ # ----------------------------------------------
40+ - name : Load cached venv
41+ id : cached-poetry-dependencies
42+ uses : actions/cache@v4
43+ with :
44+ path : .venv
45+ key : venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ github.event.repository.name }}-${{ hashFiles('**/poetry.lock') }}
46+ # ----------------------------------------------
47+ # install dependencies if cache does not exist
48+ # ----------------------------------------------
49+ - name : Install dependencies
50+ if : steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
51+ run : poetry install --no-interaction --no-root
52+ # ----------------------------------------------
53+ # install your root project, if required
54+ # ----------------------------------------------
55+ - name : Install library
56+ run : poetry install --no-interaction
57+ # ----------------------------------------------
58+ # run test suite
59+ # ----------------------------------------------
60+ - name : Run tests with coverage
61+ run : poetry run python -m pytest tests/unit --cov=src --cov-report=xml --cov-report=term
62+ # ----------------------------------------------
63+ # check for coverage override
64+ # ----------------------------------------------
65+ - name : Check for coverage override
66+ id : override
67+ run : |
68+ OVERRIDE_COMMENT=$(echo "${{ github.event.pull_request.body }}" | grep -E "SKIP_COVERAGE_CHECK\s*=" || echo "")
69+ if [ -n "$OVERRIDE_COMMENT" ]; then
70+ echo "override=true" >> $GITHUB_OUTPUT
71+ REASON=$(echo "$OVERRIDE_COMMENT" | sed -E 's/.*SKIP_COVERAGE_CHECK\s*=\s*(.+)/\1/')
72+ echo "reason=$REASON" >> $GITHUB_OUTPUT
73+ echo "Coverage override found in PR description: $REASON"
74+ else
75+ echo "override=false" >> $GITHUB_OUTPUT
76+ echo "No coverage override found"
77+ fi
78+ # ----------------------------------------------
79+ # check coverage percentage
80+ # ----------------------------------------------
81+ - name : Check coverage percentage
82+ if : steps.override.outputs.override == 'false'
83+ run : |
84+ COVERAGE_FILE="coverage.xml"
85+ if [ ! -f "$COVERAGE_FILE" ]; then
86+ echo "ERROR: Coverage file not found at $COVERAGE_FILE"
87+ exit 1
88+ fi
89+
90+ # Install xmllint if not available
91+ if ! command -v xmllint &> /dev/null; then
92+ sudo apt-get update && sudo apt-get install -y libxml2-utils
93+ fi
94+
95+ COVERED=$(xmllint --xpath "string(//coverage/@lines-covered)" "$COVERAGE_FILE")
96+ TOTAL=$(xmllint --xpath "string(//coverage/@lines-valid)" "$COVERAGE_FILE")
97+
98+ # Use Python for floating-point math
99+ PERCENTAGE=$(python3 -c "covered=${COVERED}; total=${TOTAL}; print(round((covered/total)*100, 2))")
100+
101+ echo "Branch Coverage: ${PERCENTAGE}%"
102+ echo "Required Coverage: 85%"
103+
104+ # Use Python to compare the coverage with 85
105+ python3 -c "import sys; sys.exit(0 if float('${PERCENTAGE}') >= 85 else 1)"
106+ if [ $? -eq 1 ]; then
107+ echo "ERROR: Coverage is ${PERCENTAGE}%, which is less than the required 85%"
108+ exit 1
109+ else
110+ echo "SUCCESS: Coverage is ${PERCENTAGE}%, which meets the required 85%"
111+ fi
112+ # ----------------------------------------------
113+ # coverage enforcement summary
114+ # ----------------------------------------------
115+ - name : Coverage enforcement summary
116+ run : |
117+ if [ "${{ steps.override.outputs.override }}" == "true" ]; then
118+ echo "⚠️ Coverage checks bypassed: ${{ steps.override.outputs.reason }}"
119+ echo "Please ensure this override is justified and temporary"
120+ else
121+ echo "✅ Coverage checks enforced - minimum 85% required"
122+ fi
0 commit comments