From 8b14a49035311776cf2da6d80d21b5d5c35bf529 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 6 Aug 2026 10:48:41 -0600 Subject: [PATCH 1/9] Add SignPath package signing Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/actions/publish-artifacts/action.yaml | 6 ++ .github/workflows/build.yml | 48 ++++++++- .github/workflows/release.yml | 102 ++++++++++++++---- CONTRIBUTING.md | 29 +++-- README.md | 7 ++ .../README.md | 4 + src/Nerdbank.MessagePack.Godot/README.md | 4 + src/Nerdbank.MessagePack.SignalR/README.md | 4 + src/Nerdbank.MessagePack/README.md | 4 + 9 files changed, 179 insertions(+), 29 deletions(-) diff --git a/.github/actions/publish-artifacts/action.yaml b/.github/actions/publish-artifacts/action.yaml index 87b2f9c0..5a1f9ab4 100644 --- a/.github/actions/publish-artifacts/action.yaml +++ b/.github/actions/publish-artifacts/action.yaml @@ -1,6 +1,11 @@ name: Publish artifacts description: Publish artifacts +outputs: + deployables-artifact-id: + description: The GitHub artifact ID for the deployables artifact + value: ${{ steps.upload-deployables.outputs.artifact-id }} + runs: using: composite steps: @@ -53,6 +58,7 @@ runs: path: ${{ runner.temp }}/_artifacts/symbols continue-on-error: true - name: ๐Ÿ“ข Upload deployables + id: upload-deployables uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: deployables-${{ runner.os }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 828868fb..a98be288 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,8 @@ jobs: runs-on: ${{ matrix.os }} permissions: + actions: read + contents: read packages: write strategy: fail-fast: false @@ -106,15 +108,57 @@ jobs: run: tools/variables/_define.ps1 shell: pwsh - name: ๐Ÿ“ข Publish artifacts + id: publish-artifacts uses: ./.github/actions/publish-artifacts if: cancelled() == false + - name: ๐Ÿ”ข Get package version + id: package-version + if: success() && runner.os == 'Linux' && github.event_name != 'pull_request' + run: '"version=$(nbgv get-version -v NuGetPackageVersion)" >> $env:GITHUB_OUTPUT' + shell: pwsh + - name: ๐Ÿงช Test-sign NuGet packages + id: test-sign + if: success() && runner.os == 'Linux' && github.event_name != 'pull_request' + uses: SignPath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2.2 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: test-signing + artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }} + github-artifact-id: ${{ steps.publish-artifacts.outputs.deployables-artifact-id }} + output-artifact-directory: ${{ runner.temp }}/_artifacts/test-signed-deployables + parameters: | + version: ${{ toJSON(steps.package-version.outputs.version) }} + - name: ๐Ÿ“ข Upload test-signed deployables + if: success() && runner.os == 'Linux' && github.event_name != 'pull_request' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: test-signed-deployables-${{ runner.os }} + path: ${{ runner.temp }}/_artifacts/test-signed-deployables + - name: ๐Ÿ“ Save signing request + if: success() && runner.os == 'Linux' && github.event_name != 'pull_request' + run: | + @{ + signingRequestId = '${{ steps.test-sign.outputs.signing-request-id }}' + version = '${{ steps.package-version.outputs.version }}' + commit = '${{ github.sha }}' + } | ConvertTo-Json | Set-Content '${{ runner.temp }}/_artifacts/signing-request.json' + shell: pwsh + - name: ๐Ÿ“ข Upload signing request + if: success() && runner.os == 'Linux' && github.event_name != 'pull_request' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: signing-request-${{ runner.os }} + path: ${{ runner.temp }}/_artifacts/signing-request.json - name: ๐Ÿ“ฆ Push CI package shell: pwsh run: | - dotnet nuget push ${{ runner.temp }}/_artifacts/deployables/*.nupkg -s https://nuget.pkg.github.com/aarnott/index.json -k ${{ secrets.GITHUB_TOKEN }} + $SignedDeployables = '${{ runner.temp }}/_artifacts/test-signed-deployables' + dotnet nuget push "$SignedDeployables/*.nupkg" -s https://nuget.pkg.github.com/aarnott/index.json -k ${{ secrets.GITHUB_TOKEN }} if ('${{ secrets.AZP_TOKEN }}') { dotnet nuget add source https://pkgs.dev.azure.com/andrewarnott/OSS/_packaging/PublicCI/nuget/v3/index.json -n publicCI -u andrewarnott -p ${{ secrets.AZP_TOKEN }} --store-password-in-clear-text - dotnet nuget push ${{ runner.temp }}/_artifacts/deployables/*.nupkg -s publicCI -k x + dotnet nuget push "$SignedDeployables/*.nupkg" -s publicCI -k x } if: success() && runner.os == 'Linux' && github.event_name != 'pull_request' continue-on-error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 155fcd16..9d9f1df6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,7 @@ run-name: ${{ github.ref_name }} jobs: release: runs-on: ubuntu-24.04 + timeout-minutes: 360 environment: Publishing permissions: actions: read # Required to download artifacts @@ -26,59 +27,100 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | + $restApiRoot = '/repos/${{ github.repository }}' + if ('${{ inputs.ship_run_id }}') { $runid = '${{ inputs.ship_run_id }}' + $run = gh api "$restApiRoot/actions/runs/$runid" | ConvertFrom-Json + if ($run.conclusion -ne 'success' ` + -or $run.event -ne 'push' ` + -or $run.head_branch -notmatch '^(?:main|v\d+(?:\.\d+)?)$' ` + -or $run.path -notmatch '^\.github/workflows/build\.yml(?:@|$)') { + throw "Run $runid is not a successful main or release-branch build from build.yml." + } + $commitSha = $run.head_sha } else { - $restApiRoot = '/repos/${{ github.repository }}' - - # Resolve the tag reference to a commit sha + # Resolve the release tag to the commit that the selected build must match. $resolvedRef = gh api ` -H "Accept: application/vnd.github+json" ` -H "X-GitHub-Api-Version: 2022-11-28" ` $restApiRoot/git/ref/tags/${{ github.ref_name }} ` | ConvertFrom-Json $commitSha = $resolvedRef.object.sha - Write-Host "Resolved ${{ github.ref_name }} to $commitSha" $releases = gh run list -R ${{ github.repository }} -c $commitSha -w .github/workflows/build.yml -s success --json databaseId,startedAt,event,headBranch ` | ConvertFrom-Json | Sort-Object startedAt -Descending - $preferredReleases = $releases | Where-Object { - $_.event -eq 'push' -and ($_.headBranch -eq 'main' -or $_.headBranch -match '^v\d+(?:\.\d+)?$') + $eligibleBuilds = @($releases | Where-Object { + $_.event -eq 'push' -and $_.headBranch -match '^(?:main|v\d+(?:\.\d+)?)$' + }) + $releaseBranchBuilds = @($eligibleBuilds | Where-Object { + $_.headBranch -match '^v\d+(?:\.\d+)?$' + }) + + if ($releaseBranchBuilds.Count -gt 0) { + $selectedBuilds = $releaseBranchBuilds + } else { + $selectedBuilds = $eligibleBuilds } if ($releases.length -eq 0) { - Write-Error "No successful builds found for ${{ github.ref }}." + throw "No successful builds found for ${{ github.ref }}." + } elseif ($selectedBuilds.Count -eq 0) { + throw "No successful main or v*.* release-branch build was found for ${{ github.ref }}." } else { - if ($preferredReleases.length -gt 0) { - $selectedReleases = $preferredReleases - $multipleRunsWarning = "More than one successful run found for ${{ github.ref }}. Preferring the most recent push build from a public-release branch." - } else { - $selectedReleases = $releases - $multipleRunsWarning = "More than one successful run found for ${{ github.ref }} and none were from a public-release branch. Artifacts from the most recent successful run will ship." + if ($selectedBuilds.Count -gt 1) { + Write-Warning "More than one eligible build was found for ${{ github.ref }}. Using the most recent." } - if ($releases.length -gt 1) { - Write-Warning $multipleRunsWarning - } - - $runid = $selectedReleases[0].databaseId + $runid = $selectedBuilds[0].databaseId } } Write-Host "Using artifacts from run-id: $runid" Echo "runid=$runid" >> $env:GITHUB_OUTPUT + Echo "commitsha=$commitSha" >> $env:GITHUB_OUTPUT - - name: ๐Ÿ”ป Download deployables artifacts + - name: ๐Ÿ”ป Download signing request uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: deployables-Linux - path: ${{ runner.temp }}/deployables + name: signing-request-Linux + path: ${{ runner.temp }}/signing run-id: ${{ steps.findrunid.outputs.runid }} github-token: ${{ github.token }} + - name: โš™ Install SignPath PowerShell module + run: Install-Module -Name SignPath -RequiredVersion 4.4.6 -Scope CurrentUser -Force + shell: pwsh + + - name: ๐Ÿ” Release-sign NuGet packages + timeout-minutes: 340 + env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} + SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + run: | + Import-Module SignPath -RequiredVersion 4.4.6 + $SigningRequest = Get-Content '${{ runner.temp }}/signing/signing-request.json' | ConvertFrom-Json + if ($SigningRequest.commit -ne '${{ steps.findrunid.outputs.commitsha }}') { + throw "Signing request commit $($SigningRequest.commit) does not match ${{ steps.findrunid.outputs.commitsha }}." + } + + $SignedArchive = Join-Path $env:RUNNER_TEMP release-signed-deployables.zip + Submit-SigningRequest ` + -Resubmit ` + -OriginalSigningRequestId $SigningRequest.signingRequestId ` + -SigningPolicySlug release-signing ` + -Description $SigningRequest.version ` + -WaitForCompletion ` + -WaitForCompletionTimeoutInSeconds 19800 ` + -OutputArtifactPath $SignedArchive ` + -OrganizationId $env:SIGNPATH_ORGANIZATION_ID ` + -ApiToken $env:SIGNPATH_API_TOKEN + Expand-Archive $SignedArchive -DestinationPath '${{ runner.temp }}/deployables' + shell: pwsh + - name: ๐Ÿชช Authorize NuGet package push uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 id: nuget-login @@ -88,6 +130,24 @@ jobs: - name: ๐Ÿš€ Push NuGet packages run: dotnet nuget push ${{ runner.temp }}/deployables/*.nupkg --source https://api.nuget.org/v3/index.json -k '${{ steps.nuget-login.outputs.NUGET_API_KEY }}' + - name: ๐Ÿ“ Add code signing policy to release + shell: pwsh + if: ${{ github.event_name == 'release' }} + env: + GH_TOKEN: ${{ github.token }} + run: | + $restApiRoot = '/repos/${{ github.repository }}' + $release = gh api "$restApiRoot/releases/tags/${{ github.ref_name }}" | ConvertFrom-Json + if ($release.body -notmatch '(?im)^## Code signing policy\s*$') { + $policy = @' + ## Code signing policy + + Free code signing provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org). See the project's [code signing policy](https://github.com/AArnott/Nerdbank.MessagePack#code-signing-policy). + '@ + $body = "$(([string]$release.body).TrimEnd())`n`n$policy" + @{ body = $body } | ConvertTo-Json | gh api --method PATCH "$restApiRoot/releases/$($release.id)" --input - + } + - name: ๐Ÿ’ฝ Upload artifacts to release shell: pwsh if: ${{ github.event_name == 'release' && github.event.release.assets_url != '' }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dff028e6..e6905dcc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,21 +49,38 @@ There may be tests that are known to be unstable or have special requirements. T ## Releases -Use `nbgv tag` to create a tag for a particular commit that you mean to release. -[Learn more about `nbgv` and its `tag` and `prepare-release` commands](https://dotnet.github.io/Nerdbank.GitVersioning/docs/nbgv-cli.html). +For stable releases, use `nbgv prepare-release` to create and push the `v*.*` release branch, then wait for its build to complete. Pre-release packages may be released directly from a successful `main` build. -Push the tag. +Run the **Prepare release** workflow on the branch containing the commit to release. It uses `nbgv tag` to tag that commit and creates a draft GitHub Release. Review and publish the release. Publishing resubmits the build's test-signing request to the manually approved `release-signing` policy. ### GitHub Actions When your repo is hosted by GitHub and you are using GitHub Actions, you should create a GitHub Release using the standard GitHub UI. -Having previously used `nbgv tag` and pushing the tag will help you identify the precise commit and name to use for this release. +[Learn more about the `nbgv tag` and `prepare-release` commands](https://dotnet.github.io/Nerdbank.GitVersioning/docs/nbgv-cli.html). After publishing the release, the `.github/workflows/release.yml` workflow will be automatically triggered, which will: 1. Find the most recent `.github/workflows/build.yml` GitHub workflow run of the tagged release. -1. Upload the `deployables` artifact from that workflow run to your GitHub Release. -1. If you have `NUGET_API_KEY` defined as a secret variable for your repo or org, any nuget packages in the `deployables` artifact will be pushed to nuget.org. +1. Download the test-signing request metadata from that workflow run. +1. Resubmit the exact tested artifact to SignPath's manually approved `release-signing` policy. +1. Upload the release-signed packages to the GitHub Release and nuget.org. + +### Code signing + +NuGet packages are signed through [SignPath.io](https://docs.signpath.io/trusted-build-systems/github). Non-PR Linux builds submit the GitHub-hosted `deployables` artifact to the `test-signing` policy and retain the signing request ID. When a release is published, the release workflow accepts a successful build from `main` or a `v*.*` release branch and resubmits that exact signing request to the manually approved `release-signing` policy. + +Configure these GitHub repository settings before merging signing workflow changes: + +| Setting | Kind | Value | +| --- | --- | --- | +| `SIGNPATH_API_TOKEN` | Secret | Token for a SignPath CI user that can submit to both signing policies | +| `SIGNPATH_ORGANIZATION_ID` | Variable | SignPath organization GUID | +| `SIGNPATH_PROJECT_SLUG` | Variable | SignPath project slug | +| `SIGNPATH_ARTIFACT_CONFIGURATION_SLUG` | Variable | Artifact configuration slug for the NuGet package bundle | + +The SignPath project must use `https://github.com/AArnott/Nerdbank.MessagePack` as its repository URL and GitHub Actions as a trusted build system. Configure `test-signing` without approval so CI can exercise signing immediately. Configure `release-signing` with origin verification restricted to `main` and `v*.*` branches and manual approval, as required by the [SignPath Foundation terms](https://signpath.org/terms). + +The artifact configuration must use a `` root because GitHub's artifact action submits a ZIP. Upload a representative `deployables-Linux` artifact to SignPath to generate and review the configuration. It must sign every `*.nupkg` with ``, preserve the `*.snupkg` files, enforce the project name and the `version` parameter supplied by the workflow, and reject unexpected files. ### Azure Pipelines diff --git a/README.md b/README.md index caf63ea1..d484b156 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,13 @@ There are two feeds you can use to acquire these packages: - [GitHub Packages](https://github.com/AArnott?tab=packages&repo_name=Nerdbank.MessagePack) (requires GitHub authentication) - [Azure Artifacts](https://dev.azure.com/andrewarnott/OSS/_artifacts/feed/PublicCI) (no authentication required) +## Code signing policy + +Free code signing provided by [SignPath.io](https://about.signpath.io), certificate by [SignPath Foundation](https://signpath.org). + +* Committer, reviewer, and signing approver: [Andrew Arnott](https://github.com/AArnott) +* Privacy: This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it. + ## Sponsorships [GitHub Sponsors](https://github.com/sponsors/AArnott) diff --git a/src/Nerdbank.MessagePack.AspNetCoreMvcFormatter/README.md b/src/Nerdbank.MessagePack.AspNetCoreMvcFormatter/README.md index b83e87d8..9a3c4e46 100644 --- a/src/Nerdbank.MessagePack.AspNetCoreMvcFormatter/README.md +++ b/src/Nerdbank.MessagePack.AspNetCoreMvcFormatter/README.md @@ -10,3 +10,7 @@ This library provides MessagePack-based formatters for [ASP.NET MVC](https://git ## Usage Add [`MessagePackInputFormatter`](https://aarnott.github.io/Nerdbank.MessagePack/api/Nerdbank.MessagePack.AspNetCoreMvcFormatter.MessagePackInputFormatter.html) and/or [`MessagePackOutputFormatter`](https://aarnott.github.io/Nerdbank.MessagePack/api/Nerdbank.MessagePack.AspNetCoreMvcFormatter.MessagePackOutputFormatter.html) to your input and/or output formatter collections respectively, as demonstrated [on our doc page](https://aarnott.github.io/Nerdbank.MessagePack/docs/aspnetmvc.html). + +## Code signing policy + +See the repository's [code signing policy](https://github.com/AArnott/Nerdbank.MessagePack#code-signing-policy). diff --git a/src/Nerdbank.MessagePack.Godot/README.md b/src/Nerdbank.MessagePack.Godot/README.md index 6f021102..16a151bb 100644 --- a/src/Nerdbank.MessagePack.Godot/README.md +++ b/src/Nerdbank.MessagePack.Godot/README.md @@ -10,3 +10,7 @@ MessagePackSerializer serializer = new MessagePackSerializer().WithGodotConverte ``` See the repository's Godot documentation for installation, supported types, Native AOT guidance, and MessagePack-CSharp migration compatibility. + +## Code signing policy + +See the repository's [code signing policy](https://github.com/AArnott/Nerdbank.MessagePack#code-signing-policy). diff --git a/src/Nerdbank.MessagePack.SignalR/README.md b/src/Nerdbank.MessagePack.SignalR/README.md index 32ac3270..fb3481bd 100644 --- a/src/Nerdbank.MessagePack.SignalR/README.md +++ b/src/Nerdbank.MessagePack.SignalR/README.md @@ -12,3 +12,7 @@ This library provides a MessagePack-based Hub Protocol implementation for ASP.NE Add a call to [`AddMessagePackProtocol`](https://aarnott.github.io/Nerdbank.MessagePack/api/Nerdbank.MessagePack.SignalR.ServiceCollectionExtensions.html#Nerdbank_MessagePack_SignalR_ServiceCollectionExtensions_AddMessagePackProtocol_) to your builder class. See more samples and help [on our doc page](https://aarnott.github.io/Nerdbank.MessagePack/docs/signalr.html). + +## Code signing policy + +See the repository's [code signing policy](https://github.com/AArnott/Nerdbank.MessagePack#code-signing-policy). diff --git a/src/Nerdbank.MessagePack/README.md b/src/Nerdbank.MessagePack/README.md index 2db5af81..a96cb984 100644 --- a/src/Nerdbank.MessagePack/README.md +++ b/src/Nerdbank.MessagePack/README.md @@ -26,3 +26,7 @@ As its natural successor, this library comes packed with features that its prede * Structural (i.e. deep, by-value) equality checking for arbitrary types, both with and without collision resistant hash functions. [See how these features and more compare with the leading MessagePack library](https://aarnott.github.io/Nerdbank.MessagePack/docs/features.html#feature-comparison). + +## Code signing policy + +See the repository's [code signing policy](https://github.com/AArnott/Nerdbank.MessagePack#code-signing-policy). From a2ae1f59764234ccbae22ba094960f1c875fb639 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 6 Aug 2026 11:17:10 -0600 Subject: [PATCH 2/9] Ignore unavailable SignPath attribution URL Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a98be288..3bf473f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -259,4 +259,4 @@ jobs: - name: ๐Ÿ”— Markup Link Checker (mlc) uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 with: - args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://www.npmjs.com/package/*,https://get.dot.net/,https://dev.azure.com/andrewarnott/OSS/* -i zcash:u1vv2ws6xhs72faugmlrasyeq298l05rrj6wfw8hr3r29y3czev5qt4ugp7kylz6suu04363ze92dfg8ftxf3237js0x9p5r82fgy47xkjnw75tqaevhfh0rnua72hurt22v3w3f7h8yt6mxaa0wpeeh9jcm359ww3rl6fj5ylqqv54uuwrs8q4gys9r3cxdm3yslsh3rt6p7wznzhky7 + args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://www.npmjs.com/package/*,https://get.dot.net/,https://dev.azure.com/andrewarnott/OSS/*,https://about.signpath.io/* -i zcash:u1vv2ws6xhs72faugmlrasyeq298l05rrj6wfw8hr3r29y3czev5qt4ugp7kylz6suu04363ze92dfg8ftxf3237js0x9p5r82fgy47xkjnw75tqaevhfh0rnua72hurt22v3w3f7h8yt6mxaa0wpeeh9jcm359ww3rl6fj5ylqqv54uuwrs8q4gys9r3cxdm3yslsh3rt6p7wznzhky7 From c5854e79d7193ddc5a6295b193e6d8f5f5b0d7f5 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 6 Aug 2026 17:51:59 -0600 Subject: [PATCH 3/9] Ignore bare SignPath attribution URL Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bf473f0..5c2b434a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -259,4 +259,4 @@ jobs: - name: ๐Ÿ”— Markup Link Checker (mlc) uses: becheran/mlc@7ec24825cefe0c9c8c6bac48430e1f69e3ec356e # v1.2.0 with: - args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://www.npmjs.com/package/*,https://get.dot.net/,https://dev.azure.com/andrewarnott/OSS/*,https://about.signpath.io/* -i zcash:u1vv2ws6xhs72faugmlrasyeq298l05rrj6wfw8hr3r29y3czev5qt4ugp7kylz6suu04363ze92dfg8ftxf3237js0x9p5r82fgy47xkjnw75tqaevhfh0rnua72hurt22v3w3f7h8yt6mxaa0wpeeh9jcm359ww3rl6fj5ylqqv54uuwrs8q4gys9r3cxdm3yslsh3rt6p7wznzhky7 + args: --do-not-warn-for-redirect-to https://learn.microsoft.com*,https://dotnet.microsoft.com/*,https://dev.azure.com/*,https://app.codecov.io/* -p docfx -i https://www.npmjs.com/package/*,https://get.dot.net/,https://dev.azure.com/andrewarnott/OSS/*,https://about.signpath.io,https://about.signpath.io/* -i zcash:u1vv2ws6xhs72faugmlrasyeq298l05rrj6wfw8hr3r29y3czev5qt4ugp7kylz6suu04363ze92dfg8ftxf3237js0x9p5r82fgy47xkjnw75tqaevhfh0rnua72hurt22v3w3f7h8yt6mxaa0wpeeh9jcm359ww3rl6fj5ylqqv54uuwrs8q4gys9r3cxdm3yslsh3rt6p7wznzhky7 From 824f8d433637c5e8f21e581cd2b32d998744bcd9 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 6 Aug 2026 18:25:20 -0600 Subject: [PATCH 4/9] Test SignPath signing on pull requests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 6 +++--- CONTRIBUTING.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c2b434a..57446998 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -113,12 +113,12 @@ jobs: if: cancelled() == false - name: ๐Ÿ”ข Get package version id: package-version - if: success() && runner.os == 'Linux' && github.event_name != 'pull_request' + if: success() && runner.os == 'Linux' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) run: '"version=$(nbgv get-version -v NuGetPackageVersion)" >> $env:GITHUB_OUTPUT' shell: pwsh - name: ๐Ÿงช Test-sign NuGet packages id: test-sign - if: success() && runner.os == 'Linux' && github.event_name != 'pull_request' + if: success() && runner.os == 'Linux' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) uses: SignPath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2.2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} @@ -131,7 +131,7 @@ jobs: parameters: | version: ${{ toJSON(steps.package-version.outputs.version) }} - name: ๐Ÿ“ข Upload test-signed deployables - if: success() && runner.os == 'Linux' && github.event_name != 'pull_request' + if: success() && runner.os == 'Linux' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-signed-deployables-${{ runner.os }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6905dcc..cbe2a5da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,7 +67,7 @@ After publishing the release, the `.github/workflows/release.yml` workflow will ### Code signing -NuGet packages are signed through [SignPath.io](https://docs.signpath.io/trusted-build-systems/github). Non-PR Linux builds submit the GitHub-hosted `deployables` artifact to the `test-signing` policy and retain the signing request ID. When a release is published, the release workflow accepts a successful build from `main` or a `v*.*` release branch and resubmits that exact signing request to the manually approved `release-signing` policy. +NuGet packages are signed through [SignPath.io](https://docs.signpath.io/trusted-build-systems/github). Linux builds submit the GitHub-hosted `deployables` artifact to the `test-signing` policy, including pull requests whose branches belong to this repository. Fork pull requests cannot test signing because GitHub does not expose repository secrets to them. Non-PR builds retain the signing request ID. When a release is published, the release workflow accepts a successful build from `main` or a `v*.*` release branch and resubmits that exact signing request to the manually approved `release-signing` policy. Configure these GitHub repository settings before merging signing workflow changes: From e8fa17af25e75431e11f80db098292597680f763 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 6 Aug 2026 18:29:50 -0600 Subject: [PATCH 5/9] Resolve annotated release tags Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d9f1df6..d1514aa8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,15 @@ jobs: -H "X-GitHub-Api-Version: 2022-11-28" ` $restApiRoot/git/ref/tags/${{ github.ref_name }} ` | ConvertFrom-Json - $commitSha = $resolvedRef.object.sha + $tagTarget = $resolvedRef.object + while ($tagTarget.type -eq 'tag') { + $tagObject = gh api "$restApiRoot/git/tags/$($tagTarget.sha)" | ConvertFrom-Json + $tagTarget = $tagObject.object + } + if ($tagTarget.type -ne 'commit') { + throw "Release tag points to an unsupported $($tagTarget.type) object." + } + $commitSha = $tagTarget.sha Write-Host "Resolved ${{ github.ref_name }} to $commitSha" $releases = gh run list -R ${{ github.repository }} -c $commitSha -w .github/workflows/build.yml -s success --json databaseId,startedAt,event,headBranch ` From c8bc530683ef17813ef0c5ddb61f82ab192b73c2 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 6 Aug 2026 18:57:55 -0600 Subject: [PATCH 6/9] Version SignPath artifact configuration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 30 ++++++++++++++++++++++++++++-- CONTRIBUTING.md | 5 +++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57446998..81ed4d4d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -114,7 +114,9 @@ jobs: - name: ๐Ÿ”ข Get package version id: package-version if: success() && runner.os == 'Linux' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - run: '"version=$(nbgv get-version -v NuGetPackageVersion)" >> $env:GITHUB_OUTPUT' + run: | + "version=$(nbgv get-version -v NuGetPackageVersion)" >> $env:GITHUB_OUTPUT + "productVersion=$(nbgv get-version -v AssemblyInformationalVersion)" >> $env:GITHUB_OUTPUT shell: pwsh - name: ๐Ÿงช Test-sign NuGet packages id: test-sign @@ -125,11 +127,35 @@ jobs: organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} signing-policy-slug: test-signing - artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }} + artifact-configuration-slug: initial github-artifact-id: ${{ steps.publish-artifacts.outputs.deployables-artifact-id }} output-artifact-directory: ${{ runner.temp }}/_artifacts/test-signed-deployables parameters: | version: ${{ toJSON(steps.package-version.outputs.version) }} + productVersion: ${{ toJSON(steps.package-version.outputs.productVersion) }} + - name: ๐Ÿ›ก Verify test-signed NuGet packages + if: success() && runner.os == 'Linux' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + run: | + $UnsignedPackages = @(Get-ChildItem '${{ runner.temp }}/_artifacts/deployables/*.nupkg') + $SignedPackages = @(Get-ChildItem '${{ runner.temp }}/_artifacts/test-signed-deployables/*.nupkg') + if ($UnsignedPackages.Count -eq 0) { + throw 'No unsigned NuGet packages were found.' + } + $PackageNameDifferences = Compare-Object $UnsignedPackages.Name $SignedPackages.Name + if ($PackageNameDifferences) { + throw "The signed package set does not match the unsigned package set:`n$($PackageNameDifferences | Out-String)" + } + foreach ($Package in $SignedPackages) { + $Archive = [IO.Compression.ZipFile]::OpenRead($Package.FullName) + try { + if ($Archive.Entries.FullName -notcontains '.signature.p7s') { + throw "No NuGet signature was found in $($Package.Name)." + } + } finally { + $Archive.Dispose() + } + } + shell: pwsh - name: ๐Ÿ“ข Upload test-signed deployables if: success() && runner.os == 'Linux' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cbe2a5da..536cd2e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,11 +76,12 @@ Configure these GitHub repository settings before merging signing workflow chang | `SIGNPATH_API_TOKEN` | Secret | Token for a SignPath CI user that can submit to both signing policies | | `SIGNPATH_ORGANIZATION_ID` | Variable | SignPath organization GUID | | `SIGNPATH_PROJECT_SLUG` | Variable | SignPath project slug | -| `SIGNPATH_ARTIFACT_CONFIGURATION_SLUG` | Variable | Artifact configuration slug for the NuGet package bundle | The SignPath project must use `https://github.com/AArnott/Nerdbank.MessagePack` as its repository URL and GitHub Actions as a trusted build system. Configure `test-signing` without approval so CI can exercise signing immediately. Configure `release-signing` with origin verification restricted to `main` and `v*.*` branches and manual approval, as required by the [SignPath Foundation terms](https://signpath.org/terms). -The artifact configuration must use a `` root because GitHub's artifact action submits a ZIP. Upload a representative `deployables-Linux` artifact to SignPath to generate and review the configuration. It must sign every `*.nupkg` with ``, preserve the `*.snupkg` files, enforce the project name and the `version` parameter supplied by the workflow, and reject unexpected files. +The artifact configuration must use a `` root because GitHub's artifact action submits a ZIP. Upload a representative `deployables-Linux` artifact to SignPath to generate and review the configuration. It must sign every `*.nupkg` with ``, preserve the `*.snupkg` files, and enforce the product names and the `version` and `productVersion` parameters supplied by the workflow. The build verifies that every input NuGet package is returned with a signature. + +The artifact configuration slug is intentionally hardcoded in `.github/workflows/build.yml`. When the artifact structure changes, create a new versioned configuration in SignPath and update the slug in the same pull request. The pull request will validate the new configuration while builds from the default branch continue using the previous configuration. ### Azure Pipelines From 7be6d3143b8293f2a1b40246c368938b586ee69f Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 6 Aug 2026 18:58:12 -0600 Subject: [PATCH 7/9] Verify complete signed artifact set Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81ed4d4d..674fa3f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -136,15 +136,17 @@ jobs: - name: ๐Ÿ›ก Verify test-signed NuGet packages if: success() && runner.os == 'Linux' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) run: | - $UnsignedPackages = @(Get-ChildItem '${{ runner.temp }}/_artifacts/deployables/*.nupkg') - $SignedPackages = @(Get-ChildItem '${{ runner.temp }}/_artifacts/test-signed-deployables/*.nupkg') + $UnsignedFiles = @(Get-ChildItem '${{ runner.temp }}/_artifacts/deployables' -File) + $SignedFiles = @(Get-ChildItem '${{ runner.temp }}/_artifacts/test-signed-deployables' -File) + $FileNameDifferences = Compare-Object $UnsignedFiles.Name $SignedFiles.Name + if ($FileNameDifferences) { + throw "The signed artifact does not contain the same files as the unsigned artifact:`n$($FileNameDifferences | Out-String)" + } + $UnsignedPackages = @($UnsignedFiles | Where-Object Extension -eq '.nupkg') + $SignedPackages = @($SignedFiles | Where-Object Extension -eq '.nupkg') if ($UnsignedPackages.Count -eq 0) { throw 'No unsigned NuGet packages were found.' } - $PackageNameDifferences = Compare-Object $UnsignedPackages.Name $SignedPackages.Name - if ($PackageNameDifferences) { - throw "The signed package set does not match the unsigned package set:`n$($PackageNameDifferences | Out-String)" - } foreach ($Package in $SignedPackages) { $Archive = [IO.Compression.ZipFile]::OpenRead($Package.FullName) try { From 5134aba0099ecf8e421ff89f0a903ca08671e96c Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 6 Aug 2026 19:02:12 -0600 Subject: [PATCH 8/9] Validate manual release workflow IDs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d1514aa8..11786c19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,10 +32,11 @@ jobs: if ('${{ inputs.ship_run_id }}') { $runid = '${{ inputs.ship_run_id }}' $run = gh api "$restApiRoot/actions/runs/$runid" | ConvertFrom-Json + $workflow = gh api "$restApiRoot/actions/workflows/$($run.workflow_id)" | ConvertFrom-Json if ($run.conclusion -ne 'success' ` -or $run.event -ne 'push' ` -or $run.head_branch -notmatch '^(?:main|v\d+(?:\.\d+)?)$' ` - -or $run.path -notmatch '^\.github/workflows/build\.yml(?:@|$)') { + -or $workflow.path -ne '.github/workflows/build.yml') { throw "Run $runid is not a successful main or release-branch build from build.yml." } $commitSha = $run.head_sha From 9d3f6df7ca879fa866f5f0ec8aa7475a20689012 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 6 Aug 2026 19:05:49 -0600 Subject: [PATCH 9/9] Accept patch release branches Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 11786c19..8621a65a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: $workflow = gh api "$restApiRoot/actions/workflows/$($run.workflow_id)" | ConvertFrom-Json if ($run.conclusion -ne 'success' ` -or $run.event -ne 'push' ` - -or $run.head_branch -notmatch '^(?:main|v\d+(?:\.\d+)?)$' ` + -or $run.head_branch -notmatch '^(?:main|v\d+(?:\.\d+)+)$' ` -or $workflow.path -ne '.github/workflows/build.yml') { throw "Run $runid is not a successful main or release-branch build from build.yml." } @@ -62,10 +62,10 @@ jobs: | ConvertFrom-Json | Sort-Object startedAt -Descending $eligibleBuilds = @($releases | Where-Object { - $_.event -eq 'push' -and $_.headBranch -match '^(?:main|v\d+(?:\.\d+)?)$' + $_.event -eq 'push' -and $_.headBranch -match '^(?:main|v\d+(?:\.\d+)+)$' }) $releaseBranchBuilds = @($eligibleBuilds | Where-Object { - $_.headBranch -match '^v\d+(?:\.\d+)?$' + $_.headBranch -match '^v\d+(?:\.\d+)+$' }) if ($releaseBranchBuilds.Count -gt 0) {