feat(collections): add SegmentTree — range aggregates over a non-invertible fold #291
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |