-
-
Notifications
You must be signed in to change notification settings - Fork 1
190 lines (176 loc) · 7.75 KB
/
Copy pathcoverage.yml
File metadata and controls
190 lines (176 loc) · 7.75 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
name: Coverage
# Code-coverage reporting and gate (issue #29). Collects line/branch coverage for
# all seven shipping Celerity packages via coverlet, renders an HTML report + badges
# with scripts/coverage_report.py (there is no ReportGenerator dependency), fails
# the build if coverage drops below the floor, comments the summary on PRs, and
# publishes the HTML report to gh-pages (/coverage) on main.
on:
push:
branches: [ main ]
paths:
- 'src/**'
- '.github/workflows/coverage.yml'
pull_request:
branches: [ main ]
paths:
- 'src/**'
- '.github/workflows/coverage.yml'
# Coverage floor. The suite is at 100% line and 100% branch across all six
# shipping packages, so the floor is set to match: every reachable line and branch
# is covered, and the handful of guards no test can reach (array-size ceilings,
# clamps their caller's own validation already rules out) carry
# [ExcludeFromCodeCoverage] with a Justification saying why.
#
# A 100 floor is deliberately a hair-trigger: new code arrives with its tests, or
# the gate goes red. If a genuinely unreachable branch turns up, exclude it at the
# source with a justification rather than lowering these numbers.
env:
MIN_LINE_COVERAGE: '100'
MIN_BRANCH_COVERAGE: '100'
jobs:
coverage:
name: coverage
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
filter: tree:0
# Both SDKs: the core suite is collected on net8.0 (the floor TFM), while the
# three showcase test projects are single-target net10.0.
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x
# Coverage for the core packages is collected on net8.0 only. The library
# source is identical across the multi-targeted TFMs (#189) — no #if-gated
# code paths today — so one TFM fully covers the line/branch surface and
# keeps the report deterministic. If a #if-gated path is ever introduced,
# this step has to fan out over the TFMs, or the 100% floor will start
# failing on the gated arm.
- name: Collect coverage (core packages)
working-directory: src
run: >
dotnet test Celerity.Tests/Celerity.Tests.csproj
--framework net8.0
--configuration Release
--collect:"XPlat Code Coverage"
--settings coverage.runsettings
--results-directory ./TestResults/coverage
# The showcase packages ship too, so they are inside the gate (#314). Their
# test projects are separate, hence a separate results directory that the
# report step merges with the core one.
- name: Collect coverage (showcase packages)
working-directory: src
run: |
set -euo pipefail
for project in Ring Sentinel Cardinality; do
dotnet test "Celerity.${project}.Tests/Celerity.${project}.Tests.csproj" \
--configuration Release \
--collect:"XPlat Code Coverage" \
--settings coverage.runsettings \
--results-directory "./TestResults/showcase/${project}"
done
# Renders the HTML report, badge, and PR summary, writes the run summary,
# and fails the job if coverage is below the floor — all in one script, so
# the report carries the project's own styling and no third-party upsell.
# The two --input globs are merged on (source file, line); the showcase
# projects also pull in Celerity.Collections, and a line covered by any run
# counts as covered.
- name: Generate report and enforce floor
run: >
python3 scripts/coverage_report.py
--input "src/TestResults/coverage/**/coverage.cobertura.xml"
--input "src/TestResults/showcase/*/**/coverage.cobertura.xml"
--outdir coveragereport
--min-line "$MIN_LINE_COVERAGE"
--min-branch "$MIN_BRANCH_COVERAGE"
- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coveragereport
if-no-files-found: warn
retention-days: 14
- name: Comment coverage on PR
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const marker = '<!-- celerity-coverage-comment -->';
const summary = fs.readFileSync('coveragereport/summary.md', 'utf8');
const body = `${marker}\n${summary}`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
- name: Publish report to gh-pages
# Same gate as the benchmark dashboard sync: main pushes only. Drops the
# HTML report under /coverage, leaving the benchmark data untouched.
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
set -euo pipefail
git fetch origin gh-pages
git worktree add /tmp/gh-pages gh-pages
cd /tmp/gh-pages
# The benchmark workflow also pushes to gh-pages on a main push, so refresh
# and retry to tolerate a concurrent update. Each workflow owns a different
# subtree (coverage/ here, dev/ there), so re-applying on top of the latest
# gh-pages never clobbers the other's data.
published=0
for attempt in 1 2 3 4 5; do
git fetch origin gh-pages
git reset --hard origin/gh-pages
rm -rf coverage
mkdir -p coverage
cp -r "$GITHUB_WORKSPACE"/coveragereport/* coverage/
# Stage first, then diff the index against HEAD. The previous guard used
# `git diff --quiet`, which ignores *untracked* files — so on the very first
# run (no coverage/ committed yet) it always reported "no changes" and the
# report was never published. Staging first detects new files too, which
# both bootstraps the initial publish and stays a no-op on an unchanged rerun.
git -c user.name="github-actions" -c user.email="github-actions@github.com" add coverage
if git diff --cached --quiet; then
echo "No coverage changes to sync"
published=1
break
fi
git -c user.name="github-actions" -c user.email="github-actions@github.com" \
commit -m "Sync coverage report from ${GITHUB_SHA:0:7}"
if git push origin gh-pages; then
echo "Coverage report published."
published=1
break
fi
echo "Push rejected (attempt ${attempt}); refreshing gh-pages and retrying..."
sleep $((attempt * 3))
done
if [ "$published" -ne 1 ]; then
echo "Failed to publish coverage report after retries." >&2
exit 1
fi