Repository navigation
release #33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |