Skip to content

release

release #33

Workflow file for this run

name: release
on:
workflow_dispatch:
inputs:
version:
description: Release version to build/publish (for example 0.5.1)
required: true
type: string
publish_nuget:
description: Publish packages to NuGet.org
required: false
default: false
type: boolean
publish_psgallery:
description: Publish Devolutions.Psign to PowerShell Gallery
required: false
default: false
type: boolean
github-env:
description: GitHub environment selection
required: false
type: choice
options:
- auto
- test
- prod
default: auto
dry_run_nuget:
description: Dry-run NuGet publish and GitHub release asset upload
required: false
default: true
type: boolean
sign_dry_run:
description: Sign Windows binaries during dry-run when signing secrets are available
required: false
default: false
type: boolean
permissions:
contents: write
jobs:
preflight:
name: Resolve release metadata
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.info.outputs.release_tag }}
build_ref: ${{ steps.info.outputs.build_ref }}
publish_env: ${{ steps.info.outputs.publish_env }}
publish_nuget: ${{ steps.info.outputs.publish_nuget }}
publish_psgallery: ${{ steps.info.outputs.publish_psgallery }}
dry_run: ${{ steps.info.outputs.dry_run }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Resolve release info
id: info
shell: pwsh
run: |
function Parse-BoolOrDefault {
param(
[string]$Value,
[bool]$Default
)
if ([string]::IsNullOrWhiteSpace($Value)) {
return $Default
}
try {
return [System.Boolean]::Parse($Value)
}
catch {
return $Default
}
}
$runSha = '${{ github.sha }}'
$sourceBranch = '${{ github.ref_name }}'
$buildRef = $runSha
$releaseVersion = '${{ github.event.inputs.version }}'.Trim()
if ([string]::IsNullOrWhiteSpace($releaseVersion)) {
throw 'Workflow input version is required.'
}
if ($releaseVersion.StartsWith('v')) {
$releaseVersion = $releaseVersion.Substring(1)
}
if ($releaseVersion -notmatch '^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$') {
throw "Invalid version format: $releaseVersion (expected 1.2.3 or 1.2.3-suffix)."
}
$releaseTag = "v$releaseVersion"
$isMasterBranch = $sourceBranch -eq 'master'
$publishNuget = Parse-BoolOrDefault '${{ github.event.inputs.publish_nuget }}' $false
$publishPsgallery = Parse-BoolOrDefault '${{ github.event.inputs.publish_psgallery }}' $false
$dryRun = Parse-BoolOrDefault '${{ github.event.inputs.dry_run_nuget }}' $true
$requestedEnvironment = '${{ inputs['github-env'] }}'.Trim().ToLowerInvariant()
if ([string]::IsNullOrWhiteSpace($requestedEnvironment)) {
$requestedEnvironment = 'auto'
}
switch ($requestedEnvironment) {
'auto' {
$publishEnv = if ($isMasterBranch) { 'publish-prod' } else { 'publish-test' }
}
'test' {
$publishEnv = 'publish-test'
}
'prod' {
$publishEnv = 'publish-prod'
}
default {
throw "Unsupported github-env value: $requestedEnvironment (expected auto, test, or prod)."
}
}
if ($requestedEnvironment -eq 'test' -and ($isMasterBranch -or ((-not $dryRun) -and ($publishNuget -or $publishPsgallery)))) {
$publishEnv = 'publish-prod'
Write-Host '::notice::github-env=test was promoted to publish-prod'
}
if ((-not $isMasterBranch) -and $publishEnv -ne 'publish-prod') {
$dryRun = $true
}
if ((-not $publishNuget) -and (-not $publishPsgallery)) {
$dryRun = $true
}
"release_tag=$releaseTag" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"build_ref=$buildRef" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"publish_env=$publishEnv" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"publish_nuget=$($publishNuget.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"publish_psgallery=$($publishPsgallery.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"dry_run=$($dryRun.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "::notice::Release tag: $releaseTag"
Write-Host "::notice::Build ref: $buildRef"
Write-Host "::notice::Source branch: $sourceBranch"
Write-Host "::notice::GitHub environment mode: $requestedEnvironment"
Write-Host "::notice::Publish environment: $publishEnv"
Write-Host "::notice::Publish NuGet: $($publishNuget.ToString().ToLowerInvariant())"
Write-Host "::notice::Publish PowerShell Gallery: $($publishPsgallery.ToString().ToLowerInvariant())"
Write-Host "::notice::NuGet dry-run: $($dryRun.ToString().ToLowerInvariant())"
build:
name: Build ${{ matrix.name }}
runs-on: ${{ matrix.os }}
needs: preflight
strategy:
fail-fast: false
matrix:
include:
- name: linux-x64
rid: linux-x64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
archive: psign-tool-linux-x64.zip
binary: psign-tool
- name: linux-arm64
rid: linux-arm64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
archive: psign-tool-linux-arm64.zip
binary: psign-tool
- name: windows-x64
rid: win-x64
os: windows-2022
target: x86_64-pc-windows-msvc
archive: psign-tool-windows-x64.zip
binary: psign-tool.exe
- name: windows-arm64
rid: win-arm64
os: windows-2022
target: aarch64-pc-windows-msvc
archive: psign-tool-windows-arm64.zip
binary: psign-tool.exe
- name: macos-x64
rid: osx-x64
os: macos-14
target: x86_64-apple-darwin
archive: psign-tool-macos-x64.zip
binary: psign-tool
- name: macos-arm64
rid: osx-arm64
os: macos-14
target: aarch64-apple-darwin
archive: psign-tool-macos-arm64.zip
binary: psign-tool
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.build_ref }}
- name: Install Rust toolchain
shell: pwsh
run: |
rustup toolchain install stable --profile minimal --target "${{ matrix.target }}"
rustup default stable
- name: Cache cargo artifacts
uses: Swatinem/rust-cache@v2.9.1
- name: Install Linux ARM64 linker
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Configure static MSVC runtime
if: contains(matrix.target, 'windows-msvc')
shell: pwsh
run: |
"RUSTFLAGS=-C target-feature=+crt-static" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Build psign-tool
shell: pwsh
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
run: |
$target = '${{ matrix.target }}'
$cargoArgs = @(
'build',
'--locked',
'--release',
'-p',
'psign',
'--bin',
'psign-tool',
'--target',
$target
)
cargo @cargoArgs
- name: Build psign-core shared library
shell: pwsh
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
run: |
$target = '${{ matrix.target }}'
cargo build --locked --release -p psign-portable-ffi --features azure-kv-sign,artifact-signing-rest --target $target
- name: Stage psign-core native library
shell: pwsh
env:
RID: ${{ matrix.rid }}
TARGET: ${{ matrix.target }}
run: |
$sourceName = if ($env:TARGET -like '*windows-msvc') {
'psign_core.dll'
} elseif ($env:TARGET -like '*apple-darwin') {
'libpsign_core.dylib'
} else {
'libpsign_core.so'
}
$destName = if ($env:TARGET -like '*windows-msvc') {
'psign-core.dll'
} elseif ($env:TARGET -like '*apple-darwin') {
'libpsign-core.dylib'
} else {
'libpsign-core.so'
}
$sourcePath = Join-Path (Join-Path (Join-Path 'target' $env:TARGET) 'release') $sourceName
if (-not (Test-Path -LiteralPath $sourcePath)) {
throw "psign-core native library not found: $sourcePath"
}
$nativeOut = Join-Path (Join-Path 'psign-core' $env:RID) 'native'
New-Item -ItemType Directory -Force -Path $nativeOut | Out-Null
Copy-Item -Force -LiteralPath $sourcePath -Destination (Join-Path $nativeOut $destName)
- name: Verify Windows version info
if: contains(matrix.target, 'windows-msvc')
shell: pwsh
env:
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
run: |
$version = $env:RELEASE_TAG.TrimStart('v')
./scripts/ci/test-windows-version-info.ps1 `
-Path "target/${{ matrix.target }}/release/${{ matrix.binary }}" `
-ExpectedVersion $version
- name: Package artifact
if: ${{ !contains(matrix.target, 'windows-msvc') }}
shell: pwsh
env:
TARGET: ${{ matrix.target }}
BIN_NAME: ${{ matrix.binary }}
ARCHIVE_NAME: ${{ matrix.archive }}
run: |
$target = $env:TARGET
$binName = $env:BIN_NAME
$archiveName = $env:ARCHIVE_NAME
$binaryPath = [System.IO.Path]::Combine("target", $target, "release", $binName)
if (-not (Test-Path -Path $binaryPath)) {
throw "Binary not found: $binaryPath"
}
if (Test-Path -Path $archiveName) {
Remove-Item -Path $archiveName -Force
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [System.IO.Compression.ZipFile]::Open($archiveName, [System.IO.Compression.ZipArchiveMode]::Create)
try {
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archive,
$binaryPath,
$binName,
[System.IO.Compression.CompressionLevel]::Optimal
) | Out-Null
}
finally {
$archive.Dispose()
}
Write-Host "Created $archiveName"
- name: Upload packaged artifact
if: ${{ !contains(matrix.target, 'windows-msvc') }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.archive }}
path: ${{ matrix.archive }}
if-no-files-found: error
- name: Upload unsigned Windows binary
if: ${{ contains(matrix.target, 'windows-msvc') }}
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.archive }}-unsigned-bin
path: target/${{ matrix.target }}/release/${{ matrix.binary }}
if-no-files-found: error
- name: Upload psign-core native library
if: ${{ !contains(matrix.target, 'windows-msvc') }}
uses: actions/upload-artifact@v7
with:
name: psign-core-${{ matrix.rid }}
path: psign-core/${{ matrix.rid }}/native/*
if-no-files-found: error
- name: Upload unsigned Windows psign-core native library
if: ${{ contains(matrix.target, 'windows-msvc') }}
uses: actions/upload-artifact@v7
with:
name: unsigned-psign-core-${{ matrix.rid }}
path: psign-core/${{ matrix.rid }}/native/*
if-no-files-found: error
sign_windows:
name: Sign and package Windows artifacts
runs-on: ubuntu-latest
needs:
- preflight
- build
environment: ${{ needs.preflight.outputs.publish_env }}
permissions:
id-token: write
contents: read
steps:
- name: Download unsigned Windows x64 binary
uses: actions/download-artifact@v8
with:
name: psign-tool-windows-x64.zip-unsigned-bin
path: work/win-x64
- name: Download unsigned Windows arm64 binary
uses: actions/download-artifact@v8
with:
name: psign-tool-windows-arm64.zip-unsigned-bin
path: work/win-arm64
- name: Download unsigned Windows x64 psign-core
uses: actions/download-artifact@v8
with:
name: unsigned-psign-core-win-x64
path: work/psign-core-win-x64
- name: Download unsigned Windows arm64 psign-core
uses: actions/download-artifact@v8
with:
name: unsigned-psign-core-win-arm64
path: work/psign-core-win-arm64
- name: Download Linux x64 psign-tool
uses: actions/download-artifact@v8
with:
name: psign-tool-linux-x64.zip
path: work/linux-x64
- name: Resolve signing mode
id: signing_mode
shell: pwsh
env:
PUBLISH_ENV: ${{ needs.preflight.outputs.publish_env }}
CODE_SIGNING_KEYVAULT_URL: ${{ secrets.CODE_SIGNING_KEYVAULT_URL }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
CODE_SIGNING_CLIENT_ID: ${{ secrets.CODE_SIGNING_CLIENT_ID }}
CODE_SIGNING_CLIENT_SECRET: ${{ secrets.CODE_SIGNING_CLIENT_SECRET }}
CODE_SIGNING_CERTIFICATE_NAME: ${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}
CODE_SIGNING_TIMESTAMP_SERVER: ${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }}
TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }}
TRUSTED_SIGNING_PROFILE_NAME: ${{ secrets.TRUSTED_SIGNING_PROFILE_NAME }}
run: |
$dryRun = [System.Boolean]::Parse('${{ needs.preflight.outputs.dry_run }}')
$signDryRun = [System.Boolean]::Parse('${{ github.event.inputs.sign_dry_run }}')
# publish-test signs with the Azure Trusted Signing (Artifact Signing) test
# profile over OIDC; publish-prod keeps the production EV Azure Key Vault path.
if ($env:PUBLISH_ENV -eq 'publish-test') {
$backend = 'trusted-signing'
$requiredValues = @(
$env:AZURE_CLIENT_ID,
$env:AZURE_TENANT_ID,
$env:AZURE_SUBSCRIPTION_ID,
$env:TRUSTED_SIGNING_ENDPOINT,
$env:TRUSTED_SIGNING_ACCOUNT_NAME,
$env:TRUSTED_SIGNING_PROFILE_NAME
)
} else {
$backend = 'key-vault'
$requiredValues = @(
$env:CODE_SIGNING_KEYVAULT_URL,
$env:AZURE_TENANT_ID,
$env:CODE_SIGNING_CLIENT_ID,
$env:CODE_SIGNING_CLIENT_SECRET,
$env:CODE_SIGNING_CERTIFICATE_NAME,
$env:CODE_SIGNING_TIMESTAMP_SERVER
)
}
$hasSigningSecrets = $true
foreach ($value in $requiredValues) {
if ([string]::IsNullOrWhiteSpace($value)) {
$hasSigningSecrets = $false
break
}
}
$shouldSign = $hasSigningSecrets -and ((-not $dryRun) -or $signDryRun)
if (-not $dryRun -and -not $hasSigningSecrets) {
throw "Code signing secrets are required for non-dry-run release jobs ($backend backend)."
}
"dry_run=$($dryRun.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"should_sign=$($shouldSign.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"backend=$backend" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "Windows signing dry-run mode: $dryRun"
Write-Host "Windows signing backend: $backend"
Write-Host "Windows binaries will be signed with Linux psign-tool: $shouldSign"
- name: Add Linux psign-tool to PATH
if: steps.signing_mode.outputs.should_sign == 'true'
shell: pwsh
run: |
$toolDir = Join-Path $env:RUNNER_TEMP 'psign-tool-self-sign'
if (Test-Path -Path $toolDir) {
Remove-Item -Path $toolDir -Recurse -Force
}
New-Item -Path $toolDir -ItemType Directory -Force | Out-Null
$toolArchivePath = 'work/linux-x64/psign-tool-linux-x64.zip'
if (-not (Test-Path -Path $toolArchivePath)) {
throw "Built Linux psign-tool archive was not found at $toolArchivePath"
}
Expand-Archive -Path $toolArchivePath -DestinationPath $toolDir -Force
$sourceToolPath = Join-Path $toolDir 'psign-tool'
if (-not (Test-Path -Path $sourceToolPath)) {
throw "Built Linux psign-tool executable was not found at $sourceToolPath"
}
chmod +x $sourceToolPath
$toolDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
& $sourceToolPath --version
- name: Code sign Windows executables (Azure Key Vault)
if: steps.signing_mode.outputs.should_sign == 'true' && steps.signing_mode.outputs.backend == 'key-vault'
shell: pwsh
env:
CODE_SIGNING_KEYVAULT_URL: ${{ secrets.CODE_SIGNING_KEYVAULT_URL }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
CODE_SIGNING_CLIENT_ID: ${{ secrets.CODE_SIGNING_CLIENT_ID }}
CODE_SIGNING_CLIENT_SECRET: ${{ secrets.CODE_SIGNING_CLIENT_SECRET }}
CODE_SIGNING_CERTIFICATE_NAME: ${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}
CODE_SIGNING_TIMESTAMP_SERVER: ${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}
run: |
psign-tool --mode portable --verbose sign `
--azure-key-vault-tenant-id "$env:AZURE_TENANT_ID" `
--azure-key-vault-url "$env:CODE_SIGNING_KEYVAULT_URL" `
--azure-key-vault-client-id "$env:CODE_SIGNING_CLIENT_ID" `
--azure-key-vault-client-secret "$env:CODE_SIGNING_CLIENT_SECRET" `
--azure-key-vault-certificate "$env:CODE_SIGNING_CERTIFICATE_NAME" `
--timestamp-url "$env:CODE_SIGNING_TIMESTAMP_SERVER" `
--timestamp-digest sha256 `
--digest sha256 `
--exit-codes azure `
"work/win-x64/psign-tool.exe"
if ($LASTEXITCODE -ne 0) {
throw "psign-tool signing failed for win-x64 with exit code $LASTEXITCODE"
}
psign-tool --mode portable --verbose sign `
--azure-key-vault-tenant-id "$env:AZURE_TENANT_ID" `
--azure-key-vault-url "$env:CODE_SIGNING_KEYVAULT_URL" `
--azure-key-vault-client-id "$env:CODE_SIGNING_CLIENT_ID" `
--azure-key-vault-client-secret "$env:CODE_SIGNING_CLIENT_SECRET" `
--azure-key-vault-certificate "$env:CODE_SIGNING_CERTIFICATE_NAME" `
--timestamp-url "$env:CODE_SIGNING_TIMESTAMP_SERVER" `
--timestamp-digest sha256 `
--digest sha256 `
--exit-codes azure `
"work/win-arm64/psign-tool.exe"
if ($LASTEXITCODE -ne 0) {
throw "psign-tool signing failed for win-arm64 with exit code $LASTEXITCODE"
}
psign-tool --mode portable --verbose sign `
--azure-key-vault-tenant-id "$env:AZURE_TENANT_ID" `
--azure-key-vault-url "$env:CODE_SIGNING_KEYVAULT_URL" `
--azure-key-vault-client-id "$env:CODE_SIGNING_CLIENT_ID" `
--azure-key-vault-client-secret "$env:CODE_SIGNING_CLIENT_SECRET" `
--azure-key-vault-certificate "$env:CODE_SIGNING_CERTIFICATE_NAME" `
--timestamp-url "$env:CODE_SIGNING_TIMESTAMP_SERVER" `
--timestamp-digest sha256 `
--digest sha256 `
--exit-codes azure `
"work/psign-core-win-x64/psign-core.dll"
if ($LASTEXITCODE -ne 0) {
throw "psign-core signing failed for win-x64 with exit code $LASTEXITCODE"
}
psign-tool --mode portable --verbose sign `
--azure-key-vault-tenant-id "$env:AZURE_TENANT_ID" `
--azure-key-vault-url "$env:CODE_SIGNING_KEYVAULT_URL" `
--azure-key-vault-client-id "$env:CODE_SIGNING_CLIENT_ID" `
--azure-key-vault-client-secret "$env:CODE_SIGNING_CLIENT_SECRET" `
--azure-key-vault-certificate "$env:CODE_SIGNING_CERTIFICATE_NAME" `
--timestamp-url "$env:CODE_SIGNING_TIMESTAMP_SERVER" `
--timestamp-digest sha256 `
--digest sha256 `
--exit-codes azure `
"work/psign-core-win-arm64/psign-core.dll"
if ($LASTEXITCODE -ne 0) {
throw "psign-core signing failed for win-arm64 with exit code $LASTEXITCODE"
}
- name: Azure login for Trusted Signing
if: steps.signing_mode.outputs.should_sign == 'true' && steps.signing_mode.outputs.backend == 'trusted-signing'
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Code sign Windows executables (Trusted Signing)
if: steps.signing_mode.outputs.should_sign == 'true' && steps.signing_mode.outputs.backend == 'trusted-signing'
shell: pwsh
env:
TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }}
TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }}
TRUSTED_SIGNING_PROFILE_NAME: ${{ secrets.TRUSTED_SIGNING_PROFILE_NAME }}
run: |
$accessToken = az account get-access-token `
--scope https://codesigning.azure.net/.default `
--query accessToken `
--output tsv
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($accessToken)) {
throw 'Failed to acquire a Trusted Signing data-plane access token.'
}
$targets = @(
'work/win-x64/psign-tool.exe',
'work/win-arm64/psign-tool.exe',
'work/psign-core-win-x64/psign-core.dll',
'work/psign-core-win-arm64/psign-core.dll'
)
foreach ($target in $targets) {
psign-tool --mode portable --verbose sign `
--artifact-signing-endpoint "$env:TRUSTED_SIGNING_ENDPOINT" `
--artifact-signing-account-name "$env:TRUSTED_SIGNING_ACCOUNT_NAME" `
--artifact-signing-profile-name "$env:TRUSTED_SIGNING_PROFILE_NAME" `
--artifact-signing-access-token "$accessToken" `
--timestamp-url http://timestamp.acs.microsoft.com/ `
--timestamp-digest sha256 `
--digest sha256 `
"$target"
if ($LASTEXITCODE -ne 0) {
throw "psign-tool Trusted Signing failed for $target with exit code $LASTEXITCODE"
}
}
- name: Package Windows artifacts
shell: pwsh
run: |
if (Test-Path -Path 'psign-tool-windows-x64.zip') {
Remove-Item -Path 'psign-tool-windows-x64.zip' -Force
}
if (Test-Path -Path 'psign-tool-windows-arm64.zip') {
Remove-Item -Path 'psign-tool-windows-arm64.zip' -Force
}
Compress-Archive -Path 'work/win-x64/psign-tool.exe' -DestinationPath 'psign-tool-windows-x64.zip' -Force
Compress-Archive -Path 'work/win-arm64/psign-tool.exe' -DestinationPath 'psign-tool-windows-arm64.zip' -Force
- name: Upload artifact psign-tool-windows-x64.zip
uses: actions/upload-artifact@v7
with:
name: psign-tool-windows-x64.zip
path: psign-tool-windows-x64.zip
if-no-files-found: error
- name: Upload artifact psign-tool-windows-arm64.zip
uses: actions/upload-artifact@v7
with:
name: psign-tool-windows-arm64.zip
path: psign-tool-windows-arm64.zip
if-no-files-found: error
- name: Upload artifact psign-core-win-x64
uses: actions/upload-artifact@v7
with:
name: psign-core-win-x64
path: work/psign-core-win-x64/psign-core.dll
if-no-files-found: error
- name: Upload artifact psign-core-win-arm64
uses: actions/upload-artifact@v7
with:
name: psign-core-win-arm64
path: work/psign-core-win-arm64/psign-core.dll
if-no-files-found: error
pack_dotnet_tool:
name: Pack dotnet tool from prebuilt binaries
runs-on: ubuntu-latest
needs:
- preflight
- sign_windows
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download native artifacts
uses: actions/download-artifact@v8
with:
path: dist
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Import binaries and pack dotnet tool
shell: pwsh
env:
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
run: |
$tag = $env:RELEASE_TAG
if (-not $tag.StartsWith('v')) {
throw "Release tag must start with 'v'. Actual: $tag"
}
$version = $tag.Substring(1)
$stagingRoot = Join-Path $PWD "nuget/staging"
./nuget/pack-psign-dotnet-tool.ps1 `
-Version $version `
-ArtifactsRoot "dist" `
-StagingRoot $stagingRoot `
-OutputDir "dist/nuget"
- name: Smoke-test dotnet tool package install
shell: pwsh
env:
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
run: |
$tag = $env:RELEASE_TAG
if (-not $tag.StartsWith('v')) {
throw "Release tag must start with 'v'. Actual: $tag"
}
./nuget/scripts/Test-PsignDotnetToolPackage.ps1 `
-Version $tag.Substring(1) `
-PackageDir "dist/nuget" `
-ToolPath (Join-Path $env:RUNNER_TEMP "psign-tool-package-smoke")
- name: Upload dotnet tool artifact
uses: actions/upload-artifact@v7
with:
name: devolutions-psign-tool-nupkg
path: dist/nuget/Devolutions.Psign.Tool*.nupkg
if-no-files-found: error
pack_powershell_module:
name: Pack PowerShell module from prebuilt psign-core libraries
runs-on: ubuntu-latest
needs:
- preflight
- sign_windows
environment: ${{ needs.preflight.outputs.publish_env }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.build_ref }}
- name: Download psign-core native artifacts
uses: actions/download-artifact@v8
with:
pattern: psign-core-*
path: dist/native
- name: Setup .NET 8
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.0.x
- name: Resolve PowerShell module signing mode
id: module_signing_mode
shell: pwsh
env:
CODE_SIGNING_KEYVAULT_URL: ${{ secrets.CODE_SIGNING_KEYVAULT_URL }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
CODE_SIGNING_CLIENT_ID: ${{ secrets.CODE_SIGNING_CLIENT_ID }}
CODE_SIGNING_CLIENT_SECRET: ${{ secrets.CODE_SIGNING_CLIENT_SECRET }}
CODE_SIGNING_CERTIFICATE_NAME: ${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}
CODE_SIGNING_TIMESTAMP_SERVER: ${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}
run: |
$dryRun = [System.Boolean]::Parse('${{ needs.preflight.outputs.dry_run }}')
$signDryRun = [System.Boolean]::Parse('${{ github.event.inputs.sign_dry_run }}')
$requiredValues = @(
$env:CODE_SIGNING_KEYVAULT_URL,
$env:AZURE_TENANT_ID,
$env:CODE_SIGNING_CLIENT_ID,
$env:CODE_SIGNING_CLIENT_SECRET,
$env:CODE_SIGNING_CERTIFICATE_NAME,
$env:CODE_SIGNING_TIMESTAMP_SERVER
)
$hasSigningSecrets = $true
foreach ($value in $requiredValues) {
if ([string]::IsNullOrWhiteSpace($value)) {
$hasSigningSecrets = $false
break
}
}
$shouldSign = $hasSigningSecrets -and ((-not $dryRun) -or $signDryRun)
if (-not $dryRun -and -not $hasSigningSecrets) {
throw 'Code signing secrets are required for non-dry-run release jobs.'
}
"should_sign=$($shouldSign.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Host "PowerShell module payload will be signed: $shouldSign"
- name: Package PowerShell module
shell: pwsh
env:
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
SIGN_POWERSHELL_MODULE: ${{ steps.module_signing_mode.outputs.should_sign }}
CODE_SIGNING_KEYVAULT_URL: ${{ secrets.CODE_SIGNING_KEYVAULT_URL }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
CODE_SIGNING_CLIENT_ID: ${{ secrets.CODE_SIGNING_CLIENT_ID }}
CODE_SIGNING_CLIENT_SECRET: ${{ secrets.CODE_SIGNING_CLIENT_SECRET }}
CODE_SIGNING_CERTIFICATE_NAME: ${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}
CODE_SIGNING_TIMESTAMP_SERVER: ${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}
run: |
$tag = $env:RELEASE_TAG
if (-not $tag.StartsWith('v')) {
throw "Release tag must start with 'v'. Actual: $tag"
}
$version = $tag.Substring(1)
New-Item -ItemType Directory -Force -Path dist | Out-Null
$archivePath = Join-Path 'dist' "Devolutions.Psign.$version.zip"
$packageArgs = @{
Configuration = 'Release'
NativeArtifactsRoot = 'dist/native'
SkipNativeBuild = $true
OutputDirectory = 'dist/powershell'
ModuleArchivePath = $archivePath
}
if ([System.Boolean]::Parse($env:SIGN_POWERSHELL_MODULE)) {
$packageArgs.SignModule = $true
$packageArgs.AzureKeyVaultUrl = $env:CODE_SIGNING_KEYVAULT_URL
$packageArgs.AzureKeyVaultCertificate = $env:CODE_SIGNING_CERTIFICATE_NAME
$packageArgs.AzureKeyVaultClientId = $env:CODE_SIGNING_CLIENT_ID
$packageArgs.AzureKeyVaultClientSecret = $env:CODE_SIGNING_CLIENT_SECRET
$packageArgs.AzureKeyVaultTenantId = $env:AZURE_TENANT_ID
$packageArgs.TimestampServer = $env:CODE_SIGNING_TIMESTAMP_SERVER
}
./PowerShell/package.ps1 @packageArgs
- name: Upload PowerShell module package artifact
uses: actions/upload-artifact@v7
with:
name: devolutions-psign-powershell-nupkg
path: dist/powershell/Devolutions.Psign*.nupkg
if-no-files-found: error
- name: Upload PowerShell module artifact
uses: actions/upload-artifact@v7
with:
name: powershell-module
path: dist/Devolutions.Psign.*.zip
if-no-files-found: error
publish:
name: Publish GitHub release
runs-on: ubuntu-latest
needs:
- preflight
- sign_windows
- pack_dotnet_tool
- pack_powershell_module
steps:
- name: Download platform zip artifacts
uses: actions/download-artifact@v8
with:
pattern: psign-tool-*.zip
path: dist
- name: Download dotnet tool NuGet artifact
uses: actions/download-artifact@v8
with:
name: devolutions-psign-tool-nupkg
path: dist
- name: Download PowerShell module NuGet artifact
uses: actions/download-artifact@v8
with:
name: devolutions-psign-powershell-nupkg
path: dist
- name: Download PowerShell module archive artifact
uses: actions/download-artifact@v8
with:
name: powershell-module
path: dist
- name: Generate checksums
shell: pwsh
working-directory: dist
run: |
$releaseAssets = Get-ChildItem -Recurse -File |
Where-Object {
$_.Name -like 'psign-tool-*.zip' -or
$_.Name -like 'Devolutions.Psign.Tool*.nupkg' -or
$_.Name -like 'Devolutions.Psign.*.nupkg' -or
$_.Name -like 'Devolutions.Psign.*.zip'
} |
Sort-Object Name
if (-not $releaseAssets) {
throw 'No release assets found under dist'
}
$lines = $releaseAssets |
Sort-Object Name |
ForEach-Object {
$hash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $($_.Name)"
}
$lines | Set-Content -Path checksums.txt -Encoding utf8
Get-Content -Path checksums.txt
- name: Publish release assets
if: needs.preflight.outputs.dry_run != 'true'
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ needs.preflight.outputs.release_tag }}
RELEASE_TARGET: ${{ github.sha }}
run: |
$assets = Get-ChildItem -Path dist -Recurse -File |
Where-Object {
$_.Name -like 'psign-tool-*.zip' -or
$_.Name -like 'Devolutions.Psign.Tool*.nupkg' -or
$_.Name -like 'Devolutions.Psign.*.nupkg' -or
$_.Name -like 'Devolutions.Psign.*.zip'
} |
Sort-Object Name |
ForEach-Object { $_.FullName }
if (-not $assets) {
throw 'No release assets found under dist'
}
gh release view "$env:RELEASE_TAG" *> $null
if ($LASTEXITCODE -eq 0) {
gh release upload "$env:RELEASE_TAG" @assets "dist/checksums.txt" --clobber
}
else {
$createArgs = @("$env:RELEASE_TAG")
$createArgs += $assets
$createArgs += "dist/checksums.txt"
$createArgs += "--generate-notes"
$createArgs += "--target"
$createArgs += "$env:RELEASE_TARGET"
gh release create @createArgs
}
- name: "Dry Run: skip GitHub release publish"
if: needs.preflight.outputs.dry_run == 'true'
shell: pwsh
run: |
Write-Host 'Dry Run: skipping GitHub release create/upload.'
publish_nuget:
name: Publish NuGet packages
runs-on: ubuntu-latest
needs:
- preflight
- pack_dotnet_tool
if: needs.preflight.outputs.publish_nuget == 'true'
environment: ${{ needs.preflight.outputs.publish_env }}
permissions:
id-token: write
contents: read
steps:
- name: Resolve publish mode
id: publish_mode
shell: pwsh
env:
NUGET_BOT_USERNAME: ${{ secrets.NUGET_BOT_USERNAME }}
run: |
$dryRun = [System.Boolean]::Parse('${{ needs.preflight.outputs.dry_run }}')
$hasLoginUser = -not [string]::IsNullOrWhiteSpace($env:NUGET_BOT_USERNAME)
"dry_run=$($dryRun.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"has_login_user=$($hasLoginUser.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
if (-not $dryRun -and -not $hasLoginUser) {
throw 'NUGET_BOT_USERNAME is required when dry_run is false.'
}
Write-Host "NuGet dry-run mode: $dryRun"
- name: Download dotnet tool NuGet artifact
if: steps.publish_mode.outputs.dry_run == 'true' || steps.publish_mode.outputs.has_login_user == 'true'
uses: actions/download-artifact@v8
with:
name: devolutions-psign-tool-nupkg
path: dist
- name: Setup .NET SDK
if: steps.publish_mode.outputs.dry_run == 'true' || steps.publish_mode.outputs.has_login_user == 'true'
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: NuGet login (OIDC)
if: steps.publish_mode.outputs.dry_run != 'true' && steps.publish_mode.outputs.has_login_user == 'true'
id: nuget-login
uses: NuGet/login@v1
with:
user: ${{ secrets.NUGET_BOT_USERNAME }}
- name: Publish to NuGet.org (RID first, pointer last)
if: steps.publish_mode.outputs.dry_run == 'true' || steps.publish_mode.outputs.has_login_user == 'true'
shell: pwsh
env:
NUGET_SOURCE: https://api.nuget.org/v3/index.json
run: |
$dryRun = [System.Boolean]::Parse('${{ steps.publish_mode.outputs.dry_run }}')
$apiKey = '${{ steps.nuget-login.outputs.NUGET_API_KEY }}'
if ($dryRun) {
$apiKey = 'dry-run-key'
Write-Host 'Dry Run: NuGet push commands will be printed but not executed.'
}
elseif ([string]::IsNullOrWhiteSpace($apiKey)) {
throw 'NuGet login did not return an API key.'
}
$toolPackages = Get-ChildItem -Path dist -Recurse -File -Filter 'Devolutions.Psign.Tool*.nupkg' | Sort-Object Name
if (-not $toolPackages) {
throw 'Missing Devolutions.Psign.Tool package artifacts.'
}
$toolRidPattern = 'Devolutions\.Psign\.Tool\.(win-x64|win-arm64|linux-x64|linux-arm64|osx-x64|osx-arm64|any)\.'
$toolRidPackages = $toolPackages | Where-Object { $_.Name -match $toolRidPattern }
$toolPointerPackages = $toolPackages | Where-Object { $_.Name -notmatch $toolRidPattern }
if (-not $toolRidPackages) {
throw 'Missing Devolutions.Psign.Tool RID package artifacts.'
}
if (-not $toolPointerPackages) {
throw 'Missing Devolutions.Psign.Tool pointer package artifact.'
}
foreach ($package in $toolRidPackages) {
$pushArgs = @(
'nuget', 'push', "$($package.FullName)",
'--api-key', "$apiKey",
'--source', "$env:NUGET_SOURCE",
'--skip-duplicate', '--no-symbols'
)
Write-Host "dotnet $($pushArgs -join ' ')"
if (-not $dryRun) {
& dotnet @pushArgs
}
}
foreach ($package in $toolPointerPackages) {
$pushArgs = @(
'nuget', 'push', "$($package.FullName)",
'--api-key', "$apiKey",
'--source', "$env:NUGET_SOURCE",
'--skip-duplicate', '--no-symbols'
)
Write-Host "dotnet $($pushArgs -join ' ')"
if (-not $dryRun) {
& dotnet @pushArgs
}
}
publish_psgallery:
name: Publish PowerShell Gallery
runs-on: ubuntu-latest
needs:
- preflight
- pack_powershell_module
- publish
if: needs.preflight.outputs.dry_run != 'true' && needs.preflight.outputs.publish_psgallery == 'true'
environment: ${{ needs.preflight.outputs.publish_env }}
env:
PSGALLERY_NUGET_API_KEY: ${{ secrets.PSGALLERY_NUGET_API_KEY }}
permissions:
contents: read
steps:
- name: Download PowerShell module artifact
uses: actions/download-artifact@v8
with:
name: powershell-module
path: dist/powershell-module
- name: Publish Devolutions.Psign to PowerShell Gallery
shell: pwsh
run: |
if ([string]::IsNullOrWhiteSpace($env:PSGALLERY_NUGET_API_KEY)) {
throw 'PSGALLERY_NUGET_API_KEY is not configured.'
}
$archive = Get-ChildItem -Path dist/powershell-module -Filter 'Devolutions.Psign.*.zip' |
Sort-Object LastWriteTimeUtc -Descending |
Select-Object -First 1
if ($null -eq $archive) {
throw 'PowerShell module archive was not downloaded.'
}
$expandedRoot = Join-Path 'dist' 'powershell-gallery'
Expand-Archive -Path $archive.FullName -DestinationPath $expandedRoot -Force
$modulePath = (Resolve-Path (Join-Path $expandedRoot 'Devolutions.Psign')).Path
Test-ModuleManifest -Path (Join-Path $modulePath 'Devolutions.Psign.psd1') | Out-Null
Publish-Module -Path $modulePath -NuGetApiKey $env:PSGALLERY_NUGET_API_KEY -Verbose