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