From ab0219e570ea3ddefe67013b73101032fb281fb7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 4 Nov 2026 12:43:10 +0100 Subject: [PATCH 1/7] Check release notes instead of diagnostic wording --- .../tests/Release-PSModule.WhatIf.Tests.ps1 | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) 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 e24aeaee..72c466fc 100644 --- a/.github/actions/Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 +++ b/.github/actions/Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 @@ -97,6 +97,10 @@ Describe 'Release-PSModule WhatIf' { $env:PSMODULE_RELEASE_PSMODULE_INPUT_ReleaseTag = 'v1.2.3-preview.1' $env:PSMODULE_RELEASE_PSMODULE_INPUT_PullRequest = '' $env:PSMODULE_RELEASE_PSMODULE_INPUT_CommitSha = '' + + Set-Item -Path function:global:gh -Value { + throw 'GitHub calls are not permitted in WhatIf tests.' + } } It 'creates a prefixed prerelease tag without GitHub side effects' { @@ -146,19 +150,64 @@ Describe 'Release-PSModule WhatIf' { $releaseOutput | Should -Match '--target pushed-commit-sha' } - It 'uses the checked-out commit message for a manual dispatch' { + It 'passes the dispatched commit message as release notes' { $manifest = Get-Content -Path $manifestPath -Raw $manifest -replace "Prerelease = 'preview.1'", "Prerelease = ''" | Set-Content -Path $manifestPath @{} | ConvertTo-Json | Set-Content -Path $eventPath + $env:PSMODULE_RELEASE_PSMODULE_INPUT_WhatIf = 'false' $env:PSMODULE_RELEASE_PSMODULE_INPUT_ReleaseTag = 'v1.2.3' $env:PSMODULE_RELEASE_PSMODULE_INPUT_CommitSha = 'dispatched-commit-sha' + $calls = @{ GitArguments = @(); ReleaseArguments = @(); Notes = $null } + Set-Item -Path function:global:git -Value { + [CmdletBinding()] + param( + [Parameter(ValueFromRemainingArguments)] + [string[]] $Arguments + ) + + $calls.GitArguments = $Arguments $global:LASTEXITCODE = 0 'Publish manual dispatch release' - } + }.GetNewClosure() - $releaseOutput = & $script:releaseScriptPath 6>&1 | Out-String + # Intercept unsafe GitHub writes. This proves script inputs, not the live API contract. + Set-Item -Path function:global:gh -Value { + [CmdletBinding()] + param( + [Parameter(ValueFromRemainingArguments)] + [string[]] $Arguments + ) + + $global:LASTEXITCODE = 0 + if ($Arguments[0] -eq 'api') { + $global:LASTEXITCODE = 1 + return 'HTTP 404' + } + + switch ($Arguments[0..1] -join ' ') { + 'release create' { + $notesIndex = [array]::IndexOf($Arguments, '--notes-file') + if ($notesIndex -lt 0) { + throw 'The release command did not supply a notes file.' + } + $calls.ReleaseArguments = $Arguments + $calls.Notes = Get-Content -LiteralPath $Arguments[$notesIndex + 1] + return 'https://github.com/PSModule/TestModule/releases/tag/v1.2.3' + } + 'release upload' {} + default { + throw "Unexpected GitHub command: $($Arguments -join ' ')" + } + } + }.GetNewClosure() + + & $script:releaseScriptPath - $releaseOutput | Should -Match 'Using the pushed commit message as release notes' + $calls.GitArguments | Should -Be @('log', '-1', '--format=%B', 'dispatched-commit-sha') + $calls.Notes | Should -Be 'Publish manual dispatch release' + $targetIndex = [array]::IndexOf($calls.ReleaseArguments, '--target') + $targetIndex | Should -BeGreaterOrEqual 0 + $calls.ReleaseArguments[$targetIndex + 1] | Should -Be 'dispatched-commit-sha' } } From 57fb3b0c921cc89e147c9f32c0c3b64c2f89d4a4 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 4 Nov 2026 12:55:16 +0100 Subject: [PATCH 2/7] Use an isolated Git commit for release-notes coverage --- .../tests/Release-PSModule.WhatIf.Tests.ps1 | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) 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 72c466fc..0e85640b 100644 --- a/.github/actions/Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 +++ b/.github/actions/Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 @@ -35,15 +35,13 @@ AfterAll { foreach ($name in $script:environmentVariableNames) { [System.Environment]::SetEnvironmentVariable($name, $script:originalEnvironment[$name]) } - # 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' { + AfterEach { + Remove-Item -Path 'Function:\gh' -ErrorAction SilentlyContinue + } + BeforeEach { $script:moduleName = 'TestModule' $script:workspacePath = Join-Path -Path $TestDrive -ChildPath 'workspace' @@ -156,20 +154,28 @@ Describe 'Release-PSModule WhatIf' { @{} | ConvertTo-Json | Set-Content -Path $eventPath $env:PSMODULE_RELEASE_PSMODULE_INPUT_WhatIf = 'false' $env:PSMODULE_RELEASE_PSMODULE_INPUT_ReleaseTag = 'v1.2.3' - $env:PSMODULE_RELEASE_PSMODULE_INPUT_CommitSha = 'dispatched-commit-sha' - $calls = @{ GitArguments = @(); ReleaseArguments = @(); Notes = $null } - - Set-Item -Path function:global:git -Value { - [CmdletBinding()] - param( - [Parameter(ValueFromRemainingArguments)] - [string[]] $Arguments - ) - - $calls.GitArguments = $Arguments - $global:LASTEXITCODE = 0 - 'Publish manual dispatch release' - }.GetNewClosure() + $calls = @{ ReleaseArguments = @(); Notes = $null } + git -C $script:workspacePath init --quiet + if ($LASTEXITCODE -ne 0) { + throw 'Could not create the release test repository.' + } + $gitArguments = @( + '-C', $script:workspacePath + '-c', 'user.name=PSModule Test' + '-c', 'user.email=test@example.invalid' + '-c', 'commit.gpgsign=false' + '-c', "core.hooksPath=$TestDrive" + 'commit', '--allow-empty', '--quiet', '-m', 'Publish manual dispatch release' + ) + git @gitArguments + if ($LASTEXITCODE -ne 0) { + throw 'Could not create the release test commit.' + } + $commitSha = git -C $script:workspacePath rev-parse HEAD + if ($LASTEXITCODE -ne 0) { + throw 'Could not read the release test commit.' + } + $env:PSMODULE_RELEASE_PSMODULE_INPUT_CommitSha = $commitSha # Intercept unsafe GitHub writes. This proves script inputs, not the live API contract. Set-Item -Path function:global:gh -Value { @@ -192,7 +198,7 @@ Describe 'Release-PSModule WhatIf' { throw 'The release command did not supply a notes file.' } $calls.ReleaseArguments = $Arguments - $calls.Notes = Get-Content -LiteralPath $Arguments[$notesIndex + 1] + $calls.Notes = Get-Content -LiteralPath $Arguments[$notesIndex + 1] -Raw return 'https://github.com/PSModule/TestModule/releases/tag/v1.2.3' } 'release upload' {} @@ -202,12 +208,16 @@ Describe 'Release-PSModule WhatIf' { } }.GetNewClosure() - & $script:releaseScriptPath + Push-Location -LiteralPath $script:workspacePath + try { + & $script:releaseScriptPath + } finally { + Pop-Location + } - $calls.GitArguments | Should -Be @('log', '-1', '--format=%B', 'dispatched-commit-sha') - $calls.Notes | Should -Be 'Publish manual dispatch release' + $calls.Notes.TrimEnd([char[]] "`r`n") | Should -Be 'Publish manual dispatch release' $targetIndex = [array]::IndexOf($calls.ReleaseArguments, '--target') $targetIndex | Should -BeGreaterOrEqual 0 - $calls.ReleaseArguments[$targetIndex + 1] | Should -Be 'dispatched-commit-sha' + $calls.ReleaseArguments[$targetIndex + 1] | Should -Be $commitSha } } From 61ba6575484e4ae43b02cbae21340969029fc9f5 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 4 Nov 2026 12:55:16 +0100 Subject: [PATCH 3/7] Keep version-output checks without freezing helper parameters --- .../Publish-PSModule/tests/Publish-PSModule.Helpers.Tests.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/actions/Publish-PSModule/tests/Publish-PSModule.Helpers.Tests.ps1 b/.github/actions/Publish-PSModule/tests/Publish-PSModule.Helpers.Tests.ps1 index 59a2f197..7d5002b9 100644 --- a/.github/actions/Publish-PSModule/tests/Publish-PSModule.Helpers.Tests.ps1 +++ b/.github/actions/Publish-PSModule/tests/Publish-PSModule.Helpers.Tests.ps1 @@ -35,10 +35,6 @@ Describe 'Publish-PSModule.Helpers' { Should -Be '1.1.10-mybranch001' } - It 'Get-ModuleVersionString - takes no version prefix parameter at all' { - (Get-Command Get-ModuleVersionString).Parameters.Keys | Should -Not -Contain 'VersionPrefix' - } - It 'Get-ModuleVersionString - requires a module version' { { Get-ModuleVersionString -ModuleVersion '' } | Should -Throw } From ed4af52d73ec2e8594a4488db1feeccadae17699 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 4 Nov 2026 12:55:16 +0100 Subject: [PATCH 4/7] Check authored documentation transfer without fixed prose --- .github/workflows/Workflow-Test-Default.yml | 28 +++++++++---------- .../workflows/Workflow-Test-WithManifest.yml | 28 +++++++++---------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/.github/workflows/Workflow-Test-Default.yml b/.github/workflows/Workflow-Test-Default.yml index 5b3da9fb..d6841e73 100644 --- a/.github/workflows/Workflow-Test-Default.yml +++ b/.github/workflows/Workflow-Test-Default.yml @@ -85,24 +85,22 @@ jobs: - name: Verify generated Functions index shell: pwsh run: | + $ErrorActionPreference = 'Stop' + $sourcePath = 'tests/srcTestRepo/src/functions/public/index.md' $path = 'tests/srcTestRepo/.PSModule/docs/index.md' - if (-not (Test-Path -Path $path)) { + if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) { + throw "Authored Functions index fixture is missing: $sourcePath" + } + if ((Get-Item -LiteralPath $sourcePath).Length -eq 0) { + throw "Authored Functions index fixture is empty: $sourcePath" + } + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw 'Expected root Functions index at ' + 'tests/srcTestRepo/.PSModule/docs/index.md.' } - $content = Get-Content -Path $path -Raw - $expectedSnippets = @( - '# Functions' - 'This landing page is authored from the root of ' + - '`src/functions/public`.' - 'Use it to introduce the module''s function surface ' + - 'before readers drill into each group.' - ) - - foreach ($snippet in $expectedSnippets) { - if (-not $content.Contains($snippet)) { - throw 'Expected snippet not found in generated root ' + - "Functions index: $snippet" - } + $expectedHash = (Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash + $actualHash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash + if ($actualHash -cne $expectedHash) { + throw 'Generated root Functions index does not preserve the authored Markdown.' } diff --git a/.github/workflows/Workflow-Test-WithManifest.yml b/.github/workflows/Workflow-Test-WithManifest.yml index 898308b4..5be63907 100644 --- a/.github/workflows/Workflow-Test-WithManifest.yml +++ b/.github/workflows/Workflow-Test-WithManifest.yml @@ -85,24 +85,22 @@ jobs: - name: Verify generated Functions index shell: pwsh run: | + $ErrorActionPreference = 'Stop' + $sourcePath = 'tests/srcWithManifestTestRepo/src/functions/public/index.md' $path = 'tests/srcWithManifestTestRepo/.PSModule/docs/index.md' - if (-not (Test-Path -Path $path)) { + if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) { + throw "Authored Functions index fixture is missing: $sourcePath" + } + if ((Get-Item -LiteralPath $sourcePath).Length -eq 0) { + throw "Authored Functions index fixture is empty: $sourcePath" + } + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw 'Expected root Functions index at ' + 'tests/srcWithManifestTestRepo/.PSModule/docs/index.md.' } - $content = Get-Content -Path $path -Raw - $expectedSnippets = @( - '# Functions' - 'This landing page is authored from the root of ' + - '`src/functions/public`.' - 'Use it to introduce the module''s function surface ' + - 'before readers drill into each group.' - ) - - foreach ($snippet in $expectedSnippets) { - if (-not $content.Contains($snippet)) { - throw 'Expected snippet not found in generated root ' + - "Functions index: $snippet" - } + $expectedHash = (Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash + $actualHash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash + if ($actualHash -cne $expectedHash) { + throw 'Generated root Functions index does not preserve the authored Markdown.' } From dbd162f0001b73cfa03207dd18f0ce607adc6114 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 4 Nov 2026 12:55:16 +0100 Subject: [PATCH 5/7] Run action tests with the configured Pester controls --- .github/workflows/Test-Actions.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/Test-Actions.yml b/.github/workflows/Test-Actions.yml index a42b8c7f..124ca4d6 100644 --- a/.github/workflows/Test-Actions.yml +++ b/.github/workflows/Test-Actions.yml @@ -48,19 +48,6 @@ jobs: $configuration.Run.ParallelThrottleLimit = 2 $configuration.Debug.ShowStartMarkers = $true - $expectedOptions = @{ - 'Run.Shuffle' = $configuration.Run.Shuffle.Value - 'Run.ShuffleSeed' = $configuration.Run.ShuffleSeed.Value -eq 42 - 'Run.Parallel' = $configuration.Run.Parallel.Value - 'Run.ParallelThrottleLimit' = $configuration.Run.ParallelThrottleLimit.Value -eq 2 - 'Debug.ShowStartMarkers' = $configuration.Debug.ShowStartMarkers.Value - } - foreach ($option in $expectedOptions.GetEnumerator()) { - if (-not $option.Value) { - throw "Pester 6.1 configuration option [$($option.Key)] was not applied." - } - } - $testPaths = Get-ChildItem -Path '.github/actions' -Directory | ForEach-Object { Join-Path -Path $_.FullName -ChildPath 'tests' } | Where-Object { Test-Path -Path $_ } @@ -68,7 +55,6 @@ jobs: throw 'No action test folders were found under .github/actions.' } - $configuration = New-PesterConfiguration $configuration.Run.Path = $testPaths $configuration.Run.Throw = $true $configuration.Output.Verbosity = 'Detailed' From 40d2a1d94a1b997588ec480b33d7a5c6f57e9a8b Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 4 Nov 2026 13:10:45 +0100 Subject: [PATCH 6/7] Distinguish the dispatched commit from repository HEAD --- .../Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 | 5 +++++ 1 file changed, 5 insertions(+) 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 0e85640b..6cbf9413 100644 --- a/.github/actions/Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 +++ b/.github/actions/Release-PSModule/tests/Release-PSModule.WhatIf.Tests.ps1 @@ -175,6 +175,11 @@ Describe 'Release-PSModule WhatIf' { if ($LASTEXITCODE -ne 0) { throw 'Could not read the release test commit.' } + $gitArguments[-1] = 'Do not use the latest commit for this release' + git @gitArguments + if ($LASTEXITCODE -ne 0) { + throw 'Could not create the newer unrelated test commit.' + } $env:PSMODULE_RELEASE_PSMODULE_INPUT_CommitSha = $commitSha # Intercept unsafe GitHub writes. This proves script inputs, not the live API contract. From 7a00cbb9cd72baddf14d990019cf5da3c9c3600e Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Wed, 4 Nov 2026 13:10:45 +0100 Subject: [PATCH 7/7] Keep shared-environment test files in serial execution --- .github/workflows/Test-Actions.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test-Actions.yml b/.github/workflows/Test-Actions.yml index 124ca4d6..e58a84e8 100644 --- a/.github/workflows/Test-Actions.yml +++ b/.github/workflows/Test-Actions.yml @@ -44,8 +44,8 @@ jobs: $configuration = New-PesterConfiguration $configuration.Run.Shuffle = $true $configuration.Run.ShuffleSeed = 42 - $configuration.Run.Parallel = $true - $configuration.Run.ParallelThrottleLimit = 2 + # These suites share process-wide environment variables. + $configuration.Run.Parallel = $false $configuration.Debug.ShowStartMarkers = $true $testPaths = Get-ChildItem -Path '.github/actions' -Directory |