diff --git a/.github/actions/Publish-PSModule/src/publish.ps1 b/.github/actions/Publish-PSModule/src/publish.ps1 index 24f72289..51095015 100644 --- a/.github/actions/Publish-PSModule/src/publish.ps1 +++ b/.github/actions/Publish-PSModule/src/publish.ps1 @@ -145,7 +145,18 @@ LogGroup 'Publish to PSGallery' { if ($whatIf) { Write-Host "Publish-PSResource -Path $modulePath -Repository PSGallery -ApiKey ***" } else { - $publishedPackage = Find-PSResource -Name $name -Version $publishPSVersion -Repository PSGallery -ErrorAction Stop + # A version that is not on the Gallery is the expected state for a new release, but PSResourceGet + # reports it as a PackageNotFound error, which -ErrorAction Stop turns into a throw. Only that error + # may be treated as 'not published'; any other failure (for example a Gallery outage) must stay fatal, + # otherwise a version that already exists would be re-published and fail the upload with a conflict. + $publishedPackage = $null + try { + $publishedPackage = Find-PSResource -Name $name -Version $publishPSVersion -Repository PSGallery -ErrorAction Stop + } catch { + if ($_.FullyQualifiedErrorId -notlike 'PackageNotFound,*') { throw } + Write-Host "$name $publishPSVersion is not on the PowerShell Gallery yet." + } + if ($publishedPackage) { Write-Host ( "::notice title=♻️ Resuming Gallery-only publication::$name $publishPSVersion is already " + diff --git a/.github/actions/Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 b/.github/actions/Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 index 57a9ecaa..859e0539 100644 --- a/.github/actions/Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 +++ b/.github/actions/Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 @@ -29,9 +29,13 @@ AfterAll { foreach ($name in $script:environmentVariableNames) { [System.Environment]::SetEnvironmentVariable($name, $script:originalEnvironment[$name]) } - Remove-Item -Path function:global:Find-PSResource -ErrorAction SilentlyContinue - Remove-Item -Path function:global:Publish-PSResource -ErrorAction SilentlyContinue - Remove-Item -Path function:global:Resolve-PSModuleDependency -ErrorAction SilentlyContinue + # Set-Item accepts 'function:global:X' and creates a function named 'X' in the global scope, but + # Remove-Item and Get-Item do not resolve that same path back to it, and fail silently rather than + # erroring. Removing by name is what actually deletes the shims; leaving them behind would shadow the + # real cmdlets for every test file that runs later in the session. + Remove-Item -Path 'Function:\Find-PSResource' -ErrorAction SilentlyContinue + Remove-Item -Path 'Function:\Publish-PSResource' -ErrorAction SilentlyContinue + Remove-Item -Path 'Function:\Resolve-PSModuleDependency' -ErrorAction SilentlyContinue } Describe 'Publish-PSModule recovery' { @@ -68,20 +72,64 @@ Describe 'Publish-PSModule recovery' { $env:PSMODULE_PUBLISH_PSMODULE_INPUT_PSGALLERY_API_KEY = 'test-key' $env:PSMODULE_PUBLISH_PSMODULE_INPUT_PullRequest = '' $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf = 'false' - $script:publishInvoked = $false + # The publish script runs in its own scope, so a $script: flag set inside a shim never reaches the + # test. A hashtable captured by GetNewClosure() is shared by reference and records the call reliably. + # The closure captures local variables only, so $calls must be local here, not $script:-qualified. + $calls = @{ PublishInvoked = $false } + $script:calls = $calls Set-Item -Path function:global:Resolve-PSModuleDependency -Value {} Set-Item -Path function:global:Find-PSResource -Value { [PSCustomObject]@{ Name = 'TestModule'; Version = '1.2.4' } } Set-Item -Path function:global:Publish-PSResource -Value { - $script:publishInvoked = $true - } + $calls.PublishInvoked = $true + }.GetNewClosure() } It 'skips Gallery publication when the resolved version already exists' { { & $script:publishScriptPath } | Should -Not -Throw - $script:publishInvoked | Should -BeFalse + $script:calls.PublishInvoked | Should -BeFalse + } + + It 'publishes when the resolved version is not in the Gallery' { + Set-Item -Path function:global:Find-PSResource -Value { + [CmdletBinding()] + param( + [string] $Name, + [string] $Version, + [string] $Repository + ) + + # Mirrors how PSResourceGet reports an absent version: an error with the PackageNotFound error ID + # that honours -ErrorAction, so the shim reacts to -ErrorAction the same way the real cmdlet does. + Write-Error -Message "Package with name '$Name', version '$Version' could not be found in repository '$Repository'." ` + -ErrorId 'PackageNotFound' -Category ObjectNotFound -TargetObject $Name + } + + { & $script:publishScriptPath } | Should -Not -Throw + + $script:calls.PublishInvoked | Should -BeTrue + } + + It 'fails without publishing when the Gallery lookup errors for another reason' { + Set-Item -Path function:global:Find-PSResource -Value { + [CmdletBinding()] + param( + [string] $Name, + [string] $Version, + [string] $Repository + ) + + # A transient Gallery failure carries a different error ID and must not be mistaken for + # 'version not published', otherwise an already-published version would be re-uploaded. + Write-Error -Message "Failed to find '$Name' '$Version' in repository '$Repository': Service Unavailable" ` + -ErrorId 'HttpRequestCallFailure' -Category ResourceUnavailable -TargetObject $Name + } + + { & $script:publishScriptPath } | Should -Throw + + $script:calls.PublishInvoked | Should -BeFalse } } diff --git a/.github/actions/Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 b/.github/actions/Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 index 5af24d8c..d7df20e8 100644 --- a/.github/actions/Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 +++ b/.github/actions/Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 @@ -35,8 +35,12 @@ AfterAll { foreach ($name in $script:environmentVariableNames) { [System.Environment]::SetEnvironmentVariable($name, $script:originalEnvironment[$name]) } - Remove-Item -Path function:global:gh -ErrorAction SilentlyContinue - Remove-Item -Path function:global:git -ErrorAction SilentlyContinue + # Set-Item accepts 'function:global:X' and creates a function named 'X' in the global scope, but + # Remove-Item and Get-Item do not resolve that same path back to it, and fail silently rather than + # erroring. Removing by name is what actually deletes the shims; leaving them behind would shadow the + # real commands for every test file that runs later in the session. + Remove-Item -Path 'Function:\gh' -ErrorAction SilentlyContinue + Remove-Item -Path 'Function:\git' -ErrorAction SilentlyContinue } Describe 'Release-PSModule WhatIf' {