1515 required : false
1616 default : true
1717 type : boolean
18+ github-env :
19+ description : GitHub environment selection.
20+ required : true
21+ type : choice
22+ options :
23+ - auto
24+ - test
25+ - prod
26+ default : auto
1827 publish-nuget :
1928 description : Publish packages to NuGet.org during a manual release.
2029 required : false
2130 default : false
2231 type : boolean
23- publish-powershell-gallery :
32+ publish-psgallery :
2433 description : Publish Devolutions.Pinget.Client to PowerShell Gallery during a manual release.
2534 required : false
2635 default : false
4150 version : ${{ steps.info.outputs.version }}
4251 tag-name : ${{ steps.info.outputs.tag-name }}
4352 dry-run : ${{ steps.info.outputs.dry-run }}
53+ package-env : ${{ steps.info.outputs.package-env }}
4454 publish-nuget : ${{ steps.info.outputs.publish-nuget }}
45- publish-powershell-gallery : ${{ steps.info.outputs.publish-powershell-gallery }}
55+ publish-psgallery : ${{ steps.info.outputs.publish-psgallery }}
4656
4757 steps :
4858 - name : Checkout
@@ -58,17 +68,49 @@ jobs:
5868 $DryRun = $false
5969 $PublishNuget = $true
6070 $PublishPowerShellGallery = $true
71+ $RequestedEnvironment = 'auto'
6172
6273 if ($IsWorkflowDispatch) {
6374 try { $DryRun = [System.Boolean]::Parse('${{ inputs['dry-run'] }}') } catch { $DryRun = $true }
6475 try { $PublishNuget = [System.Boolean]::Parse('${{ inputs['publish-nuget'] }}') } catch { $PublishNuget = $false }
65- try { $PublishPowerShellGallery = [System.Boolean]::Parse('${{ inputs['publish-powershell-gallery'] }}') } catch { $PublishPowerShellGallery = $false }
76+ try { $PublishPowerShellGallery = [System.Boolean]::Parse('${{ inputs['publish-psgallery'] }}') } catch { $PublishPowerShellGallery = $false }
77+
78+ $RequestedEnvironment = '${{ inputs['github-env'] }}'.Trim().ToLowerInvariant()
79+ if ([string]::IsNullOrWhiteSpace($RequestedEnvironment)) {
80+ $RequestedEnvironment = 'auto'
81+ }
82+ }
83+
84+ switch ($RequestedEnvironment) {
85+ 'auto' {
86+ $PackageEnv = if ($IsMasterBranch) {
87+ 'publish-prod'
88+ } else {
89+ 'publish-test'
90+ }
91+ }
92+ 'test' {
93+ $PackageEnv = 'publish-test'
94+ }
95+ 'prod' {
96+ $PackageEnv = 'publish-prod'
97+ }
98+ default {
99+ throw "Unsupported github-env value: $RequestedEnvironment (expected auto, test, or prod)"
100+ }
101+ }
102+
103+ if ($RequestedEnvironment -eq 'test' -and ($IsMasterBranch -or (-not $DryRun -and ($PublishNuget -or $PublishPowerShellGallery)))) {
104+ $PackageEnv = 'publish-prod'
105+ Write-Host '::notice::github-env=test was promoted to publish-prod'
66106 }
67107
68108 if (-not $IsMasterBranch) {
69- $DryRun = $true
70- $PublishNuget = $false
71- $PublishPowerShellGallery = $false
109+ if ($PackageEnv -ne 'publish-prod') {
110+ $DryRun = $true
111+ $PublishNuget = $false
112+ $PublishPowerShellGallery = $false
113+ }
72114 }
73115
74116 if ($DryRun) {
@@ -106,19 +148,23 @@ jobs:
106148 "version=$Version" >> $Env:GITHUB_OUTPUT
107149 "tag-name=$TagName" >> $Env:GITHUB_OUTPUT
108150 "dry-run=$($DryRun.ToString().ToLower())" >> $Env:GITHUB_OUTPUT
151+ "package-env=$PackageEnv" >> $Env:GITHUB_OUTPUT
109152 "publish-nuget=$($PublishNuget.ToString().ToLower())" >> $Env:GITHUB_OUTPUT
110- "publish-powershell-gallery =$($PublishPowerShellGallery.ToString().ToLower())" >> $Env:GITHUB_OUTPUT
153+ "publish-psgallery =$($PublishPowerShellGallery.ToString().ToLower())" >> $Env:GITHUB_OUTPUT
111154
112155 Write-Host "::notice::Version: $Version"
113156 Write-Host "::notice::Tag: $TagName"
114157 Write-Host "::notice::DryRun: $DryRun"
158+ Write-Host "::notice::Environment: $PackageEnv"
159+ Write-Host "::notice::GitHubEnvMode: $RequestedEnvironment"
115160 Write-Host "::notice::PublishNuget: $PublishNuget"
116161 Write-Host "::notice::PublishPowerShellGallery: $PublishPowerShellGallery"
117162
118163 rust-artifacts :
119164 name : Rust Artifacts (${{ matrix.name }})
120165 runs-on : ${{ matrix.os }}
121166 needs : preflight
167+ environment : ${{ needs.preflight.outputs.package-env }}
122168 strategy :
123169 fail-fast : false
124170 matrix :
@@ -184,6 +230,72 @@ jobs:
184230 run : |
185231 cargo build --release --package pinget-cli --bin pinget --manifest-path rust/Cargo.toml --target "${{ matrix.target }}"
186232
233+ - name : Install code signing tools
234+ if : contains(matrix.target, 'windows-msvc')
235+ shell : pwsh
236+ run : |
237+ dotnet tool install --global AzureSignTool
238+
239+ $TestCertsUrl = "https://raw.githubusercontent.com/Devolutions/devolutions-authenticode/master/data/certs"
240+ Invoke-WebRequest -Uri "$TestCertsUrl/authenticode-test-ca.crt" -OutFile ".\authenticode-test-ca.crt"
241+ Import-Certificate -FilePath ".\authenticode-test-ca.crt" -CertStoreLocation "cert:\LocalMachine\Root"
242+ Remove-Item ".\authenticode-test-ca.crt" -ErrorAction SilentlyContinue | Out-Null
243+
244+ - name : Code sign Rust binary
245+ if : contains(matrix.target, 'windows-msvc')
246+ shell : pwsh
247+ env :
248+ AZURE_TENANT_ID : ${{ secrets.AZURE_TENANT_ID }}
249+ CODE_SIGNING_CERTIFICATE_NAME : ${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}
250+ CODE_SIGNING_CLIENT_ID : ${{ secrets.CODE_SIGNING_CLIENT_ID }}
251+ CODE_SIGNING_CLIENT_SECRET : ${{ secrets.CODE_SIGNING_CLIENT_SECRET }}
252+ CODE_SIGNING_KEYVAULT_URL : ${{ secrets.CODE_SIGNING_KEYVAULT_URL }}
253+ CODE_SIGNING_TIMESTAMP_SERVER : ${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}
254+ TARGET : ${{ matrix.target }}
255+ PINGET_BIN_NAME : ${{ matrix.binary_name }}
256+ run : |
257+ $PackageEnv = '${{ needs.preflight.outputs.package-env }}'
258+ $DryRun = [System.Boolean]::Parse('${{ needs.preflight.outputs.dry-run }}')
259+ $RequiredVariables = @(
260+ 'AZURE_TENANT_ID',
261+ 'CODE_SIGNING_CERTIFICATE_NAME',
262+ 'CODE_SIGNING_CLIENT_ID',
263+ 'CODE_SIGNING_CLIENT_SECRET',
264+ 'CODE_SIGNING_KEYVAULT_URL',
265+ 'CODE_SIGNING_TIMESTAMP_SERVER'
266+ )
267+ $MissingVariables = @($RequiredVariables | Where-Object {
268+ [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_))
269+ })
270+
271+ if ($MissingVariables.Count -gt 0) {
272+ $Message = "Code signing configuration missing: $($MissingVariables -join ', ')"
273+ if ($PackageEnv -eq 'publish-prod' -and -not $DryRun) {
274+ throw $Message
275+ }
276+
277+ Write-Host "$Message; skipping code signing."
278+ return
279+ }
280+
281+ $BinaryPath = [System.IO.Path]::Combine('rust', 'target', $env:TARGET, 'release', $env:PINGET_BIN_NAME)
282+ if (-not (Test-Path -Path $BinaryPath)) {
283+ throw "Binary not found: $BinaryPath"
284+ }
285+
286+ AzureSignTool sign `
287+ -kvt $env:AZURE_TENANT_ID `
288+ -kvu $env:CODE_SIGNING_KEYVAULT_URL `
289+ -kvi $env:CODE_SIGNING_CLIENT_ID `
290+ -kvs $env:CODE_SIGNING_CLIENT_SECRET `
291+ -kvc $env:CODE_SIGNING_CERTIFICATE_NAME `
292+ -tr $env:CODE_SIGNING_TIMESTAMP_SERVER `
293+ -v `
294+ $BinaryPath
295+ if ($LASTEXITCODE -ne 0) {
296+ throw "Code signing failed for $BinaryPath."
297+ }
298+
187299 - name : Package artifacts
188300 shell : pwsh
189301 env :
@@ -232,6 +344,7 @@ jobs:
232344 name : C# Packages
233345 runs-on : windows-latest
234346 needs : preflight
347+ environment : ${{ needs.preflight.outputs.package-env }}
235348
236349 steps :
237350 - name : Checkout
@@ -261,6 +374,87 @@ jobs:
261374 shell : pwsh
262375 run : pwsh -NoLogo -NoProfile -File (Resolve-Path 'dotnet/tests/RunTests.ps1')
263376
377+ - name : Install code signing tools
378+ shell : pwsh
379+ run : |
380+ dotnet tool install --global AzureSignTool
381+
382+ $TestCertsUrl = "https://raw.githubusercontent.com/Devolutions/devolutions-authenticode/master/data/certs"
383+ Invoke-WebRequest -Uri "$TestCertsUrl/authenticode-test-ca.crt" -OutFile ".\authenticode-test-ca.crt"
384+ Import-Certificate -FilePath ".\authenticode-test-ca.crt" -CertStoreLocation "cert:\LocalMachine\Root"
385+ Remove-Item ".\authenticode-test-ca.crt" -ErrorAction SilentlyContinue | Out-Null
386+
387+ - name : Code sign C# binaries
388+ shell : pwsh
389+ env :
390+ AZURE_TENANT_ID : ${{ secrets.AZURE_TENANT_ID }}
391+ CODE_SIGNING_CERTIFICATE_NAME : ${{ secrets.CODE_SIGNING_CERTIFICATE_NAME }}
392+ CODE_SIGNING_CLIENT_ID : ${{ secrets.CODE_SIGNING_CLIENT_ID }}
393+ CODE_SIGNING_CLIENT_SECRET : ${{ secrets.CODE_SIGNING_CLIENT_SECRET }}
394+ CODE_SIGNING_KEYVAULT_URL : ${{ secrets.CODE_SIGNING_KEYVAULT_URL }}
395+ CODE_SIGNING_TIMESTAMP_SERVER : ${{ vars.CODE_SIGNING_TIMESTAMP_SERVER }}
396+ run : |
397+ $PackageEnv = '${{ needs.preflight.outputs.package-env }}'
398+ $DryRun = [System.Boolean]::Parse('${{ needs.preflight.outputs.dry-run }}')
399+ $RequiredVariables = @(
400+ 'AZURE_TENANT_ID',
401+ 'CODE_SIGNING_CERTIFICATE_NAME',
402+ 'CODE_SIGNING_CLIENT_ID',
403+ 'CODE_SIGNING_CLIENT_SECRET',
404+ 'CODE_SIGNING_KEYVAULT_URL',
405+ 'CODE_SIGNING_TIMESTAMP_SERVER'
406+ )
407+ $MissingVariables = @($RequiredVariables | Where-Object {
408+ [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_))
409+ })
410+
411+ if ($MissingVariables.Count -gt 0) {
412+ $Message = "Code signing configuration missing: $($MissingVariables -join ', ')"
413+ if ($PackageEnv -eq 'publish-prod' -and -not $DryRun) {
414+ throw $Message
415+ }
416+
417+ Write-Host "$Message; skipping code signing."
418+ return
419+ }
420+
421+ $SignParams = @(
422+ 'sign',
423+ '-kvt', $env:AZURE_TENANT_ID,
424+ '-kvu', $env:CODE_SIGNING_KEYVAULT_URL,
425+ '-kvi', $env:CODE_SIGNING_CLIENT_ID,
426+ '-kvs', $env:CODE_SIGNING_CLIENT_SECRET,
427+ '-kvc', $env:CODE_SIGNING_CERTIFICATE_NAME,
428+ '-tr', $env:CODE_SIGNING_TIMESTAMP_SERVER,
429+ '-v'
430+ )
431+
432+ $BinaryRoots = @(
433+ 'dotnet/src/Devolutions.Pinget.Core/bin/Release',
434+ 'dotnet/src/Devolutions.Pinget.PowerShell.Engine/bin/Release',
435+ 'dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/bin/Release'
436+ )
437+ $Binaries = @(Get-ChildItem -Path $BinaryRoots -Recurse -File -Include '*.dll','*.exe' | Where-Object {
438+ $_.FullName -like '*\bin\Release\*' -and $_.FullName -notmatch '\\ref\\'
439+ })
440+
441+ if ($Binaries.Count -eq 0) {
442+ throw 'No C# binaries were found for code signing.'
443+ }
444+
445+ foreach ($Binary in $Binaries) {
446+ $Signature = Get-AuthenticodeSignature -FilePath $Binary.FullName
447+ if ($Signature.Status -eq 'Valid') {
448+ Write-Host "Code signing skipped for already-signed binary: $($Binary.FullName)"
449+ continue
450+ }
451+
452+ AzureSignTool @SignParams $Binary.FullName
453+ if ($LASTEXITCODE -ne 0) {
454+ throw "Code signing failed for $($Binary.FullName)."
455+ }
456+ }
457+
264458 - name : Pack NuGet packages
265459 shell : pwsh
266460 env :
@@ -352,8 +546,10 @@ jobs:
352546 publish-nuget :
353547 name : Publish NuGet
354548 runs-on : ubuntu-latest
355- env :
356- NUGET_API_KEY : ${{ secrets.NUGET_API_KEY }}
549+ environment : ${{ needs.preflight.outputs.package-env }}
550+ permissions :
551+ contents : read
552+ id-token : write
357553 needs :
358554 - preflight
359555 - csharp-packages
@@ -367,30 +563,53 @@ jobs:
367563 with :
368564 dotnet-version : 10.0.x
369565
566+ - name : NuGet login (OIDC)
567+ id : nuget-login
568+ uses : NuGet/login@v1
569+ with :
570+ user : ${{ secrets.NUGET_BOT_USERNAME }}
571+
370572 - name : Download package artifacts
371573 uses : actions/download-artifact@v8
372574 with :
373575 name : csharp-nuget
374576 path : dist/nuget
375577
376578 - name : Push packages to NuGet.org
377- if : env.NUGET_API_KEY != ''
378- shell : bash
579+ shell : pwsh
379580 run : |
380- dotnet nuget push dist/nuget/*.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate
381- dotnet nuget push dist/nuget/*.snupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate
581+ $Packages = @(Get-ChildItem -Path dist/nuget -Filter '*.nupkg')
582+ if ($Packages.Count -eq 0) {
583+ throw 'No NuGet packages were downloaded.'
584+ }
382585
383- publish-powershell-gallery :
586+ foreach ($Package in $Packages) {
587+ dotnet nuget push $Package.FullName --api-key '${{ steps.nuget-login.outputs.NUGET_API_KEY }}' --source https://api.nuget.org/v3/index.json --skip-duplicate
588+ if ($LASTEXITCODE -ne 0) {
589+ throw "NuGet package publishing failed for $($Package.FullName)."
590+ }
591+ }
592+
593+ $SymbolPackages = @(Get-ChildItem -Path dist/nuget -Filter '*.snupkg')
594+ foreach ($SymbolPackage in $SymbolPackages) {
595+ dotnet nuget push $SymbolPackage.FullName --api-key '${{ steps.nuget-login.outputs.NUGET_API_KEY }}' --source https://api.nuget.org/v3/index.json --skip-duplicate
596+ if ($LASTEXITCODE -ne 0) {
597+ throw "NuGet symbol package publishing failed for $($SymbolPackage.FullName)."
598+ }
599+ }
600+
601+ publish-psgallery :
384602 name : Publish PowerShell Gallery
385603 runs-on : ubuntu-latest
604+ environment : ${{ needs.preflight.outputs.package-env }}
386605 env :
387- PS_GALLERY_API_KEY : ${{ secrets.PS_GALLERY_API_KEY }}
606+ PSGALLERY_NUGET_API_KEY : ${{ secrets.PSGALLERY_NUGET_API_KEY }}
388607 needs :
389608 - preflight
390609 - csharp-packages
391610 - create-tag
392611 - github-release
393- if : ${{ fromJSON(needs.preflight.outputs.dry-run) == false && fromJSON(needs.preflight.outputs.publish-powershell-gallery ) == true }}
612+ if : ${{ fromJSON(needs.preflight.outputs.dry-run) == false && fromJSON(needs.preflight.outputs.publish-psgallery ) == true }}
394613
395614 steps :
396615 - name : Download PowerShell module artifact
@@ -400,9 +619,12 @@ jobs:
400619 path : dist/powershell-module
401620
402621 - name : Publish Devolutions.Pinget.Client to PowerShell Gallery
403- if : env.PS_GALLERY_API_KEY != ''
404622 shell : pwsh
405623 run : |
624+ if ([string]::IsNullOrWhiteSpace($env:PSGALLERY_NUGET_API_KEY)) {
625+ throw 'PSGALLERY_NUGET_API_KEY is not configured.'
626+ }
627+
406628 $archive = Get-ChildItem -Path dist/powershell-module -Filter 'Devolutions.Pinget.Client.*.zip' | Select-Object -First 1
407629 if ($null -eq $archive) {
408630 throw 'PowerShell module archive was not downloaded.'
@@ -413,4 +635,4 @@ jobs:
413635 $modulePath = (Resolve-Path (Join-Path $expandedRoot 'Devolutions.Pinget.Client')).Path
414636
415637 Test-ModuleManifest -Path (Join-Path $modulePath 'Devolutions.Pinget.Client.psd1') | Out-Null
416- Publish-Module -Path $modulePath -NuGetApiKey $env:PS_GALLERY_API_KEY -Verbose
638+ Publish-Module -Path $modulePath -NuGetApiKey $env:PSGALLERY_NUGET_API_KEY -Verbose
0 commit comments