5454 - name : Convert to Cobertura XML
5555 run : gocover-cobertura < coverage.out > coverage.xml
5656
57+ - name : Normalize Cobertura paths for diff-cover
58+ run : |
59+ MODULE_PATH=$(go list -m)
60+ python3 -c "from pathlib import Path; import sys; module = sys.argv[1].rstrip('/') + '/'; path = Path('coverage.xml'); path.write_text(path.read_text(encoding='utf-8').replace(f'filename=\"{module}', 'filename=\"'), encoding='utf-8')" "$MODULE_PATH"
61+
5762 - name : Install diff-cover
5863 run : pip install diff-cover
5964
@@ -84,74 +89,103 @@ jobs:
8489 BASE=origin/${{ github.base_ref }}
8590 DIFF_OUTPUT=$(diff-cover coverage.xml \
8691 --compare-branch="${BASE}" \
92+ --format json:diff-cover.json \
8793 --fail-under=${{ env.THRESHOLD_DIFF }} \
8894 2>&1) && DIFF_EXIT=0 || DIFF_EXIT=$?
8995
90- DIFF_PCT=$(echo "$DIFF_OUTPUT" | grep -oP 'Diff coverage is \K[0-9.]+' || echo "N/A")
96+ if [ -f diff-cover.json ]; then
97+ DIFF_PCT=$(python3 -c "import json; report=json.load(open('diff-cover.json', encoding='utf-8')); num_changed_lines=report.get('num_changed_lines', 0); total_num_lines=report.get('total_num_lines', 0); total_percent_covered=report.get('total_percent_covered', 'N/A'); print('N/A' if num_changed_lines > 0 and total_num_lines == 0 else total_percent_covered)")
98+ else
99+ DIFF_PCT="N/A"
100+ fi
101+
102+ if [ "$DIFF_PCT" = "N/A" ]; then
103+ DIFF_DISPLAY="N/A"
104+ else
105+ DIFF_DISPLAY="${DIFF_PCT}%"
106+ fi
107+
91108 echo "diff_pct=${DIFF_PCT}" >> "$GITHUB_OUTPUT"
109+ echo "diff_display=${DIFF_DISPLAY}" >> "$GITHUB_OUTPUT"
92110 echo "$DIFF_OUTPUT"
93111
94- if [ "$DIFF_EXIT" -ne 0 ]; then
112+ if [ "$DIFF_EXIT" -ne 0 ] || [ "$DIFF_PCT" = "N/A" ] ; then
95113 echo "diff_status=fail" >> "$GITHUB_OUTPUT"
114+
115+ if [ "$DIFF_PCT" = "N/A" ]; then
116+ echo "❌ Changed-line coverage could not be computed because diff-cover found changed lines without matching coverage information."
117+ fi
96118 else
97119 echo "diff_status=pass" >> "$GITHUB_OUTPUT"
98120 fi
99121
100122 # ── Generate per-package coverage table ──────────────────────────────
101123 - name : Generate coverage report markdown
124+ if : always()
102125 run : |
103126 {
104127 echo "## 📊 Code Coverage Report"
105128 echo ""
106129
107130 TOTAL="${{ steps.total_coverage.outputs.total }}"
108131 STATUS="${{ steps.total_coverage.outputs.status }}"
132+ [ -n "$TOTAL" ] || TOTAL="N/A"
109133 [ "$STATUS" = "pass" ] && ICON="✅" || ICON="❌"
110134
111135 echo "| Metric | Coverage | Threshold | Status |"
112136 echo "|--------|----------|-----------|--------|"
113137 echo "| Overall | **${TOTAL}%** | ${{ env.THRESHOLD_TOTAL }}% | ${ICON} |"
114138
115139 if [ "${{ github.event_name }}" = "pull_request" ]; then
116- DIFF_PCT ="${{ steps.diff_coverage.outputs.diff_pct }}"
140+ DIFF_DISPLAY ="${{ steps.diff_coverage.outputs.diff_display }}"
117141 DIFF_STATUS="${{ steps.diff_coverage.outputs.diff_status }}"
142+ [ -n "$DIFF_DISPLAY" ] || DIFF_DISPLAY="N/A"
118143 [ "$DIFF_STATUS" = "pass" ] && DIFF_ICON="✅" || DIFF_ICON="❌"
119- echo "| Changed lines | **${DIFF_PCT}% ** | ${{ env.THRESHOLD_DIFF }}% | ${DIFF_ICON} |"
144+ echo "| Changed lines | **${DIFF_DISPLAY} ** | ${{ env.THRESHOLD_DIFF }}% | ${DIFF_ICON} |"
120145 fi
121146
122- echo ""
123- echo "<details>"
124- echo "<summary>📦 Per-package breakdown</summary>"
125- echo ""
126- echo '```'
127- go tool cover -func=coverage.out | grep -v "^total:" | \
128- awk '{printf "%-80s %s\n", $1, $3}' | sort
129- echo ""
130- go tool cover -func=coverage.out | grep "^total:"
131- echo '```'
132- echo ""
133- echo "</details>"
147+ if [ -f coverage.out ]; then
148+ echo ""
149+ echo "<details>"
150+ echo "<summary>📦 Per-package breakdown</summary>"
151+ echo ""
152+ echo '```'
153+ go tool cover -func=coverage.out | grep -v "^total:" | \
154+ awk '{printf "%-80s %s\n", $1, $3}' | sort
155+ echo ""
156+ go tool cover -func=coverage.out | grep "^total:"
157+ echo '```'
158+ echo ""
159+ echo "</details>"
160+ else
161+ echo ""
162+ echo "Coverage artifacts were not generated because the workflow failed before coverage collection completed."
163+ fi
134164 } > coverage-report.md
135165
136166 # ── Write step summary ────────────────────────────────────────────────
137167 - name : Write job summary
168+ if : always()
138169 run : cat coverage-report.md >> "$GITHUB_STEP_SUMMARY"
139170
140171 # ── Save PR number so the comment workflow can find the right PR ──────
141172 - name : Save PR number
142- if : github.event_name == 'pull_request'
173+ if : always() && github.event_name == 'pull_request'
143174 run : echo "${{ github.event.pull_request.number }}" > pr-number.txt
144175
145176 # ── Upload artifacts (report + pr-number for comment workflow) ────────
146177 - name : Upload coverage artifacts
178+ if : always()
147179 uses : actions/upload-artifact@v4
148180 with :
149181 name : coverage-report
150182 path : |
151183 coverage.out
152184 coverage.xml
185+ diff-cover.json
153186 coverage-report.md
154187 pr-number.txt
188+ if-no-files-found : warn
155189 retention-days : 14
156190
157191 - name : Upload coverage to Codecov
@@ -176,6 +210,6 @@ jobs:
176210 github.event_name == 'pull_request' &&
177211 steps.diff_coverage.outputs.diff_status == 'fail'
178212 run : |
179- echo "❌ Changed-line coverage ${{ steps.diff_coverage.outputs.diff_pct }}% is below the required ${{ env.THRESHOLD_DIFF }}%"
213+ echo "❌ Changed-line coverage ${{ steps.diff_coverage.outputs.diff_display }} is below the required ${{ env.THRESHOLD_DIFF }}%"
180214 exit 1
181215
0 commit comments