Publish NuGet #27
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: Publish NuGet | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| target: | |
| description: "Dry run only, or publish to nuget.org" | |
| required: true | |
| default: dry-run | |
| type: choice | |
| options: | |
| - dry-run | |
| - nuget-org | |
| confirm: | |
| description: "Required for nuget-org: enter the exact shared package version" | |
| required: false | |
| type: string | |
| permissions: | |
| contents: read | |
| jobs: | |
| pack: | |
| name: Validate, test, and pack | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.validate.outputs.version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Validate release inputs | |
| id: validate | |
| shell: pwsh | |
| env: | |
| SDK_PROJECT: ./sdk/dotnet/src/DotCraft.Sdk/DotCraft.Sdk.csproj | |
| HARNESS_PROJECT: ./src/DotCraft.Harness/DotCraft.Harness.csproj | |
| TARGET: ${{ inputs.target }} | |
| CONFIRM: ${{ inputs.confirm }} | |
| GITHUB_REF_FULL: ${{ github.ref }} | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $projects = @( | |
| @{ Id = "DotCraft.Sdk"; Path = $env:SDK_PROJECT }, | |
| @{ Id = "DotCraft.Harness"; Path = $env:HARNESS_PROJECT } | |
| ) | |
| $versionsById = @{} | |
| foreach ($package in $projects) { | |
| if (!(Test-Path $package.Path)) { | |
| throw "Package project not found: $($package.Path)" | |
| } | |
| [xml]$project = Get-Content -Raw $package.Path | |
| $packageVersion = @($project.Project.PropertyGroup | ForEach-Object { $_.Version } | Where-Object { ![string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1) | |
| $packageVersion = "$packageVersion".Trim() | |
| if ([string]::IsNullOrWhiteSpace($packageVersion)) { | |
| throw "Set <Version> in $($package.Path) before publishing." | |
| } | |
| $versionsById[$package.Id] = $packageVersion | |
| } | |
| $version = $versionsById["DotCraft.Sdk"] | |
| if ($versionsById["DotCraft.Harness"] -cne $version) { | |
| throw "DotCraft.Sdk and DotCraft.Harness must use the same version." | |
| } | |
| $previewPattern = '^\d+\.\d+\.\d+-preview\.\d+$' | |
| $stablePattern = '^\d+\.\d+\.\d+$' | |
| if ($version -match $previewPattern) { | |
| $channel = "preview" | |
| } | |
| elseif ($version -match $stablePattern) { | |
| $channel = "stable" | |
| } | |
| else { | |
| throw "Package version must be MAJOR.MINOR.PATCH-preview.N or MAJOR.MINOR.PATCH. Current value: $version" | |
| } | |
| if ($env:TARGET -eq "nuget-org") { | |
| if ($env:GITHUB_REF_FULL -ne "refs/heads/main") { | |
| throw "nuget.org publishing is allowed only from the main branch." | |
| } | |
| if ($env:CONFIRM -cne $version) { | |
| throw "Confirmation must exactly match the shared package version: $version" | |
| } | |
| } | |
| foreach ($package in $projects) { | |
| $packagePath = $package.Id.ToLowerInvariant() | |
| $indexUrl = "https://api.nuget.org/v3-flatcontainer/$packagePath/index.json" | |
| try { | |
| $index = Invoke-RestMethod -Uri $indexUrl | |
| $publishedVersions = @($index.versions) | |
| } | |
| catch { | |
| $statusCode = 0 | |
| if ($_.Exception.Response -and $_.Exception.Response.StatusCode) { | |
| $statusCode = [int]$_.Exception.Response.StatusCode | |
| } | |
| if ($statusCode -eq 404) { | |
| $publishedVersions = @() | |
| } | |
| else { | |
| throw | |
| } | |
| } | |
| if ($publishedVersions -contains $version.ToLowerInvariant()) { | |
| throw "$($package.Id) $version already exists on nuget.org." | |
| } | |
| } | |
| "version=$version" >> $env:GITHUB_OUTPUT | |
| Write-Host "Validated DotCraft.Sdk and DotCraft.Harness $version ($channel) for $($env:TARGET)." | |
| - name: Restore SDK | |
| run: dotnet restore ./sdk/dotnet/DotCraft.Sdk.sln | |
| - name: Restore Harness | |
| run: dotnet restore ./tests/DotCraft.Harness.Tests/DotCraft.Harness.Tests.csproj | |
| - name: Build SDK | |
| shell: pwsh | |
| env: | |
| PACKAGE_VERSION: ${{ steps.validate.outputs.version }} | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| dotnet build ./sdk/dotnet/DotCraft.Sdk.sln ` | |
| --configuration Release ` | |
| --no-restore ` | |
| /p:Version=$env:PACKAGE_VERSION ` | |
| /p:PackageVersion=$env:PACKAGE_VERSION ` | |
| /p:DebugSymbols=true ` | |
| /p:DebugType=portable ` | |
| /p:ContinuousIntegrationBuild=true | |
| - name: Test SDK | |
| run: dotnet test ./sdk/dotnet/DotCraft.Sdk.sln --configuration Release --no-build | |
| - name: Build Harness | |
| shell: pwsh | |
| env: | |
| PACKAGE_VERSION: ${{ steps.validate.outputs.version }} | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| dotnet build ./tests/DotCraft.Harness.Tests/DotCraft.Harness.Tests.csproj ` | |
| --configuration Release ` | |
| --no-restore ` | |
| /p:Version=$env:PACKAGE_VERSION ` | |
| /p:PackageVersion=$env:PACKAGE_VERSION ` | |
| /p:ContinuousIntegrationBuild=true | |
| - name: Test Harness | |
| run: dotnet test ./tests/DotCraft.Harness.Tests/DotCraft.Harness.Tests.csproj --configuration Release --no-build | |
| - name: Pack release artifacts | |
| shell: pwsh | |
| env: | |
| PACKAGE_VERSION: ${{ steps.validate.outputs.version }} | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| New-Item -ItemType Directory -Force -Path artifacts/nuget | Out-Null | |
| dotnet pack ./sdk/dotnet/src/DotCraft.Sdk/DotCraft.Sdk.csproj ` | |
| --configuration Release ` | |
| --no-build ` | |
| --output artifacts/nuget ` | |
| /p:Version=$env:PACKAGE_VERSION ` | |
| /p:PackageVersion=$env:PACKAGE_VERSION ` | |
| /p:DebugSymbols=true ` | |
| /p:DebugType=portable ` | |
| /p:ContinuousIntegrationBuild=true | |
| dotnet pack ./src/DotCraft.Harness/DotCraft.Harness.csproj ` | |
| --configuration Release ` | |
| --no-build ` | |
| --output artifacts/nuget ` | |
| /p:Version=$env:PACKAGE_VERSION ` | |
| /p:PackageVersion=$env:PACKAGE_VERSION ` | |
| /p:ContinuousIntegrationBuild=true | |
| - name: Inspect package artifacts | |
| shell: pwsh | |
| env: | |
| PACKAGE_VERSION: ${{ steps.validate.outputs.version }} | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $nupkg = "artifacts/nuget/DotCraft.Sdk.$env:PACKAGE_VERSION.nupkg" | |
| $snupkg = "artifacts/nuget/DotCraft.Sdk.$env:PACKAGE_VERSION.snupkg" | |
| if (!(Test-Path $nupkg)) { | |
| throw "Missing package: $nupkg" | |
| } | |
| if (!(Test-Path $snupkg)) { | |
| throw "Missing symbol package: $snupkg" | |
| } | |
| $harnessPackage = "artifacts/nuget/DotCraft.Harness.$env:PACKAGE_VERSION.nupkg" | |
| if (!(Test-Path $harnessPackage)) { | |
| throw "Missing package: $harnessPackage" | |
| } | |
| Add-Type -AssemblyName System.IO.Compression.FileSystem | |
| $package = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $nupkg)) | |
| try { | |
| $entries = @($package.Entries | ForEach-Object { $_.FullName }) | |
| foreach ($required in @( | |
| "lib/net10.0/DotCraft.Sdk.dll", | |
| "lib/net10.0/DotCraft.Protocol.dll" | |
| )) { | |
| if ($entries -notcontains $required) { | |
| throw "Package is missing required entry: $required" | |
| } | |
| } | |
| $nuspecEntries = @($package.Entries | Where-Object { $_.FullName -like "*.nuspec" }) | |
| if ($nuspecEntries.Count -ne 1) { | |
| throw "Expected exactly one .nuspec entry, found $($nuspecEntries.Count)." | |
| } | |
| $nuspecEntry = $nuspecEntries[0] | |
| $reader = [System.IO.StreamReader]::new($nuspecEntry.Open()) | |
| try { | |
| $nuspec = $reader.ReadToEnd() | |
| } | |
| finally { | |
| $reader.Dispose() | |
| } | |
| if ($nuspec -match '<dependency\s+id="DotCraft\.Protocol"') { | |
| throw "DotCraft.Sdk must bundle the protocol assembly instead of declaring a package dependency." | |
| } | |
| } | |
| finally { | |
| $package.Dispose() | |
| } | |
| $symbols = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $snupkg)) | |
| try { | |
| if ($symbols.Entries.FullName -contains "lib/net10.0/DotCraft.Protocol.pdb") { | |
| throw "The symbol package must not contain DotCraft.Protocol.pdb." | |
| } | |
| } | |
| finally { | |
| $symbols.Dispose() | |
| } | |
| Get-ChildItem artifacts/nuget | Format-Table Name, Length | |
| Get-FileHash $nupkg, $snupkg, $harnessPackage -Algorithm SHA256 | Format-Table Algorithm, Hash, Path | |
| - name: Restore bundled contracts from isolated source | |
| shell: pwsh | |
| env: | |
| PACKAGE_VERSION: ${{ steps.validate.outputs.version }} | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $probe = Join-Path ([System.IO.Path]::GetTempPath()) "dotcraft-sdk-package-probe-$([guid]::NewGuid().ToString('N'))" | |
| New-Item -ItemType Directory -Path $probe | Out-Null | |
| try { | |
| dotnet new console --framework net10.0 --no-restore --output $probe | |
| Push-Location $probe | |
| try { | |
| dotnet add package DotCraft.Sdk --version $env:PACKAGE_VERSION --no-restore | |
| $nugetConfig = @' | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <configuration> | |
| <packageSources> | |
| <clear /> | |
| <add key="package-under-test" value="PACKAGE_SOURCE" /> | |
| </packageSources> | |
| </configuration> | |
| '@ | |
| $packageSource = (Resolve-Path "$env:GITHUB_WORKSPACE/artifacts/nuget").Path | |
| $nugetConfig.Replace("PACKAGE_SOURCE", $packageSource) | Set-Content -Encoding utf8 NuGet.config | |
| $program = @' | |
| using DotCraft.Protocol; | |
| using DotCraft.Protocol.AppServer; | |
| using DotCraft.Sdk; | |
| _ = new RpcEmpty(); | |
| _ = AppServerRpc.Initialize; | |
| _ = new DotCraftClientOptions(); | |
| '@ | |
| $program | Set-Content -Encoding utf8 Program.cs | |
| dotnet restore --configfile NuGet.config | |
| dotnet build --no-restore | |
| } | |
| finally { | |
| Pop-Location | |
| } | |
| } | |
| finally { | |
| Remove-Item -LiteralPath $probe -Recurse -Force | |
| } | |
| - name: Restore and run Harness from isolated source | |
| shell: pwsh | |
| env: | |
| PACKAGE_VERSION: ${{ steps.validate.outputs.version }} | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $probe = Join-Path ([System.IO.Path]::GetTempPath()) "dotcraft-harness-package-probe-$([guid]::NewGuid().ToString('N'))" | |
| New-Item -ItemType Directory -Path $probe | Out-Null | |
| try { | |
| dotnet new console --framework net10.0 --no-restore --output $probe | |
| Copy-Item ./tests/DotCraft.Harness.Consumer/Program.cs (Join-Path $probe "Program.cs") | |
| Push-Location $probe | |
| try { | |
| dotnet add package DotCraft.Harness --version $env:PACKAGE_VERSION --no-restore | |
| dotnet add package Microsoft.Extensions.Hosting --version 10.0.10 --no-restore | |
| $packageSource = (Resolve-Path "$env:GITHUB_WORKSPACE/artifacts/nuget").Path | |
| dotnet restore --source $packageSource --source=https://api.nuget.org/v3/index.json | |
| dotnet run --no-restore | |
| } | |
| finally { | |
| Pop-Location | |
| } | |
| } | |
| finally { | |
| Remove-Item -LiteralPath $probe -Recurse -Force | |
| } | |
| - name: Upload package artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dotcraft-nuget-${{ steps.validate.outputs.version }} | |
| path: artifacts/nuget/* | |
| if-no-files-found: error | |
| publish: | |
| name: Publish to nuget.org | |
| needs: pack | |
| if: ${{ inputs.target == 'nuget-org' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Download package artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dotcraft-nuget-${{ needs.pack.outputs.version }} | |
| path: artifacts/nuget | |
| - name: Validate NuGet trusted publishing user | |
| shell: pwsh | |
| env: | |
| NUGET_USER: ${{ vars.NUGET_USER }} | |
| run: | | |
| if ([string]::IsNullOrWhiteSpace($env:NUGET_USER)) { | |
| throw "Set NUGET_USER to the nuget.org username that created the Trusted Publishing policy. Do not use the GitHubActions publisher label or an email address." | |
| } | |
| - name: NuGet login through Trusted Publishing | |
| id: login | |
| uses: NuGet/login@v1 | |
| with: | |
| user: ${{ vars.NUGET_USER }} | |
| - name: Push exact package artifacts | |
| shell: pwsh | |
| env: | |
| PACKAGE_VERSION: ${{ needs.pack.outputs.version }} | |
| NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $source = "https://api.nuget.org/v3/index.json" | |
| $sdkPackage = "artifacts/nuget/DotCraft.Sdk.$env:PACKAGE_VERSION.nupkg" | |
| $harnessPackage = "artifacts/nuget/DotCraft.Harness.$env:PACKAGE_VERSION.nupkg" | |
| dotnet nuget push $sdkPackage --source $source --api-key $env:NUGET_API_KEY | |
| dotnet nuget push $harnessPackage --source $source --api-key $env:NUGET_API_KEY |