Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Isolate NuGet cache
run: echo "NUGET_PACKAGES=$RUNNER_TEMP/ci-nuget" >> "$GITHUB_ENV"

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
Expand All @@ -35,7 +38,9 @@ jobs:
run: dotnet tool restore

- name: Restore dependencies
run: dotnet restore RestClient.sln
run: |
python3 scripts/release.py pack-analyzer --output .artifacts/packages
dotnet restore RestClient.sln --source "$PWD/.artifacts/packages" --source https://api.nuget.org/v3/index.json

- name: Check code formatting with CSharpier
run: dotnet csharpier --check .
Expand All @@ -61,6 +66,24 @@ jobs:
- name: Run F# tests outside the solution
run: dotnet test RestClient.Net.FsTest/RestClient.Net.FsTest.fsproj --configuration Release --verbosity normal

- name: Test release ordering and artifact verification
run: python3 -m unittest discover -s scripts/tests -p test_release_orchestration.py -v

- name: Build and test release packages before publication
id: release_versions
run: |
python3 scripts/release.py versions >> "$GITHUB_OUTPUT"
python3 scripts/release.py pack --output .artifacts/release

- name: Verify fixed analyzer through package consumers
env:
CLIENT_VERSION: ${{ steps.release_versions.outputs.client_version }}
ANALYZER_VERSION: ${{ steps.release_versions.outputs.analyzer_version }}
run: |
for framework in net8.0 net9.0; do
python3 scripts/tests/test_release_packages.py --packages .artifacts/release --restclient-version "$CLIENT_VERSION" --exhaustion-version "$ANALYZER_VERSION" --framework "$framework"
done

- name: Cleanup Docker containers
if: always()
run: |
Expand Down
76 changes: 51 additions & 25 deletions .github/workflows/publish-exhaustion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,76 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version number (e.g., 1.0.0)'
description: 'Exhaustion version (e.g., 1.0.1)'
required: true
type: string

permissions:
contents: read

concurrency:
group: nuget-release-${{ github.repository }}
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
DOTNET_PROCESSOR_COUNT: '2'

steps:
- name: Checkout code
- name: Checkout release source
uses: actions/checkout@v4

- name: Isolate NuGet cache
run: echo "NUGET_PACKAGES=$RUNNER_TEMP/release-nuget" >> "$GITHUB_ENV"

- name: Resolve and validate release versions
id: versions
env:
REQUESTED_VERSION: ${{ inputs.version || github.ref_name }}
run: python3 scripts/release.py versions --analyzer-version "$REQUESTED_VERSION" >> "$GITHUB_OUTPUT"

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Restore dependencies
run: dotnet restore Exhaustion/Exhaustion.csproj
dotnet-version: |
8.0.x
9.0.x

- name: Build
run: dotnet build Exhaustion/Exhaustion.csproj --configuration Release --no-restore

