diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b55c491..1f65154 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,6 +79,11 @@ jobs: MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} MACOS_SIGN_TEAM_ID: ${{ vars.MACOS_SIGN_TEAM_ID }} run: python scripts/release.py settings + - name: Install the pinned Artifact Signing client + timeout-minutes: 5 + if: steps.saved.outputs.restored != 'true' + shell: pwsh + run: ./scripts/install-signing-client.ps1 - name: Sign in to Azure with OIDC if: steps.saved.outputs.restored != 'true' uses: azure/login@8216e11d8cd9b42fe925c852af8e76311ff067ac # v2 @@ -86,7 +91,7 @@ jobs: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - name: Install the Artifact Signing client and sign the installers + - name: Sign the Windows installers if: steps.saved.outputs.restored != 'true' shell: pwsh env: @@ -94,7 +99,6 @@ jobs: ARTIFACT_SIGNING_ACCOUNT_NAME: ${{ vars.ARTIFACT_SIGNING_ACCOUNT_NAME }} ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ vars.ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }} run: | - Install-Module -Name ArtifactSigning -RequiredVersion 0.1.8 -Scope CurrentUser -Force -Repository PSGallery python scripts/release.py prepare-windows if ($LASTEXITCODE -ne 0) { throw 'Installer preparation failed.' } Get-ChildItem packaging/*.ps1 | ForEach-Object { ./scripts/sign-windows.ps1 -Path $_.FullName } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a4043c..9abea9f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,16 @@ on: permissions: contents: read jobs: + signing-client: + runs-on: windows-2025 + timeout-minutes: 15 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - name: Recover the signing client from a missing package repository + shell: pwsh + run: ./scripts/test-signing-client.ps1 saved-release: if: github.event_name == 'workflow_dispatch' && inputs.release_run_id != '' permissions: diff --git a/scripts/install-signing-client.ps1 b/scripts/install-signing-client.ps1 new file mode 100644 index 0000000..f922a97 --- /dev/null +++ b/scripts/install-signing-client.ps1 @@ -0,0 +1,31 @@ +#Requires -Version 7.2 +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +for ($attempt = 1; $attempt -le 3; $attempt++) { + try { + $gallery = @(Get-PSRepository -ErrorAction Stop | Where-Object Name -eq 'PSGallery') + if ($gallery.Count -eq 0) { + Register-PSRepository -Default -ErrorAction Stop + $gallery = @(Get-PSRepository -Name PSGallery -ErrorAction Stop) + } + if ($gallery.Count -ne 1 -or $gallery[0].SourceLocation.TrimEnd('/') -cne 'https://www.powershellgallery.com/api/v2') { + throw [System.Security.SecurityException]::new('PSGallery must use the official HTTPS package source.') + } + Install-Module -Name ArtifactSigning -RequiredVersion 0.1.8 -Scope CurrentUser -Force -Repository PSGallery -ErrorAction Stop + $module = Import-Module ArtifactSigning -RequiredVersion 0.1.8 -Force -PassThru -ErrorAction Stop + if ($module.Version -ne [version]'0.1.8' -or -not $module.ExportedCommands.ContainsKey('Invoke-ArtifactSigning')) { + throw 'The pinned Artifact Signing client did not load.' + } + Write-Output 'ArtifactSigning 0.1.8 is installed and ready.' + # PackageManagement can leave a native NuGet error code after recovery. + # Report success only after the exact module and command have loaded. + exit 0 + } catch [System.Security.SecurityException] { + throw + } catch { + if ($attempt -eq 3) { throw } + Write-Warning "Artifact Signing setup attempt $attempt failed: $($_.Exception.Message). Retrying." + Start-Sleep -Seconds (5 * $attempt) + } +} diff --git a/scripts/test-signing-client.ps1 b/scripts/test-signing-client.ps1 new file mode 100644 index 0000000..3f26a30 --- /dev/null +++ b/scripts/test-signing-client.ps1 @@ -0,0 +1,49 @@ +#Requires -Version 7.2 +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +if ($env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_OS -ne 'Windows') { + throw 'Run this test only on a disposable Windows Actions runner.' +} +$setup = Join-Path $PSScriptRoot 'install-signing-client.ps1' + +# Reproduce the failed release with no registered PSGallery, then use the real service. +if (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue) { + Unregister-PSRepository -Name PSGallery -ErrorAction Stop +} +if (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue) { throw 'PSGallery was not removed for the test.' } +$global:LASTEXITCODE = 23 +& $setup +if ($LASTEXITCODE -ne 0) { throw 'Successful setup kept a stale native process exit code.' } +$module = Get-Module -ListAvailable ArtifactSigning | Where-Object Version -eq ([version]'0.1.8') +if (-not $module) { throw 'The pinned module was not installed.' } +& $setup +Write-Output 'Real missing-repository recovery, module import, and repeated setup passed.' + +# Check bounded retries without waiting or making extra network requests. +& { + $state = @{ Attempts=0; Sleeps=0; Failures=1; Source='https://www.powershellgallery.com/api/v2' } + function Get-PSRepository { + [CmdletBinding()] param([string]$Name) + [pscustomobject]@{ Name='PSGallery'; SourceLocation=$state.Source } + } + function Install-Module { + [CmdletBinding()] param($Name, $RequiredVersion, $Scope, [switch]$Force, $Repository) + if ($Name -ne 'ArtifactSigning' -or $RequiredVersion -ne '0.1.8' -or $Scope -ne 'CurrentUser' -or $Repository -ne 'PSGallery') { + throw 'The installer changed its pinned module or repository.' + } + $state.Attempts++ + if ($state.Attempts -le $state.Failures) { throw 'Simulated package service failure.' } + } + function Start-Sleep { param($Seconds) $state.Sleeps++ } + & $setup + if ($state.Attempts -ne 2 -or $state.Sleeps -ne 1) { throw 'Transient failure recovery failed.' } + $state.Attempts=0; $state.Sleeps=0; $state.Failures=3 + $failed=$false + try { & $setup } catch { $failed=$true } + if (-not $failed -or $state.Attempts -ne 3 -or $state.Sleeps -ne 2) { throw 'Persistent failures did not stop after three attempts.' } + $state.Attempts=0; $state.Sleeps=0; $state.Source='https://packages.example.invalid/api/v2' + $failed=$false + try { & $setup } catch [System.Security.SecurityException] { $failed=$true } + if (-not $failed -or $state.Attempts -ne 0 -or $state.Sleeps -ne 0) { throw 'An unexpected package source was not rejected.' } +} +Write-Output 'Retry limits and package source checks passed.'