Skip to content

Publish NuGet

Publish NuGet #27

Workflow file for this run

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