@@ -279,6 +279,60 @@ jobs:
279279 echo "base_xmls=$BASE_XMLS" >> "$GITHUB_OUTPUT"
280280 echo "pr_xmls=$PR_XMLS" >> "$GITHUB_OUTPUT"
281281
282+ - name : Set up Python
283+ uses : actions/setup-python@v5
284+ with :
285+ python-version : ' 3.11'
286+
287+ - name : Changed-line coverage (diff-cover)
288+ id : diff-cover
289+ env :
290+ BASE_REF : ${{ github.event.pull_request.base.ref }}
291+ run : |
292+ set -euo pipefail
293+ pip install --quiet 'diff-cover==9.2.0'
294+
295+ # Ensure the base branch ref is available locally for diff-cover.
296+ git fetch --no-tags origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
297+
298+ PR_XMLS=$(find coverage/pr -name "jacocoTestReport.xml" | sort)
299+ if [ -z "$PR_XMLS" ]; then
300+ echo "No PR jacoco XML reports found, skipping diff-cover."
301+ exit 0
302+ fi
303+
304+ SRC_ROOTS=$(find . -type d -path '*/src/main/java' \
305+ -not -path './coverage/*' -not -path './.git/*' | sort)
306+
307+ set +e
308+ diff-cover $PR_XMLS \
309+ --compare-branch="origin/${BASE_REF}" \
310+ --src-roots $SRC_ROOTS \
311+ --fail-under=0 \
312+ --json-report=diff-cover.json \
313+ --markdown-report=diff-cover.md
314+ DIFF_RC=$?
315+ set -e
316+
317+ CHANGED_LINE_COVERAGE=$(jq -r '.total_percent_covered // empty' diff-cover.json)
318+ if [ -z "$CHANGED_LINE_COVERAGE" ]; then
319+ echo "Unable to parse changed-line coverage from diff-cover.json."
320+ exit 1
321+ fi
322+ echo "changed_line_coverage=${CHANGED_LINE_COVERAGE}" >> "$GITHUB_OUTPUT"
323+
324+ {
325+ echo "### Changed-line Coverage (diff-cover)"
326+ echo ""
327+ if [ -f diff-cover.md ] && [ -s diff-cover.md ]; then
328+ cat diff-cover.md
329+ else
330+ echo "_diff-cover produced no report._"
331+ fi
332+ } >> "$GITHUB_STEP_SUMMARY"
333+
334+ echo "diff-cover exit code: ${DIFF_RC}"
335+
282336 - name : Aggregate base coverage
283337 id : jacoco-base
284338 uses : madrapps/jacoco-report@v1.7.2
@@ -288,6 +342,7 @@ jobs:
288342 min-coverage-overall : 0
289343 min-coverage-changed-files : 0
290344 skip-if-no-changes : true
345+ comment-type : summary
291346 title : ' ## Base Coverage Snapshot'
292347 update-comment : false
293348
@@ -300,14 +355,15 @@ jobs:
300355 min-coverage-overall : 0
301356 min-coverage-changed-files : 0
302357 skip-if-no-changes : true
358+ comment-type : summary
303359 title : ' ## PR Code Coverage Report'
304360 update-comment : false
305361
306362 - name : Enforce coverage gates
307363 env :
308364 BASE_OVERALL_RAW : ${{ steps.jacoco-base.outputs.coverage-overall }}
309365 PR_OVERALL_RAW : ${{ steps.jacoco-pr.outputs.coverage-overall }}
310- PR_CHANGED_RAW : ${{ steps.jacoco-pr .outputs.coverage-changed-files }}
366+ CHANGED_LINE_RAW : ${{ steps.diff-cover .outputs.changed_line_coverage }}
311367 run : |
312368 set -euo pipefail
313369
@@ -329,7 +385,7 @@ jobs:
329385 # 1) Parse metrics from jacoco-report outputs
330386 BASE_OVERALL="$(sanitize "$BASE_OVERALL_RAW")"
331387 PR_OVERALL="$(sanitize "$PR_OVERALL_RAW")"
332- PR_CHANGED ="$(sanitize "$PR_CHANGED_RAW ")"
388+ CHANGED_LINE ="$(sanitize "$CHANGED_LINE_RAW ")"
333389
334390 if ! is_number "$BASE_OVERALL" || ! is_number "$PR_OVERALL"; then
335391 echo "Failed to parse coverage values: base='${BASE_OVERALL}', pr='${PR_OVERALL}'."
@@ -340,19 +396,16 @@ jobs:
340396 DELTA=$(awk -v pr="$PR_OVERALL" -v base="$BASE_OVERALL" 'BEGIN { printf "%.4f", pr - base }')
341397 DELTA_OK=$(compare_float "${DELTA} >= ${MAX_DROP}")
342398
343- CHANGED_STATUS="SKIPPED (no changed coverage value)"
344- CHANGED_OK=1
345- if [ -n "$PR_CHANGED" ] && [ "$PR_CHANGED" != "NaN" ]; then
346- if ! is_number "$PR_CHANGED"; then
347- echo "Failed to parse changed-files coverage: changed='${PR_CHANGED}'."
348- exit 1
349- fi
350- CHANGED_OK=$(compare_float "${PR_CHANGED} > ${MIN_CHANGED}")
351- if [ "$CHANGED_OK" -eq 1 ]; then
352- CHANGED_STATUS="PASS (> ${MIN_CHANGED}%)"
353- else
354- CHANGED_STATUS="FAIL (<= ${MIN_CHANGED}%)"
355- fi
399+ if [ -z "$CHANGED_LINE" ] || [ "$CHANGED_LINE" = "NaN" ] || ! is_number "$CHANGED_LINE"; then
400+ echo "Failed to parse changed-line coverage: changed-line='${CHANGED_LINE}'."
401+ exit 1
402+ fi
403+
404+ CHANGED_LINE_OK=$(compare_float "${CHANGED_LINE} > ${MIN_CHANGED}")
405+ if [ "$CHANGED_LINE_OK" -eq 1 ]; then
406+ CHANGED_LINE_STATUS="PASS (> ${MIN_CHANGED}%)"
407+ else
408+ CHANGED_LINE_STATUS="FAIL (<= ${MIN_CHANGED}%)"
356409 fi
357410
358411 # 3) Output base metrics (always visible in logs + step summary)
@@ -362,11 +415,11 @@ jobs:
362415 fi
363416
364417 METRICS_TEXT=$(cat <<EOF
365- Changed Files Coverage: ${PR_CHANGED }%
418+ Changed-line Coverage: ${CHANGED_LINE }%
366419 PR Overall Coverage: ${PR_OVERALL}%
367420 Base Overall Coverage: ${BASE_OVERALL}%
368421 Delta (PR - Base): ${DELTA}%
369- Changed Files Gate: ${CHANGED_STATUS }
422+ Changed-line Gate: ${CHANGED_LINE_STATUS }
370423 Overall Delta Gate: ${OVERALL_STATUS}
371424 EOF
372425 )
@@ -376,11 +429,11 @@ jobs:
376429 {
377430 echo "### Coverage Gate Metrics"
378431 echo ""
379- echo "- Changed Files Coverage: ${PR_CHANGED }%"
432+ echo "- Changed-line Coverage: ${CHANGED_LINE }%"
380433 echo "- PR Overall Coverage: ${PR_OVERALL}%"
381434 echo "- Base Overall Coverage: ${BASE_OVERALL}%"
382435 echo "- Delta (PR - Base): ${DELTA}%"
383- echo "- Changed Files Gate: ${CHANGED_STATUS }"
436+ echo "- Changed-line Gate: ${CHANGED_LINE_STATUS }"
384437 echo "- Overall Delta Gate: ${OVERALL_STATUS}"
385438 } >> "$GITHUB_STEP_SUMMARY"
386439
@@ -391,14 +444,9 @@ jobs:
391444 exit 1
392445 fi
393446
394- if [ -z "$PR_CHANGED" ] || [ "$PR_CHANGED" = "NaN" ]; then
395- echo "No changed-files coverage value detected, skip changed-files gate."
396- exit 0
397- fi
398-
399- if [ "$CHANGED_OK" -ne 1 ]; then
400- echo "Coverage gate failed: changed files coverage must be > 60%."
401- echo "changed=${PR_CHANGED}%"
447+ if [ "$CHANGED_LINE_OK" -ne 1 ]; then
448+ echo "Coverage gate failed: changed-line coverage must be > 60%."
449+ echo "changed-line=${CHANGED_LINE}%"
402450 exit 1
403451 fi
404452
0 commit comments