Skip to content

Merge pull request #343 from marius-bughiu/feat/issue-309-celerity-so… #285

Merge pull request #343 from marius-bughiu/feat/issue-309-celerity-so…

Merge pull request #343 from marius-bughiu/feat/issue-309-celerity-so… #285

Workflow file for this run

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