- name: Set version from tag
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
run: |
VERSION=${GITHUB_REF#refs/tags/exhaustion-v}
echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV
- name: Test release ordering and artifact verification
run: python3 -m unittest discover -s scripts/tests -p test_release_orchestration.py -v

- name: Set version from input
if: github.event_name == 'workflow_dispatch'
run: |
echo "PACKAGE_VERSION=${{ vars.PACKAGE_VERSION }}" >> $GITHUB_ENV
- name: Build analyzer and local consumer package
env:
CLIENT_VERSION: ${{ steps.versions.outputs.client_version }}
ANALYZER_VERSION: ${{ steps.versions.outputs.analyzer_version }}
run: python3 scripts/release.py pack --client-version "$CLIENT_VERSION" --analyzer-version "$ANALYZER_VERSION" --output .artifacts/release

- name: Pack
run: dotnet pack Exhaustion/Exhaustion.csproj --configuration Release --no-build --output ./packages /p:Version=${{ env.PACKAGE_VERSION }}
- name: Run analyzer regressions
env:
ANALYZER_VERSION: ${{ steps.versions.outputs.analyzer_version }}
run: dotnet test Exhaustion.Tests/Exhaustion.Tests.csproj --configuration Release --verbosity normal -p:ExhaustionVersion="$ANALYZER_VERSION"

- name: Publish to NuGet
run: dotnet nuget push ./packages/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
- name: Verify analyzer through the consumer package
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
CLIENT_VERSION: ${{ steps.versions.outputs.client_version }}
ANALYZER_VERSION: ${{ steps.versions.outputs.analyzer_version }}
run: |
for framework in net8.0 net9.0; do
python3 scripts/tests/test_release_packages.py --packages .artifacts/release --restclient-version "$CLIENT_VERSION" --exhaustion-version "$ANALYZER_VERSION" --framework "$framework"
done

- name: Upload package artifact
- name: Upload verified analyzer package
uses: actions/upload-artifact@v4
with:
name: exhaustion-nuget-package
path: ./packages/*.nupkg
path: .artifacts/release/Exhaustion.*.nupkg
if-no-files-found: error

- name: Publish and verify analyzer
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
ANALYZER_VERSION: ${{ steps.versions.outputs.analyzer_version }}
run: python3 scripts/release.py publish-analyzer --analyzer-version "$ANALYZER_VERSION" --output .artifacts/release
86 changes: 61 additions & 25 deletions .github/workflows/publish-restclient.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Publish RestClient.Net to NuGet
name: Release RestClient.Net and Exhaustion

on:
push:
Expand All @@ -7,50 +7,86 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version number (e.g., 6.0.0)'
description: 'RestClient.Net version (e.g., 7.3.1); ExhaustionVersion comes from Directory.Build.props'
required: true
type: string

permissions:
contents: read

concurrency:
group: nuget-release-${{ github.repository }}
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
DOTNET_PROCESSOR_COUNT: '2'

steps:
- name: Checkout code
- name: Checkout release source
uses: actions/checkout@v4

- name: Isolate NuGet cache
run: echo "NUGET_PACKAGES=$RUNNER_TEMP/release-nuget" >> "$GITHUB_ENV"

- name: Resolve and validate release versions
id: versions
env:
REQUESTED_VERSION: ${{ inputs.version || github.ref_name }}
run: python3 scripts/release.py versions --client-version "$REQUESTED_VERSION" >> "$GITHUB_OUTPUT"

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: |
8.0.x
9.0.x

- name: Restore dependencies
run: dotnet restore RestClient.Net/RestClient.Net.csproj
- name: Test release ordering and artifact verification
run: python3 -m unittest discover -s scripts/tests -p test_release_orchestration.py -v

- name: Build
run: dotnet build RestClient.Net/RestClient.Net.csproj --configuration Release --no-restore
- name: Build both release packages from this commit
env:
CLIENT_VERSION: ${{ steps.versions.outputs.client_version }}
ANALYZER_VERSION: ${{ steps.versions.outputs.analyzer_version }}
run: python3 scripts/release.py pack --client-version "$CLIENT_VERSION" --analyzer-version "$ANALYZER_VERSION" --output .artifacts/release

- name: Set version from tag
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
run: |
VERSION=${GITHUB_REF#refs/tags/restclient-v}
echo "REST_CLIENT_DOTNET_VERSION=$VERSION" >> $GITHUB_ENV
- name: Run analyzer regressions
run: dotnet test Exhaustion.Tests/Exhaustion.Tests.csproj --configuration Release --verbosity normal

- name: Set version from input
if: github.event_name == 'workflow_dispatch'
- name: Verify actual packages in isolated consumers
env:
CLIENT_VERSION: ${{ steps.versions.outputs.client_version }}
ANALYZER_VERSION: ${{ steps.versions.outputs.analyzer_version }}
run: |
echo "REST_CLIENT_DOTNET_VERSION=${{ vars.REST_CLIENT_DOTNET_VERSION }}" >> $GITHUB_ENV
for framework in net8.0 net9.0; do
python3 scripts/tests/test_release_packages.py --packages .artifacts/release --restclient-version "$CLIENT_VERSION" --exhaustion-version "$ANALYZER_VERSION" --framework "$framework"
done

- name: Pack
run: dotnet pack RestClient.Net/RestClient.Net.csproj --configuration Release --no-build --output ./packages /p:Version=${{ env.REST_CLIENT_DOTNET_VERSION }}
- name: Upload verified release packages
uses: actions/upload-artifact@v4
with:
name: restclient-and-exhaustion-packages
path: |
.artifacts/release/*.nupkg
.artifacts/release/*.snupkg
if-no-files-found: error

- name: Publish to NuGet
run: dotnet nuget push ./packages/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
- name: Publish and verify Exhaustion before RestClient.Net
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
CLIENT_VERSION: ${{ steps.versions.outputs.client_version }}
ANALYZER_VERSION: ${{ steps.versions.outputs.analyzer_version }}
run: python3 scripts/release.py publish --client-version "$CLIENT_VERSION" --analyzer-version "$ANALYZER_VERSION" --output .artifacts/release

- name: Upload package artifact
uses: actions/upload-artifact@v4
with:
name: restclient-nuget-package
path: ./packages/*.nupkg
- name: Verify published packages from NuGet in fresh consumers
env:
CLIENT_VERSION: ${{ steps.versions.outputs.client_version }}
ANALYZER_VERSION: ${{ steps.versions.outputs.analyzer_version }}
run: |
for framework in net8.0 net9.0; do
python3 scripts/tests/test_release_packages.py --public-nuget --restclient-version "$CLIENT_VERSION" --exhaustion-version "$ANALYZER_VERSION" --framework "$framework"
done
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ coverage.cobertura.xml
final-check/
test-output-final
nupkgs/
.artifacts/
__pycache__/

# Website
Website/node_modules/
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The existing dotnet commands are the local equivalents of the Makefile commands

| Purpose | Command |
| --- | --- |
| Setup | `dotnet tool restore` and `dotnet restore RestClient.sln` |
| Setup | `dotnet tool restore`, `python3 scripts/release.py pack-analyzer`, then `dotnet restore RestClient.sln --source .artifacts/packages --source https://api.nuget.org/v3/index.json` |
| Build | `dotnet build RestClient.sln --configuration Release --no-restore /warnaserror` |
| Analysis | `dotnet build RestClient.sln --configuration Release --no-restore /p:RunAnalyzers=true /p:TreatWarningsAsErrors=true` |
| Format check | `dotnet csharpier --check .` |
Expand All @@ -31,6 +31,8 @@ Use a feature branch and a PR to `main`; derive the PR title and description fro

Dependabot updates accumulate on `dependabot-upgrades`; ordinary CI and CodeQL run on the consolidation PR to `main`, not on each bot bump. Never publish packages or create release tags as part of an ordinary PR.

When a release is authorized, use `.github/workflows/publish-restclient.yml`. It builds and tests both packages, publishes and verifies `Exhaustion` first, then publishes `RestClient.Net` and verifies a fresh NuGet consumer. `ExhaustionVersion` in `Directory.Build.props` is shared by the analyzer and all references. See `scripts/README.md` for package verification and immutable-version rules.

## Integration-test isolation

The Nuclia integration fixture starts Docker Compose and removes its project's containers and volumes. If the host already has services, use an isolated `COMPOSE_PROJECT_NAME` and Compose configuration with non-conflicting ports before running it. Keep the heap-limited child processes in the Exhaustion regression tests intact.
5 changes: 3 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<NoWarn>$(NoWarn);CA2234;IDE0290;SA1513;SA1010;SA1400;SA1122;SA1124;SA1516;SA1114;SA1005;SA1124</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>latest</LangVersion>
<ExhaustionVersion>1.0.1</ExhaustionVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
Expand All @@ -21,9 +22,9 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Exhaustion" Version="1.0.0" Condition="'$(MSBuildProjectName)' != 'Exhaustion' AND '$(MSBuildProjectName)' != 'Exhaustion.Tests' AND '$(MSBuildProjectName)' != 'RestClient.Net' AND '$(MSBuildProjectName)' != 'Buh'">
<PackageReference Include="Exhaustion" Version="$(ExhaustionVersion)" Condition="'$(MSBuildProjectName)' != 'Exhaustion' AND '$(MSBuildProjectName)' != 'Exhaustion.Tests' AND '$(MSBuildProjectName)' != 'RestClient.Net' AND '$(MSBuildProjectName)' != 'Buh'">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
</Project>
3 changes: 2 additions & 1 deletion Exhaustion/Exhaustion.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

<!-- NuGet Package Metadata -->
<PackageId>Exhaustion</PackageId>
<Version>1.0.0</Version>
<Version>$(ExhaustionVersion)</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Authors>RestClient.Net Contributors</Authors>
<Company>RestClient.Net</Company>
<Description>Roslyn analyzer that enforces exhaustive pattern matching for closed type hierarchies in C#</Description>
Expand Down
8 changes: 4 additions & 4 deletions RestClient.Net/RestClient.Net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<TargetFrameworks>net8.0;net9.0;netstandard2.1</TargetFrameworks>
<IsPackable>true</IsPackable>
<PackageId>RestClient.Net</PackageId>
<Version>7.1.2</Version>
<Version>7.3.1</Version>
<Authors>Christian Findlay</Authors>
<Company>Christian Findlay</Company>
<Description>The safest way to make REST calls in C#. Functional HTTP client library with Result types, exhaustiveness checking, and zero exceptions.</Description>
Expand All @@ -22,13 +22,13 @@
<PrivateAssets></PrivateAssets>
<IncludeAssets>all</IncludeAssets>
</PackageReference>
<PackageReference Include="Exhaustion" Version="1.0.0">
<PrivateAssets></PrivateAssets>
<PackageReference Include="Exhaustion" Version="$(ExhaustionVersion)">
<PrivateAssets>none</PrivateAssets>
<IncludeAssets>all</IncludeAssets>
</PackageReference>
<PackageReference Include="Outcome" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>
</Project>
43 changes: 43 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Verified package releases

The `Release RestClient.Net and Exhaustion` GitHub Action accepts a RestClient.Net
version or a `restclient-v*` tag. It publishes both packages from the checked-out
commit. It uses the requested version directly, never a repository version variable.

`ExhaustionVersion` in `Directory.Build.props` controls the analyzer package version
and the dependency in every RestClient.Net target framework. Increment it when
changing the analyzer. RestClient.Net's project version records the next client
release; a manual release input overrides it consistently during restore, build,
and pack.

Before publishing, the action runs analyzer regressions and installs the packed
RestClient.Net into separate consumers with fresh NuGet caches. These consumers
reference only RestClient.Net: incomplete switches must report `EXHAUSTION001`,
complete switches must compile, and the issue #146 hierarchy must finish with the
bounded `EXHAUSTION002` diagnostic. Consumer processes have time and heap limits.

The action publishes Exhaustion first and waits until its exact analyzer DLL is
downloadable and its version is indexed. Only then can it publish RestClient.Net.
It verifies both published packages and repeats the consumer checks using only
nuget.org. The standalone Exhaustion action uses the same packaging and verification
helpers. Both publishers share a concurrency group.

NuGet versions are immutable. A retry can reuse an existing version only if its
DLLs, package identity, and dependencies match this build. A mismatch fails the
release; increment the affected package version instead of accepting an older DLL.
NuGet's signing changes are excluded from the binary comparison.

## Local verification

```sh
python3 scripts/release.py pack --output .artifacts/release
python3 -m unittest discover -s scripts/tests -p test_release_orchestration.py -v
python3 scripts/tests/test_release_packages.py --packages .artifacts/release \
--restclient-version 7.3.1 --exhaustion-version 1.0.1
```

Packaging uses a new restore cache and separate build output so an existing local
package cannot hide a stale analyzer. CI bootstraps the unpublished analyzer into
`.artifacts/packages` before restoring the solution, allowing the complete suite to
run before a new analyzer version exists on NuGet. These verification commands do
not publish packages.
Loading
Loading