mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 00:56:28 +08:00
b292e243c6
## Summary - replace the preview-only workflow with `nuget-publish.yml`, running on every branch push, `v*` tag, and manual dispatch - build and sign all Release packages plus genuine `.Debug` packages, publish normal packages to GitHub Packages, and retain exact signed package and symbol bytes - add manual `release.yml` promotion from a successful `release/*` publish run to nuget.org through `NuGet/login@v1` trusted publishing - validate package identities, versions, signatures, symbols, duplicates, and SHA-256 hashes before publication and promotion ## Signing configuration All required repository Actions secrets are configured, including `OPCFOUNDATION_NETSTANDARD_SNK_BASE64`, which was transferred directly from Azure DevOps Secure Files without placing the private key in a pipeline artifact. ## nuget.org setup The account-side trusted publishing policy must use: - Repository Owner: `OPCFoundation` - Repository: `UA-.NETStandard` - Workflow File: `release.yml` - Environment: `release` The existing `release` environment contains `NUGET_USER` and is restricted to `release/*` branches. ## Validation - parsed both workflow YAML files against the GitHub workflow schema - parsed all embedded PowerShell and the shared package validator - packed real Release and Debug `Opc.Ua.Types` packages and validated their identities, matching versions, symbols, and `.Debug` family - verified all seven signing secret names and removal of temporary key material --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d639a224-b97c-4dbf-ba98-f99e6e3f6b9a
204 lines
7.8 KiB
YAML
204 lines
7.8 KiB
YAML
name: Release to nuget.org
|
|
|
|
# Manual promotion of the exact signed package bytes produced by a successful
|
|
# nuget-publish.yml run on a release/* branch. The release environment provides
|
|
# the approval boundary and NuGet trusted publishing identity.
|
|
#
|
|
# One-time nuget.org trusted publishing policy:
|
|
# Repository Owner: OPCFoundation
|
|
# Repository: UA-.NETStandard
|
|
# Workflow File: release.yml
|
|
# Environment: release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
release_run_id:
|
|
description: Successful nuget-publish.yml run ID from a release/* branch
|
|
required: true
|
|
type: string
|
|
dry_run:
|
|
description: Validate the selected package set without publishing
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
id-token: write
|
|
|
|
concurrency:
|
|
group: nuget-release
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
publish:
|
|
name: Promote signed packages
|
|
runs-on: windows-2025
|
|
environment: release
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Setup .NET 10.0
|
|
uses: actions/setup-dotnet@v6
|
|
with:
|
|
dotnet-version: '10.0.x'
|
|
|
|
- name: Validate selected publish run
|
|
id: source
|
|
shell: pwsh
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_RUN_ID: ${{ inputs.release_run_id }}
|
|
run: |
|
|
if ($env:RELEASE_RUN_ID -notmatch '^\d+$') {
|
|
throw 'release_run_id must be a numeric GitHub Actions run ID.'
|
|
}
|
|
|
|
$runJson = gh api "repos/${{ github.repository }}/actions/runs/$env:RELEASE_RUN_ID"
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Unable to read workflow run $env:RELEASE_RUN_ID."
|
|
}
|
|
$run = $runJson | ConvertFrom-Json
|
|
|
|
$workflowJson = gh api (
|
|
"repos/${{ github.repository }}/actions/workflows/$($run.workflow_id)")
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Unable to identify the workflow for run $env:RELEASE_RUN_ID."
|
|
}
|
|
$workflow = $workflowJson | ConvertFrom-Json
|
|
|
|
if ($workflow.path -cne '.github/workflows/nuget-publish.yml') {
|
|
throw "Run $env:RELEASE_RUN_ID belongs to '$($workflow.path)', not nuget-publish.yml."
|
|
}
|
|
if ($run.status -cne 'completed' -or $run.conclusion -cne 'success') {
|
|
throw (
|
|
"Run $env:RELEASE_RUN_ID is not successful: " +
|
|
"status=$($run.status), conclusion=$($run.conclusion).")
|
|
}
|
|
if (-not $run.head_branch.StartsWith(
|
|
'release/',
|
|
[StringComparison]::OrdinalIgnoreCase)) {
|
|
throw (
|
|
"Run $env:RELEASE_RUN_ID came from '$($run.head_branch)'. " +
|
|
'Only release/* branches can be promoted.')
|
|
}
|
|
if ($run.head_repository.full_name -cne '${{ github.repository }}') {
|
|
throw (
|
|
"Run $env:RELEASE_RUN_ID came from '$($run.head_repository.full_name)', " +
|
|
"not '${{ github.repository }}'.")
|
|
}
|
|
|
|
"commit=$($run.head_sha)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
|
"branch=$($run.head_branch)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
|
"artifact=opcua-packages-$env:RELEASE_RUN_ID" |
|
|
Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
|
Write-Host (
|
|
"Selected successful nuget-publish.yml run $env:RELEASE_RUN_ID " +
|
|
"for $($run.head_branch) at $($run.head_sha).")
|
|
|
|
- name: Download signed promotion artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
run-id: ${{ inputs.release_run_id }}
|
|
name: ${{ steps.source.outputs.artifact }}
|
|
path: promotion
|
|
|
|
- name: Validate promotion manifest and package bytes
|
|
shell: pwsh
|
|
run: |
|
|
$manifestPath = 'promotion/release-manifest.json'
|
|
if (-not (Test-Path -LiteralPath $manifestPath)) {
|
|
throw "The selected run artifact contains no release-manifest.json."
|
|
}
|
|
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
|
$expectedRef = 'refs/heads/${{ steps.source.outputs.branch }}'
|
|
if ($manifest.schemaVersion -ne 1 -or
|
|
$manifest.repository -cne '${{ github.repository }}' -or
|
|
$manifest.workflow -cne '.github/workflows/nuget-publish.yml' -or
|
|
$manifest.runId -cne '${{ inputs.release_run_id }}' -or
|
|
$manifest.ref -cne $expectedRef -or
|
|
$manifest.commit -cne '${{ steps.source.outputs.commit }}') {
|
|
throw (
|
|
"The promotion manifest does not match nuget-publish.yml run " +
|
|
"${{ inputs.release_run_id }} on $expectedRef.")
|
|
}
|
|
|
|
$currentPath = 'promotion/current-package-set.json'
|
|
./.azurepipelines/validate-nuget-package-set.ps1 `
|
|
-PackageDirectory promotion `
|
|
-ManifestPath $currentPath `
|
|
-ExpectedVersion $manifest.packageVersion `
|
|
-RequireDebug `
|
|
-VerifySignatures
|
|
$current = Get-Content -LiteralPath $currentPath -Raw | ConvertFrom-Json
|
|
|
|
if ($current.packageCount -ne $manifest.packageCount -or
|
|
$current.symbolPackageCount -ne $manifest.symbolPackageCount -or
|
|
$current.debugPackageCount -ne $manifest.debugPackageCount) {
|
|
throw 'The downloaded package counts do not match the promotion manifest.'
|
|
}
|
|
|
|
$expectedArchives = @($manifest.archives | Sort-Object id, type, file) |
|
|
ConvertTo-Json -Depth 5 -Compress
|
|
$actualArchives = @($current.archives | Sort-Object id, type, file) |
|
|
ConvertTo-Json -Depth 5 -Compress
|
|
if ($actualArchives -cne $expectedArchives) {
|
|
throw 'The downloaded package names, identities, versions, or hashes have changed.'
|
|
}
|
|
|
|
Write-Host (
|
|
"Validated $($current.packageCount) exact package bytes at version " +
|
|
"$($current.packageVersion) from run ${{ inputs.release_run_id }}.")
|
|
|
|
- name: NuGet login
|
|
if: ${{ !inputs.dry_run }}
|
|
id: login
|
|
uses: NuGet/login@v1
|
|
with:
|
|
user: ${{ secrets.NUGET_USER }}
|
|
|
|
- name: Push to nuget.org
|
|
if: ${{ !inputs.dry_run }}
|
|
shell: pwsh
|
|
run: |
|
|
$packages = @(Get-ChildItem promotion -Filter '*.nupkg' -File | Sort-Object Name)
|
|
foreach ($package in $packages) {
|
|
dotnet nuget push $package.FullName `
|
|
--source 'https://api.nuget.org/v3/index.json' `
|
|
--symbol-source 'https://api.nuget.org/v3/index.json' `
|
|
--api-key '${{ steps.login.outputs.NUGET_API_KEY }}' `
|
|
--skip-duplicate
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "nuget.org push failed for '$($package.Name)'."
|
|
}
|
|
}
|
|
|
|
- name: Summary
|
|
if: always()
|
|
shell: pwsh
|
|
run: |
|
|
$manifestPath = 'promotion/release-manifest.json'
|
|
if (Test-Path -LiteralPath $manifestPath) {
|
|
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
|
$version = $manifest.packageVersion
|
|
$count = $manifest.packageCount
|
|
}
|
|
else {
|
|
$version = 'unavailable'
|
|
$count = 'unavailable'
|
|
}
|
|
@"
|
|
### nuget.org release
|
|
|
|
- Source run: ``${{ inputs.release_run_id }}``
|
|
- Source branch: ``${{ steps.source.outputs.branch }}``
|
|
- Version: ``$version``
|
|
- Packages: ``$count``
|
|
- Dry run: ``${{ inputs.dry_run }}``
|
|
"@ | Add-Content -LiteralPath $env:GITHUB_STEP_SUMMARY
|