Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 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
6 changes: 6 additions & 0 deletions .github/actions/publish-artifacts/action.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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 }}
Expand Down
78 changes: 75 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:

runs-on: ${{ matrix.os }}
permissions:
actions: read
contents: read
packages: write
Comment thread
AArnott marked this conversation as resolved.
Resolved
strategy:
fail-fast: false
Expand Down Expand Up @@ -106,15 +108,85 @@ 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' || github.event.pull_request.head.repo.full_name == github.repository)
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
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 }}
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: test-signing
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: |
$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.'
}
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
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
Expand Down Expand Up @@ -215,4 +287,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,https://about.signpath.io/* -i zcash:u1vv2ws6xhs72faugmlrasyeq298l05rrj6wfw8hr3r29y3czev5qt4ugp7kylz6suu04363ze92dfg8ftxf3237js0x9p5r82fgy47xkjnw75tqaevhfh0rnua72hurt22v3w3f7h8yt6mxaa0wpeeh9jcm359ww3rl6fj5ylqqv54uuwrs8q4gys9r3cxdm3yslsh3rt6p7wznzhky7
113 changes: 91 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,59 +27,109 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
$restApiRoot = '/repos/${{ github.repository }}'

if ('${{ inputs.ship_run_id }}') {
$runid = '${{ inputs.ship_run_id }}'
Comment thread
AArnott marked this conversation as resolved.
Resolved
$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 $workflow.path -ne '.github/workflows/build.yml') {
throw "Run $runid is not a successful main or release-branch build from build.yml."
}
Comment thread
AArnott marked this conversation as resolved.
Resolved
$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

$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 `
| 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 ($releases.length -gt 1) {
Write-Warning $multipleRunsWarning
if ($selectedBuilds.Count -gt 1) {
Write-Warning "More than one eligible build was found for ${{ github.ref }}. Using the most recent."
}

$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
Comment thread
AArnott marked this conversation as resolved.
Resolved
Expand-Archive $SignedArchive -DestinationPath '${{ runner.temp }}/deployables'
shell: pwsh

- name: 🪪 Authorize NuGet package push
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
id: nuget-login
Expand All @@ -88,6 +139,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).
'@
Comment thread
AArnott marked this conversation as resolved.
Resolved
$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 != '' }}
Expand Down
30 changes: 24 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,39 @@ 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). 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:

| 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 |

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 `<zip-file>` 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 `<nuget-sign>`, 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

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading