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 } 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..6cbf9413 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' @@ -97,6 +95,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 +148,81 @@ 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' - Set-Item -Path function:global:git -Value { - $global:LASTEXITCODE = 0 - 'Publish manual dispatch release' + $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.' + } + $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 - $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] -Raw + return 'https://github.com/PSModule/TestModule/releases/tag/v1.2.3' + } + 'release upload' {} + default { + throw "Unexpected GitHub command: $($Arguments -join ' ')" + } + } + }.GetNewClosure() + + Push-Location -LiteralPath $script:workspacePath + try { + & $script:releaseScriptPath + } finally { + Pop-Location + } - $releaseOutput | Should -Match 'Using the pushed commit message as release notes' + $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 $commitSha } } diff --git a/.github/workflows/Test-Actions.yml b/.github/workflows/Test-Actions.yml index a42b8c7f..e58a84e8 100644 --- a/.github/workflows/Test-Actions.yml +++ b/.github/workflows/Test-Actions.yml @@ -44,23 +44,10 @@ 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 - $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' 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.' }