-
Notifications
You must be signed in to change notification settings - Fork 0
638 lines (537 loc) · 28.4 KB
/
Copy pathcoverage-report.yml
File metadata and controls
638 lines (537 loc) · 28.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
name: Coverage Report
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'src/**'
- 'tests/**'
- '*.sln'
- '*.csproj'
- 'coverlet.runsettings'
- '.github/workflows/coverage-report.yml'
permissions:
contents: read
pull-requests: write
checks: write
jobs:
detect-changes:
name: Detect File Changes
runs-on: ubuntu-latest
outputs:
skip_coverage: ${{ steps.file_changes.outputs.skip_coverage }}
change_type: ${{ steps.file_changes.outputs.change_type }}
src_files: ${{ steps.file_changes.outputs.src_files }}
test_files: ${{ steps.file_changes.outputs.test_files }}
config_files: ${{ steps.file_changes.outputs.config_files }}
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for merge detection
- name: Detect File Changes
id: file_changes
run: |
# Fetch base branch for comparison
git fetch origin ${{ github.base_ref }}
# Get list of changed files
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
# Count different types of changes
SRC_FILES=$(echo "$CHANGED_FILES" | grep -E '^src/.*\.cs$' | grep -v -E '(Migrations/|\.Designer\.cs$|DbContextModelSnapshot\.cs$)' | wc -l || echo "0")
TEST_FILES=$(echo "$CHANGED_FILES" | grep -E '^tests/.*\.cs$' | wc -l || echo "0")
CONFIG_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(json|csproj|sln|props|targets)$' | wc -l || echo "0")
WORKFLOW_FILES=$(echo "$CHANGED_FILES" | grep -E '\.github/workflows/.*\.yml$' | wc -l || echo "0")
DOC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(md|txt)$' | wc -l || echo "0")
# Check if this is a merge commit
IS_MERGE=$(git rev-list --merges HEAD^..HEAD | wc -l)
# Determine change type
if [ "$SRC_FILES" -eq 0 ] && [ "$TEST_FILES" -eq 0 ]; then
CHANGE_TYPE="config-only"
SKIP_COVERAGE="true"
echo "::notice::No source or test code changes detected. Coverage check will be skipped."
elif [ "$SRC_FILES" -eq 0 ] && [ "$TEST_FILES" -gt 0 ]; then
CHANGE_TYPE="test-only"
SKIP_COVERAGE="informational"
echo "::notice::Only test files changed. Coverage check will be informational."
elif [ "$IS_MERGE" -gt 0 ] && [ "$SRC_FILES" -lt 3 ]; then
CHANGE_TYPE="merge-commit"
SKIP_COVERAGE="informational"
echo "::notice::Merge commit with minimal code changes. Coverage check will be informational."
else
CHANGE_TYPE="source-code"
SKIP_COVERAGE="false"
echo "::notice::Source code changes detected ($SRC_FILES files). Full coverage check enabled."
fi
# Output variables
echo "src_files=${SRC_FILES}" >> $GITHUB_OUTPUT
echo "test_files=${TEST_FILES}" >> $GITHUB_OUTPUT
echo "config_files=${CONFIG_FILES}" >> $GITHUB_OUTPUT
echo "change_type=${CHANGE_TYPE}" >> $GITHUB_OUTPUT
echo "skip_coverage=${SKIP_COVERAGE}" >> $GITHUB_OUTPUT
# Log summary
echo "📊 Change Detection Summary:"
echo " Source files: ${SRC_FILES}"
echo " Test files: ${TEST_FILES}"
echo " Config files: ${CONFIG_FILES}"
echo " Workflow files: ${WORKFLOW_FILES}"
echo " Documentation: ${DOC_FILES}"
echo " Change type: ${CHANGE_TYPE}"
echo " Coverage enforcement: ${SKIP_COVERAGE}"
run-coverage:
name: Run Coverage Analysis
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.skip_coverage != 'true'
steps:
- name: Checkout PR
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Restore tools
run: dotnet tool restore
- name: Install ReportGenerator
run: dotnet tool install -g dotnet-reportgenerator-globaltool
- name: Install jq for JSON parsing
run: |
if ! command -v jq &> /dev/null; then
sudo apt-get update
sudo apt-get install -y jq
fi
jq --version
- name: Install diff-cover
run: pip install diff-cover
- name: Restore dependencies
run: dotnet restore ProbotSharp.sln
- name: Build in Debug for Coverage
run: dotnet build ProbotSharp.sln --configuration Debug --no-restore
# Run coverage for current PR (Debug mode for accurate instrumentation)
- name: Run tests with coverage (PR)
run: |
dotnet test ProbotSharp.sln \
--configuration Debug \
--no-build \
--collect:"XPlat Code Coverage" \
--settings coverlet.runsettings \
--results-directory ./coverage-pr
- name: Generate PR Coverage Report
run: |
reportgenerator \
"-reports:coverage-pr/**/coverage.cobertura.xml" \
"-targetdir:coverage-report-pr" \
"-reporttypes:Html;Cobertura;TextSummary;JsonSummary;Badges;MarkdownSummary" \
"-assemblyfilters:+ProbotSharp.*;-*.Tests;-*.Tests.*" \
"-classfilters:+*;-*Migrations*;-*DbContextModelSnapshot" \
"-title:ProbotSharp Coverage Report (PR)"
- name: Generate Merged Cobertura for Diff Analysis
run: |
reportgenerator \
"-reports:coverage-pr/**/coverage.cobertura.xml" \
"-targetdir:coverage-merged" \
"-reporttypes:Cobertura" \
"-assemblyfilters:+ProbotSharp.*;-*.Tests;-*.Tests.*" \
"-classfilters:+*;-*Migrations*;-*DbContextModelSnapshot"
# Checkout base branch for comparison
- name: Checkout base branch
run: |
git fetch origin ${{ github.base_ref }}
git checkout origin/${{ github.base_ref }}
- name: Restore base branch
run: dotnet restore ProbotSharp.sln
- name: Build base branch in Debug for Coverage
run: dotnet build ProbotSharp.sln --configuration Debug --no-restore
# Run coverage for base branch (Debug mode for accurate instrumentation)
- name: Run tests with coverage (Base)
run: |
dotnet test ProbotSharp.sln \
--configuration Debug \
--no-build \
--collect:"XPlat Code Coverage" \
--settings coverlet.runsettings \
--results-directory ./coverage-base
continue-on-error: true
- name: Generate Base Coverage Report
run: |
reportgenerator \
"-reports:coverage-base/**/coverage.cobertura.xml" \
"-targetdir:coverage-report-base" \
"-reporttypes:JsonSummary" \
"-assemblyfilters:+ProbotSharp.*;-*.Tests;-*.Tests.*" \
"-classfilters:+*;-*Migrations*;-*DbContextModelSnapshot"
continue-on-error: true
- name: Upload Coverage Artifacts
uses: actions/upload-artifact@v4
with:
name: coverage-reports-${{ github.event.pull_request.number }}
path: |
coverage-report-pr/
coverage-report-base/
coverage-merged/
coverage-pr/
retention-days: 30
- name: Upload Summary.json for debugging
if: always()
uses: actions/upload-artifact@v4
with:
name: summary-json-debug-${{ github.event.pull_request.number }}
path: |
coverage-report-pr/Summary.json
coverage-report-base/Summary.json
retention-days: 7
report-coverage:
name: Report Coverage Results
runs-on: ubuntu-latest
needs: [detect-changes, run-coverage]
if: always()
steps:
- name: Checkout PR
if: needs.detect-changes.outputs.skip_coverage != 'true'
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
if: needs.detect-changes.outputs.skip_coverage != 'true'
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install jq for JSON parsing
if: needs.detect-changes.outputs.skip_coverage != 'true'
run: |
if ! command -v jq &> /dev/null; then
sudo apt-get update
sudo apt-get install -y jq
fi
jq --version
- name: Install diff-cover
if: needs.detect-changes.outputs.skip_coverage != 'true'
run: pip install diff-cover
- name: Download Coverage Artifacts
if: needs.detect-changes.outputs.skip_coverage != 'true'
uses: actions/download-artifact@v4
with:
name: coverage-reports-${{ github.event.pull_request.number }}
- name: Analyze PR Diff Coverage
if: needs.detect-changes.outputs.skip_coverage != 'true'
id: diff_coverage
run: |
# Fetch base branch for diff-cover comparison
git fetch origin ${{ github.base_ref }}
# Run diff-cover against base branch to get coverage of changed lines only
diff-cover coverage-merged/Cobertura.xml \
--compare-branch=origin/${{ github.base_ref }} \
--json-report coverage-diff.json \
--html-report coverage-diff.html || true
# Extract diff coverage metrics (only changed lines)
if [ -f coverage-diff.json ]; then
DIFF_LINE=$(jq -r '.totals.line_coverage // 0' coverage-diff.json)
DIFF_BRANCH=$(jq -r '.totals.branch_coverage // 0' coverage-diff.json)
DIFF_LINES_CHANGED=$(jq -r '.totals.num_changed_lines // 0' coverage-diff.json)
DIFF_LINES_COVERED=$(jq -r '.totals.num_covered_lines // 0' coverage-diff.json)
else
echo "::warning::Failed to generate diff coverage, will skip diff check"
DIFF_LINE="N/A"
DIFF_BRANCH="N/A"
DIFF_LINES_CHANGED="0"
DIFF_LINES_COVERED="0"
fi
echo "diff_line=${DIFF_LINE}" >> $GITHUB_OUTPUT
echo "diff_branch=${DIFF_BRANCH}" >> $GITHUB_OUTPUT
echo "diff_lines_changed=${DIFF_LINES_CHANGED}" >> $GITHUB_OUTPUT
echo "diff_lines_covered=${DIFF_LINES_COVERED}" >> $GITHUB_OUTPUT
# Determine diff coverage status (this is what blocks for source-code changes)
MIN_DIFF_LINE=80
MIN_DIFF_BRANCH=75
if [ "$DIFF_LINE" = "N/A" ]; then
echo "diff_status=warning" >> $GITHUB_OUTPUT
echo "diff_status_msg=⚠️ Unable to calculate diff coverage" >> $GITHUB_OUTPUT
elif [ "$DIFF_LINES_CHANGED" = "0" ]; then
echo "diff_status=success" >> $GITHUB_OUTPUT
echo "diff_status_msg=ℹ️ No code changes detected" >> $GITHUB_OUTPUT
elif (( $(echo "$DIFF_LINE < $MIN_DIFF_LINE" | bc -l) )); then
echo "diff_status=failure" >> $GITHUB_OUTPUT
echo "diff_status_msg=❌ Changed lines coverage below ${MIN_DIFF_LINE}% threshold" >> $GITHUB_OUTPUT
elif (( $(echo "$DIFF_BRANCH < $MIN_DIFF_BRANCH" | bc -l) )); then
echo "diff_status=failure" >> $GITHUB_OUTPUT
echo "diff_status_msg=❌ Changed branches coverage below ${MIN_DIFF_BRANCH}% threshold" >> $GITHUB_OUTPUT
else
echo "diff_status=success" >> $GITHUB_OUTPUT
echo "diff_status_msg=✅ Changed code meets coverage requirements" >> $GITHUB_OUTPUT
fi
- name: Generate Coverage Comparison
id: coverage_comparison
run: |
CHANGE_TYPE="${{ needs.detect-changes.outputs.change_type }}"
SKIP_COVERAGE="${{ needs.detect-changes.outputs.skip_coverage }}"
SRC_FILES="${{ needs.detect-changes.outputs.src_files }}"
TEST_FILES="${{ needs.detect-changes.outputs.test_files }}"
CONFIG_FILES="${{ needs.detect-changes.outputs.config_files }}"
# Handle edge case: No source code changes
if [ "$SKIP_COVERAGE" = "true" ]; then
echo "pr_line=N/A" >> $GITHUB_OUTPUT
echo "pr_branch=N/A" >> $GITHUB_OUTPUT
echo "base_line=N/A" >> $GITHUB_OUTPUT
echo "base_branch=N/A" >> $GITHUB_OUTPUT
echo "line_delta=0%" >> $GITHUB_OUTPUT
echo "branch_delta=0%" >> $GITHUB_OUTPUT
echo "line_emoji=ℹ️" >> $GITHUB_OUTPUT
echo "branch_emoji=ℹ️" >> $GITHUB_OUTPUT
echo "project_status=skipped" >> $GITHUB_OUTPUT
echo "project_status_msg=✅ **No source code changes detected** - Coverage check skipped" >> $GITHUB_OUTPUT
echo "change_summary=**Change Summary:** ${CONFIG_FILES} config file(s) changed, no source code modifications" >> $GITHUB_OUTPUT
exit 0
fi
# Extract PR coverage using jq for reliable JSON parsing
if [ -f coverage-report-pr/Summary.json ]; then
echo "::debug::PR Summary.json structure:"
jq '.summary | {linecoverage, branchcoverage, methodcoverage}' coverage-report-pr/Summary.json || echo "::warning::Failed to parse PR JSON"
PR_LINE=$(jq -r '.summary.linecoverage // 0' coverage-report-pr/Summary.json 2>/dev/null)
PR_BRANCH=$(jq -r '.summary.branchcoverage // 0' coverage-report-pr/Summary.json 2>/dev/null)
PR_METHOD=$(jq -r '.summary.methodcoverage // 0' coverage-report-pr/Summary.json 2>/dev/null)
if [ -z "$PR_LINE" ] || [ "$PR_LINE" = "null" ]; then
echo "::error::Failed to extract PR line coverage from Summary.json"
PR_LINE="0"
fi
echo "::notice::PR Coverage - Line: ${PR_LINE}%, Branch: ${PR_BRANCH}%, Method: ${PR_METHOD}%"
else
echo "::error::PR coverage report not found at coverage-report-pr/Summary.json"
PR_LINE="0"
PR_BRANCH="0"
PR_METHOD="0"
fi
# Extract base coverage using jq
if [ -f coverage-report-base/Summary.json ]; then
echo "::debug::Base Summary.json structure:"
jq '.summary | {linecoverage, branchcoverage, methodcoverage}' coverage-report-base/Summary.json || echo "::warning::Failed to parse base JSON"
BASE_LINE=$(jq -r '.summary.linecoverage // 0' coverage-report-base/Summary.json 2>/dev/null)
BASE_BRANCH=$(jq -r '.summary.branchcoverage // 0' coverage-report-base/Summary.json 2>/dev/null)
BASE_METHOD=$(jq -r '.summary.methodcoverage // 0' coverage-report-base/Summary.json 2>/dev/null)
if [ -z "$BASE_LINE" ] || [ "$BASE_LINE" = "null" ]; then
echo "::error::Failed to extract base line coverage from Summary.json"
BASE_LINE="0"
fi
echo "::notice::Base Coverage - Line: ${BASE_LINE}%, Branch: ${BASE_BRANCH}%, Method: ${BASE_METHOD}%"
BASE_AVAILABLE="true"
else
echo "::warning::Base branch coverage report not found - using absolute thresholds only"
BASE_LINE="${PR_LINE}"
BASE_BRANCH="${PR_BRANCH}"
BASE_METHOD="${PR_METHOD}"
BASE_AVAILABLE="false"
fi
# Calculate deltas
LINE_DELTA=$(echo "$PR_LINE - $BASE_LINE" | bc)
BRANCH_DELTA=$(echo "$PR_BRANCH - $BASE_BRANCH" | bc)
METHOD_DELTA=$(echo "$PR_METHOD - $BASE_METHOD" | bc)
# Format deltas with sign
if (( $(echo "$LINE_DELTA > 0" | bc -l) )); then
LINE_DELTA_STR="+${LINE_DELTA}%"
LINE_EMOJI="📈"
elif (( $(echo "$LINE_DELTA < 0" | bc -l) )); then
LINE_DELTA_STR="${LINE_DELTA}%"
LINE_EMOJI="📉"
else
LINE_DELTA_STR="0%"
LINE_EMOJI="➡️"
fi
if (( $(echo "$BRANCH_DELTA > 0" | bc -l) )); then
BRANCH_DELTA_STR="+${BRANCH_DELTA}%"
BRANCH_EMOJI="📈"
elif (( $(echo "$BRANCH_DELTA < 0" | bc -l) )); then
BRANCH_DELTA_STR="${BRANCH_DELTA}%"
BRANCH_EMOJI="📉"
else
BRANCH_DELTA_STR="0%"
BRANCH_EMOJI="➡️"
fi
# Save values for next steps
echo "pr_line=${PR_LINE}" >> $GITHUB_OUTPUT
echo "pr_branch=${PR_BRANCH}" >> $GITHUB_OUTPUT
echo "base_line=${BASE_LINE}" >> $GITHUB_OUTPUT
echo "base_branch=${BASE_BRANCH}" >> $GITHUB_OUTPUT
echo "line_delta=${LINE_DELTA_STR}" >> $GITHUB_OUTPUT
echo "branch_delta=${BRANCH_DELTA_STR}" >> $GITHUB_OUTPUT
echo "line_emoji=${LINE_EMOJI}" >> $GITHUB_OUTPUT
echo "branch_emoji=${BRANCH_EMOJI}" >> $GITHUB_OUTPUT
# Define minimum thresholds for project-wide coverage
MIN_LINE_COVERAGE=75
MIN_BRANCH_COVERAGE=70
# Generate change summary
if [ "$CHANGE_TYPE" = "test-only" ]; then
CHANGE_SUMMARY="**Change Summary:** ${TEST_FILES} test file(s) modified, no production code changes"
elif [ "$CHANGE_TYPE" = "merge-commit" ]; then
CHANGE_SUMMARY="**Change Summary:** Merge commit with ${SRC_FILES} source file(s) and ${TEST_FILES} test file(s) modified"
else
CHANGE_SUMMARY="**Change Summary:** ${SRC_FILES} source file(s) and ${TEST_FILES} test file(s) modified"
fi
echo "change_summary=${CHANGE_SUMMARY}" >> $GITHUB_OUTPUT
# Informational project-wide status - does NOT block
if (( $(echo "$PR_LINE < $MIN_LINE_COVERAGE" | bc -l) )) || (( $(echo "$PR_BRANCH < $MIN_BRANCH_COVERAGE" | bc -l) )); then
echo "project_status=below_target" >> $GITHUB_OUTPUT
echo "project_status_msg=⚠️ Project-wide coverage below targets (${MIN_LINE_COVERAGE}% line, ${MIN_BRANCH_COVERAGE}% branch)" >> $GITHUB_OUTPUT
echo "project_emoji=⚠️" >> $GITHUB_OUTPUT
elif [ "$BASE_AVAILABLE" = "true" ] && (( $(echo "$LINE_DELTA < -5" | bc -l) )) || (( $(echo "$BRANCH_DELTA < -5" | bc -l) )); then
echo "project_status=significant_decrease" >> $GITHUB_OUTPUT
echo "project_status_msg=📉 Significant coverage decrease detected" >> $GITHUB_OUTPUT
echo "project_emoji=📉" >> $GITHUB_OUTPUT
elif [ "$BASE_AVAILABLE" = "true" ] && (( $(echo "$LINE_DELTA < 0" | bc -l) )) || (( $(echo "$BRANCH_DELTA < 0" | bc -l) )); then
echo "project_status=slight_decrease" >> $GITHUB_OUTPUT
echo "project_status_msg=➖ Coverage decreased slightly" >> $GITHUB_OUTPUT
echo "project_emoji=➖" >> $GITHUB_OUTPUT
elif [ "$BASE_AVAILABLE" = "true" ] && (( $(echo "$LINE_DELTA > 0" | bc -l) )) && (( $(echo "$BRANCH_DELTA > 0" | bc -l) )); then
echo "project_status=improved" >> $GITHUB_OUTPUT
echo "project_status_msg=📈 Coverage improved!" >> $GITHUB_OUTPUT
echo "project_emoji=📈" >> $GITHUB_OUTPUT
else
echo "project_status=stable" >> $GITHUB_OUTPUT
echo "project_status_msg=➡️ Coverage stable" >> $GITHUB_OUTPUT
echo "project_emoji=➡️" >> $GITHUB_OUTPUT
fi
- name: Upload Diff Coverage HTML Report
if: needs.detect-changes.outputs.skip_coverage != 'true' && always()
uses: actions/upload-artifact@v4
with:
name: coverage-diff-html-pr-${{ github.event.pull_request.number }}
path: coverage-diff.html
retention-days: 30
- name: Comment PR with Coverage (Config-Only)
if: needs.detect-changes.outputs.skip_coverage == 'true'
uses: thollander/actions-comment-pull-request@v2
with:
message: |
## 📊 Coverage Report
${{ steps.coverage_comparison.outputs.project_status_msg }}
${{ steps.coverage_comparison.outputs.change_summary }}
<details>
<summary>Edge Case Handling</summary>
- **Config-only changes**: Coverage check skipped automatically
- **Test-only changes**: Coverage check runs in informational mode (non-blocking)
- **Merge commits**: Relaxed enforcement for conflict resolution
- **Missing base coverage**: Falls back to absolute threshold checking
</details>
---
*Coverage report generated by [ProbotSharp CI](.github/workflows/coverage-report.yml)*
- name: Comment PR with Coverage (Test-Only or Merge)
if: needs.detect-changes.outputs.skip_coverage == 'informational'
uses: thollander/actions-comment-pull-request@v2
with:
message: |
## 📊 Coverage Report
### ℹ️ Informational Mode
${{ needs.detect-changes.outputs.change_type == 'test-only' && '**Test-only changes** - Coverage check is informational' || '**Merge commit** - Relaxed enforcement' }}
${{ steps.coverage_comparison.outputs.change_summary }}
### Project-Wide Coverage (informational only)
${{ steps.coverage_comparison.outputs.project_emoji }} ${{ steps.coverage_comparison.outputs.project_status_msg }}
| Metric | Base Branch | This PR | Delta | Target |
|--------|-------------|---------|-------|--------|
| **Line Coverage** | ${{ steps.coverage_comparison.outputs.base_line }}% | ${{ steps.coverage_comparison.outputs.pr_line }}% | ${{ steps.coverage_comparison.outputs.line_emoji }} ${{ steps.coverage_comparison.outputs.line_delta }} | 75% |
| **Branch Coverage** | ${{ steps.coverage_comparison.outputs.base_branch }}% | ${{ steps.coverage_comparison.outputs.pr_branch }}% | ${{ steps.coverage_comparison.outputs.branch_emoji }} ${{ steps.coverage_comparison.outputs.branch_delta }} | 70% |
<details>
<summary>Edge Case Handling</summary>
- **Config-only changes**: Coverage check skipped automatically
- **Test-only changes**: Coverage check runs in informational mode (non-blocking)
- **Merge commits**: Relaxed enforcement for conflict resolution
- **Missing base coverage**: Falls back to absolute threshold checking
</details>
---
*Coverage report generated by [ProbotSharp CI](.github/workflows/coverage-report.yml)*
- name: Comment PR with Coverage (Source Code)
if: needs.detect-changes.outputs.skip_coverage == 'false'
uses: thollander/actions-comment-pull-request@v2
with:
message: |
## 📊 Coverage Report
### ${{ steps.diff_coverage.outputs.diff_status_msg }}
**PR Diff Coverage** (Changed lines only - **blocks merge**):
| Metric | Changed Lines Coverage | Threshold | Status |
|--------|----------------------|-----------|--------|
| **Line Coverage** | **${{ steps.diff_coverage.outputs.diff_line }}%** | 80% | ${{ steps.diff_coverage.outputs.diff_line >= 80 && '✅ Pass' || steps.diff_coverage.outputs.diff_line == 'N/A' && 'ℹ️ N/A' || '❌ Fail' }} |
| **Branch Coverage** | **${{ steps.diff_coverage.outputs.diff_branch }}%** | 75% | ${{ steps.diff_coverage.outputs.diff_branch >= 75 && '✅ Pass' || steps.diff_coverage.outputs.diff_branch == 'N/A' && 'ℹ️ N/A' || '❌ Fail' }} |
<sub>📝 Changed: ${{ steps.diff_coverage.outputs.diff_lines_covered }}/${{ steps.diff_coverage.outputs.diff_lines_changed }} lines covered</sub>
---
### Project-Wide Coverage (informational only)
${{ steps.coverage_comparison.outputs.project_emoji }} ${{ steps.coverage_comparison.outputs.project_status_msg }}
${{ steps.coverage_comparison.outputs.change_summary }}
| Metric | Base Branch | This PR | Delta | Target |
|--------|-------------|---------|-------|--------|
| **Line Coverage** | ${{ steps.coverage_comparison.outputs.base_line }}% | ${{ steps.coverage_comparison.outputs.pr_line }}% | ${{ steps.coverage_comparison.outputs.line_emoji }} ${{ steps.coverage_comparison.outputs.line_delta }} | 75% |
| **Branch Coverage** | ${{ steps.coverage_comparison.outputs.base_branch }}% | ${{ steps.coverage_comparison.outputs.pr_branch }}% | ${{ steps.coverage_comparison.outputs.branch_emoji }} ${{ steps.coverage_comparison.outputs.branch_delta }} | 70% |
<details>
<summary>📋 Coverage Targets by Layer</summary>
- **Domain Layer**: 90% line, 85% branch
- **Application Layer**: 80% line, 75% branch
- **Infrastructure Layer**: 70% line, 65% branch
- **Overall Target**: 75% line, 70% branch
ℹ️ These are aspirational targets. Only **PR Diff Coverage** blocks merges.
</details>
<details>
<summary>Edge Case Handling</summary>
- **Config-only changes**: Coverage check skipped automatically
- **Test-only changes**: Coverage check runs in informational mode (non-blocking)
- **Merge commits**: Relaxed enforcement for conflict resolution
- **Missing base coverage**: Falls back to absolute threshold checking
</details>
---
*Coverage analysis by [diff-cover](https://github.com/Bachmann1234/diff_cover) | [Full report](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})*
- name: Set Coverage Status Check
if: always()
run: |
SKIP_COVERAGE="${{ needs.detect-changes.outputs.skip_coverage }}"
CHANGE_TYPE="${{ needs.detect-changes.outputs.change_type }}"
DIFF_STATUS="${{ steps.diff_coverage.outputs.diff_status }}"
DIFF_LINE="${{ steps.diff_coverage.outputs.diff_line }}"
DIFF_BRANCH="${{ steps.diff_coverage.outputs.diff_branch }}"
PROJECT_STATUS="${{ steps.coverage_comparison.outputs.project_status }}"
PR_LINE="${{ steps.coverage_comparison.outputs.pr_line }}"
PR_BRANCH="${{ steps.coverage_comparison.outputs.pr_branch }}"
echo "=== Coverage Gate Results ==="
# Handle edge cases first
if [ "$SKIP_COVERAGE" = "true" ]; then
echo "Coverage check skipped - no source code changes detected"
echo "::notice::Coverage check skipped - no source code changes detected"
exit 0
fi
if [ "$SKIP_COVERAGE" = "informational" ]; then
if [ "$CHANGE_TYPE" = "test-only" ]; then
echo "Coverage check informational only - test-only changes"
echo "::notice::Coverage check informational only - test-only changes. Line: ${PR_LINE}%, Branch: ${PR_BRANCH}%"
elif [ "$CHANGE_TYPE" = "merge-commit" ]; then
echo "Coverage check informational - merge commit with minimal changes"
echo "::notice::Coverage check informational - merge commit with minimal changes. Line: ${PR_LINE}%, Branch: ${PR_BRANCH}%"
fi
exit 0
fi
# Normal enforcement for source code changes - use diff coverage as gate
echo "PR Diff Coverage (BLOCKING): ${DIFF_STATUS}"
echo " - Line: ${DIFF_LINE}% (min: 80%)"
echo " - Branch: ${DIFF_BRANCH}% (min: 75%)"
echo ""
echo "Project-Wide Status (INFORMATIONAL): ${PROJECT_STATUS}"
echo " - Line: ${PR_LINE}%"
echo " - Branch: ${PR_BRANCH}%"
echo ""
# Only block based on diff coverage
if [ "$DIFF_STATUS" = "failure" ]; then
echo "::error::PR diff coverage check failed. Changed lines coverage: Line ${DIFF_LINE}%, Branch ${DIFF_BRANCH}%"
echo "::error::Your changed code must have at least 80% line coverage and 75% branch coverage."
exit 1
elif [ "$DIFF_STATUS" = "warning" ]; then
echo "::warning::Unable to calculate diff coverage - manual review recommended"
else
echo "::notice::PR diff coverage check passed! Line: ${DIFF_LINE}%, Branch: ${DIFF_BRANCH}%"
fi
# Show project-wide status as informational notice
case "$PROJECT_STATUS" in
below_target)
echo "::notice::Project-wide coverage is below targets but this does not block the PR"
;;
significant_decrease)
echo "::warning::Project-wide coverage decreased significantly - consider adding more tests"
;;
improved)
echo "::notice::Great job! Project-wide coverage improved 🎉"
;;
esac