Skip to content
Merged
150 changes: 87 additions & 63 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ on:
pull_request:
branches:
- main
release:
types: [published]

permissions:
contents: read
pull-requests: write

jobs:
build:
runs-on: ubuntu-latest

# Debug keeps per-statement sequence points so coverage lines up 1:1
# with source. Release merges points and folds constant branches,
# which skews the numbers; shipping validation happens in release.yml.
env:
config: 'Release'
config: 'Debug'
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true

Expand All @@ -37,8 +39,89 @@ jobs:
- name: Build
run: dotnet build --configuration $config --no-restore

# `-- --coverage ...` forwards flags to Microsoft.Testing.Platform's code
# coverage extension; each test project emits a Cobertura XML under its
# own TestResults/ folder, which ReportGenerator merges in the next step.
- name: Test
run: dotnet test --no-restore --no-build --configuration $config
run: >-
dotnet test --no-restore --no-build --configuration $config
--
--coverage
--coverage-output-format cobertura

- name: Generate coverage report
if: ${{ !cancelled() }}
run: |
shopt -s globstar nullglob
files=(**/TestResults/**/*.cobertura.xml)
if [ ${#files[@]} -eq 0 ]; then
echo "No coverage files found; skipping report generation."
exit 0
fi
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator \
"-reports:**/TestResults/**/*.cobertura.xml" \
"-targetdir:coverage-report" \
"-reporttypes:HtmlInline;Cobertura" \
"-title:StrongTypes"

# List the files this PR touches so the summary can highlight their coverage
# instead of drowning in a `below 50%` list dominated by unmigrated legacy code.
- name: List changed files
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' > changed-files.txt

# ReportGenerator's markdown outputs group by class, which surfaces every
# compiler-generated closure type as its own row (e.g. `MaybeExtensions<A, B>`,
# `MaybeExtensions<A, R>`, …). Our own summary groups by source file instead.
- name: Render file-grouped coverage summary
if: ${{ !cancelled() && hashFiles('coverage-report/Cobertura.xml') != '' }}
run: |
args=(coverage-report/Cobertura.xml coverage-report/FileSummary.md)
if [ -f changed-files.txt ]; then
args+=(--changed-files changed-files.txt)
fi
python3 scripts/coverage-summary.py "${args[@]}"

- name: Append coverage summary to job summary
if: ${{ !cancelled() }}
run: |
if [ -f coverage-report/FileSummary.md ]; then
cat coverage-report/FileSummary.md >> "$GITHUB_STEP_SUMMARY"
else
echo "No coverage summary to append."
fi

# Sticky comment: subsequent runs on the same PR edit the existing comment
# rather than piling up new ones, so authors get a single up-to-date view
# of coverage without inbox noise.
- name: Post coverage summary as PR comment
if: ${{ !cancelled() && github.event_name == 'pull_request' && hashFiles('coverage-report/FileSummary.md') != '' }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: coverage
path: coverage-report/FileSummary.md

# Evaluation mode alongside the custom sticky comment above. The repo
# must be enabled at https://app.codecov.io/ with CODECOV_TOKEN stored
# as a repo secret.
- name: Upload coverage to Codecov
if: ${{ !cancelled() && hashFiles('coverage-report/Cobertura.xml') != '' }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage-report/Cobertura.xml
fail_ci_if_error: false

- name: Upload coverage report
if: ${{ !cancelled() && hashFiles('coverage-report/**') != '' }}
uses: actions/upload-artifact@v7
with:
name: coverage-report
path: coverage-report
retention-days: 14

- name: Upload test logs
if: always()
Expand All @@ -49,62 +132,3 @@ jobs:
**/TestResults/**/*.log
**/TestResults/**/*.trx
retention-days: 5

publish:
needs: build
# Gate on main: a Release drafted through the UI carries the selected
# branch in target_commitish. Only releases targeted at main publish.
if: github.event_name == 'release' && github.event.release.target_commitish == 'main'
runs-on: ubuntu-latest
environment: Nuget.org
permissions:
contents: write

env:
config: 'Release'
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

- name: Parse version from tag
id: version
run: |
TAG="${GITHUB_REF_NAME}"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"

# GeneratePackageOnBuild=true in the csproj files means `dotnet build`
# produces the nupkg as a side-effect; no separate `dotnet pack` step
# needed. -p:Version cascades into referenced projects (EfCore's
# ProjectReference to core), so EfCore's nuspec pins its core
# dependency to the same version.
# Build the whole solution; non-publishable projects (tests, internal
# source generators, analyzers) set IsPackable=false so they produce no
# nupkg. GeneratePackageOnBuild=true on publishable csprojs means the
# build itself emits .nupkg files into PackageOutputPath. New publishable
# packages are picked up automatically by setting the same two properties.
- name: Build and pack
env:
RELEASE_URL: ${{ github.event.release.html_url }}
VERSION: ${{ steps.version.outputs.version }}
run: |
dotnet build StrongTypes.slnx \
-c $config \
-p:Version="$VERSION" \
-p:PackageReleaseNotes="See $RELEASE_URL" \
-p:PackageOutputPath="$PWD/out"

- name: Publish packages
run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }}

- name: Attach packages to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "$GITHUB_REF_NAME" ./out/*.nupkg
84 changes: 84 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: release

on:
release:
types: [published]

permissions:
contents: read

jobs:
publish:
# Gate on main: a Release drafted through the UI carries the selected
# branch in target_commitish. Only releases targeted at main publish.
if: github.event.release.target_commitish == 'main'
runs-on: ubuntu-latest
environment: Nuget.org
permissions:
contents: write

env:
config: 'Release'
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

- name: Parse version from tag
id: version
run: |
TAG="${GITHUB_REF_NAME}"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"

# GeneratePackageOnBuild=true in the csproj files means `dotnet build`
# produces the nupkg as a side-effect; no separate `dotnet pack` step
# needed. -p:Version cascades into referenced projects (EfCore's
# ProjectReference to core), so EfCore's nuspec pins its core
# dependency to the same version.
# Build the whole solution; non-publishable projects (tests, internal
# source generators, analyzers) set IsPackable=false so they produce no
# nupkg. GeneratePackageOnBuild=true on publishable csprojs means the
# build itself emits .nupkg files into PackageOutputPath. New publishable
# packages are picked up automatically by setting the same two properties.
- name: Build and pack
env:
RELEASE_URL: ${{ github.event.release.html_url }}
VERSION: ${{ steps.version.outputs.version }}
run: |
dotnet build StrongTypes.slnx \
-c $config \
-p:Version="$VERSION" \
-p:PackageReleaseNotes="See $RELEASE_URL" \
-p:PackageOutputPath="$PWD/out"

# Validate the Release binaries we're about to publish. PRs already
# ran the full suite in Debug; this step catches config-specific
# regressions (compiler optimizations, trimming, etc.) before nupkgs
# leave the runner.
- name: Test
run: dotnet test --no-restore --no-build --configuration $config

- name: Upload test logs
if: always()
uses: actions/upload-artifact@v7
with:
name: test-logs-release
path: |
**/TestResults/**/*.log
**/TestResults/**/*.trx
retention-days: 5

- name: Publish packages
run: dotnet nuget push ./out/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }}

- name: Attach packages to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "$GITHUB_REF_NAME" ./out/*.nupkg
32 changes: 32 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
component_management:
individual_components:
- component_id: strongtypes
name: StrongTypes
paths: ["src/StrongTypes/**"]
- component_id: analyzers
name: StrongTypes.Analyzers
paths: ["src/StrongTypes.Analyzers/**"]
- component_id: efcore
name: StrongTypes.EfCore
paths: ["src/StrongTypes.EfCore/**"]
- component_id: api
name: StrongTypes.Api
paths: ["src/StrongTypes.Api/**"]
- component_id: fscheck
name: StrongTypes.FsCheck
paths: ["src/StrongTypes.FsCheck/**"]

comment:
layout: "header, diff, components, tree"
require_changes: false

coverage:
status:
# `informational: true` posts a status but never fails the PR. Keeps
# codecov in evaluation mode alongside our custom sticky comment.
project:
default:
informational: true
patch:
default:
informational: true
Loading
Loading