From 1ea1be894e448a856816a6f372db884fa2cb74d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:01:59 +0000 Subject: [PATCH 1/6] Initial plan From 41f022c78e25cdb67acc0be6344ba83649bedce6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:07:31 +0000 Subject: [PATCH 2/6] Fix publishing new Gallery versions Co-authored-by: MariusStorhaug <17722253+MariusStorhaug@users.noreply.github.com> --- .../actions/Publish-PSModule/src/publish.ps1 | 2 +- .../tests/Publish-PSModule.Recovery.Tests.ps1 | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/actions/Publish-PSModule/src/publish.ps1 b/.github/actions/Publish-PSModule/src/publish.ps1 index 24f72289..d71e80bc 100644 --- a/.github/actions/Publish-PSModule/src/publish.ps1 +++ b/.github/actions/Publish-PSModule/src/publish.ps1 @@ -145,7 +145,7 @@ 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 + $publishedPackage = Find-PSResource -Name $name -Version $publishPSVersion -Repository PSGallery -ErrorAction SilentlyContinue 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..bb16c231 100644 --- a/.github/actions/Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 +++ b/.github/actions/Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 @@ -84,4 +84,25 @@ Describe 'Publish-PSModule recovery' { $script:publishInvoked | Should -BeFalse } + + It 'publishes when the resolved version is not in the Gallery' { + $publishMarkerPath = Join-Path -Path $script:workspacePath -ChildPath 'publish-invoked' + Set-Item -Path function:global:Find-PSResource -Value { + [CmdletBinding()] + param( + [string] $Name, + [string] $Version, + [string] $Repository + ) + + Write-Error "Package with name '$Name', version '$Version' could not be found in repository '$Repository'." + } + Set-Item -Path function:global:Publish-PSResource -Value { + $null = New-Item -Path (Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath 'publish-invoked') -ItemType File -Force + } + + { & $script:publishScriptPath } | Should -Not -Throw + + (Test-Path -Path $publishMarkerPath) | Should -BeTrue + } } From 7019cd9a5b8d5d8e443a5140fa02da28646d241d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Tue, 1 Sep 2026 22:45:44 +0200 Subject: [PATCH 3/6] Stop test command shims leaking into the global scope Remove-Item does not accept a 'global:' scope qualifier in a provider path, so the AfterAll cleanup silently failed and left the Find-PSResource, Publish-PSResource, Resolve-PSModuleDependency, gh, and git shims in the global scope. Later test files then resolved the shims instead of the real commands, which failed Get-NextPrereleaseNumber with 'A parameter cannot be found that matches parameter name Prerelease'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tests/Publish-PSModule.Recovery.Tests.ps1 | 8 +++++--- .../tests/Release-PSModule.WhatIf.Tests.ps1 | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) 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 bb16c231..fa7c50ea 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,11 @@ 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 + # The 'global:' scope qualifier is not a valid provider path, so it must be omitted here. + # Otherwise the shims leak into the global scope and shadow the real cmdlets in later test files. + 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' { 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..076c7688 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,10 @@ 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 + # The 'global:' scope qualifier is not a valid provider path, so it must be omitted here. + # Otherwise the shims leak into the global scope and shadow the real commands in later test files. + Remove-Item -Path 'Function:\gh' -ErrorAction SilentlyContinue + Remove-Item -Path 'Function:\git' -ErrorAction SilentlyContinue } Describe 'Release-PSModule WhatIf' { From 38ac1f160ddcb7f4de5b8cb03a8fc956a4e42f50 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 2 Sep 2026 06:50:35 +0200 Subject: [PATCH 4/6] Narrow the Gallery existence probe to PackageNotFound Addresses review feedback on #528: -ErrorAction SilentlyContinue swallowed every Find-PSResource failure, not just 'version not found'. During a Gallery outage the probe returned null, publish was attempted anyway, and an already-published version failed the upload with a conflict - losing the resume behaviour the probe exists to provide. The probe now runs with -ErrorAction Stop and only treats the PackageNotFound error ID as 'not yet published'. Any other error stays fatal. Also fixed the test harness, which could not observe whether publish ran: - The $script:publishInvoked flag was never set, because the publish script runs in its own scope. Both existing assertions were vacuous. Replaced with a hashtable captured by GetNewClosure(), shared by reference. - The marker-file variant depended on $env:GITHUB_WORKSPACE, which is process-wide and races between parallel Pester runspaces. - The not-found shim used ThrowTerminatingError, which ignores -ErrorAction and so could not distinguish the fix from the bug. It now uses Write-Error with the real PackageNotFound error ID. Added a case asserting a non-PackageNotFound lookup failure stays fatal and does not publish. Verified each test fails against the defect it guards. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../actions/Publish-PSModule/src/publish.ps1 | 13 +++++- .../tests/Publish-PSModule.Recovery.Tests.ps1 | 43 ++++++++++++++----- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/.github/actions/Publish-PSModule/src/publish.ps1 b/.github/actions/Publish-PSModule/src/publish.ps1 index d71e80bc..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 SilentlyContinue + # 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 fa7c50ea..63ec96a3 100644 --- a/.github/actions/Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 +++ b/.github/actions/Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 @@ -70,25 +70,28 @@ 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' { - $publishMarkerPath = Join-Path -Path $script:workspacePath -ChildPath 'publish-invoked' Set-Item -Path function:global:Find-PSResource -Value { [CmdletBinding()] param( @@ -97,14 +100,34 @@ Describe 'Publish-PSModule recovery' { [string] $Repository ) - Write-Error "Package with name '$Name', version '$Version' could not be found in repository '$Repository'." - } - Set-Item -Path function:global:Publish-PSResource -Value { - $null = New-Item -Path (Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath 'publish-invoked') -ItemType File -Force + # 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 - (Test-Path -Path $publishMarkerPath) | Should -BeTrue + $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 "Error occured while trying to find '$Name' '$Version' in repository '$Repository': Service Unavailable" ` + -ErrorId 'HttpRequestCallFailure' -Category ResourceUnavailable -TargetObject $Name + } + + { & $script:publishScriptPath } | Should -Throw + + $script:calls.PublishInvoked | Should -BeFalse } } From c8d1ade6d400a32fc930a72f459b3246521fcad3 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 2 Sep 2026 06:56:19 +0200 Subject: [PATCH 5/6] Fix spelling in test error message Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 63ec96a3..8a2f418e 100644 --- a/.github/actions/Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 +++ b/.github/actions/Publish-PSModule/tests/Publish-PSModule.Recovery.Tests.ps1 @@ -122,7 +122,7 @@ Describe 'Publish-PSModule recovery' { # 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 "Error occured while trying to find '$Name' '$Version' in repository '$Repository': Service Unavailable" ` + Write-Error -Message "Failed to find '$Name' '$Version' in repository '$Repository': Service Unavailable" ` -ErrorId 'HttpRequestCallFailure' -Category ResourceUnavailable -TargetObject $Name } From a683b641fa580dd44e63ace4221d7dc9c2764594 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 2 Sep 2026 08:30:59 +0200 Subject: [PATCH 6/6] Correct the reason given for shim teardown by name Copilot review flagged the comments as factually wrong, and it was half right. Set-Item does accept 'function:global:X' - it creates X in the global scope. The real problem is asymmetry: Remove-Item and Get-Item do not resolve that same path back to the function, and fail silently instead of erroring, so the teardown was a no-op. Verified no 'global:' path form removes the function (function:global:X, Function:global:X, Function:\global:X all leave it in place), so removing by name is required. Behaviour is unchanged; only the explanation is corrected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../tests/Publish-PSModule.Recovery.Tests.ps1 | 6 ++++-- .../tests/Release-PSModule.WhatIf.Tests.ps1 | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) 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 8a2f418e..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,8 +29,10 @@ AfterAll { foreach ($name in $script:environmentVariableNames) { [System.Environment]::SetEnvironmentVariable($name, $script:originalEnvironment[$name]) } - # The 'global:' scope qualifier is not a valid provider path, so it must be omitted here. - # Otherwise the shims leak into the global scope and shadow the real cmdlets in later test files. + # 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 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 076c7688..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,10 @@ AfterAll { foreach ($name in $script:environmentVariableNames) { [System.Environment]::SetEnvironmentVariable($name, $script:originalEnvironment[$name]) } - # The 'global:' scope qualifier is not a valid provider path, so it must be omitted here. - # Otherwise the shims leak into the global scope and shadow the real commands in later test files. + # 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 }