Skip to content

Commit 3eba878

Browse files
Reproduce publication without source-bound preflight
1 parent 64ffb1d commit 3eba878

1 file changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
2+
'PSUseDeclaredVarsMoreThanAssignments', '',
3+
Justification = 'Pester setup variables are used by test blocks and command replacements.'
4+
)]
5+
[CmdletBinding()]
6+
param()
7+
8+
$ErrorActionPreference = 'Stop'
9+
10+
BeforeAll {
11+
$script:environmentMutex = [Threading.Mutex]::new($false, "PSModule-ActionTests-Environment-$PID")
12+
if (-not $script:environmentMutex.WaitOne([TimeSpan]::FromMinutes(5))) {
13+
throw 'Cannot acquire the process environment lock for action tests.'
14+
}
15+
Import-Module -Name 'PSModule' -Force
16+
17+
$script:publishScriptPath = [IO.Path]::GetFullPath(
18+
[IO.Path]::Combine($PSScriptRoot, '..', 'src', 'publish.ps1')
19+
)
20+
$script:environmentVariableNames = @(
21+
'GITHUB_EVENT_PATH'
22+
'GITHUB_REPOSITORY'
23+
'GITHUB_REPOSITORY_NAME'
24+
'GITHUB_SHA'
25+
'GITHUB_WORKSPACE'
26+
'PSMODULE_PUBLISH_PSMODULE_INPUT_Name'
27+
'PSMODULE_PUBLISH_PSMODULE_INPUT_ModulePath'
28+
'PSMODULE_PUBLISH_PSMODULE_INPUT_PSGALLERY_API_KEY'
29+
'PSMODULE_PUBLISH_PSMODULE_INPUT_PullRequest'
30+
'PSMODULE_PUBLISH_PSMODULE_INPUT_ReleaseTag'
31+
'PSMODULE_PUBLISH_PSMODULE_INPUT_CommitSha'
32+
'PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf'
33+
)
34+
$script:originalEnvironment = @{}
35+
foreach ($name in $script:environmentVariableNames) {
36+
$script:originalEnvironment[$name] = [Environment]::GetEnvironmentVariable($name)
37+
}
38+
$script:replacedCommands = @('gh', 'Find-PSResource', 'Publish-PSResource', 'Invoke-WebRequest')
39+
$script:originalFunctions = @{}
40+
foreach ($name in $script:replacedCommands) {
41+
$function = Get-Item -LiteralPath "Function:\$name" -ErrorAction SilentlyContinue
42+
$script:originalFunctions[$name] = $function.ScriptBlock
43+
}
44+
}
45+
46+
AfterAll {
47+
try {
48+
foreach ($name in $script:environmentVariableNames) {
49+
[Environment]::SetEnvironmentVariable($name, $script:originalEnvironment[$name])
50+
}
51+
foreach ($name in $script:replacedCommands) {
52+
Remove-Item -LiteralPath "Function:\$name" -ErrorAction SilentlyContinue
53+
if ($null -ne $script:originalFunctions[$name]) {
54+
Set-Item -Path "Function:global:$name" -Value $script:originalFunctions[$name]
55+
}
56+
}
57+
} finally {
58+
$script:environmentMutex.ReleaseMutex()
59+
$script:environmentMutex.Dispose()
60+
}
61+
}
62+
63+
Describe 'Publish-PSModule source-bound preflight' {
64+
BeforeEach {
65+
$script:modulePath = Join-Path -Path $TestDrive -ChildPath 'workspace' -AdditionalChildPath '.PSModule', 'module', 'TestModule'
66+
$null = New-Item -Path $script:modulePath -ItemType Directory -Force
67+
Set-Content -LiteralPath (Join-Path -Path $script:modulePath -ChildPath 'TestModule.psm1') -Value '# Tested bytes.'
68+
Set-Content -LiteralPath (Join-Path -Path $script:modulePath -ChildPath 'TestModule.psd1') -Value @'
69+
@{
70+
RootModule = 'TestModule.psm1'
71+
ModuleVersion = '1.2.4'
72+
GUID = '3c0f8d73-60b7-4f38-b3cf-9386db4982a4'
73+
Author = 'PSModule'
74+
Description = 'Anonymous publication fixture'
75+
PrivateData = @{
76+
ConsumerMetadata = 'Preserve this value.'
77+
PSData = @{ Prerelease = '' }
78+
}
79+
}
80+
'@
81+
$eventPath = Join-Path -Path $TestDrive -ChildPath 'event.json'
82+
@{ head_commit = @{ message = 'Anonymous release notes.' } } |
83+
ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $eventPath
84+
85+
$env:GITHUB_EVENT_PATH = $eventPath
86+
$env:GITHUB_REPOSITORY = 'PSModule/TestModule'
87+
$env:GITHUB_SHA = '1111111111111111111111111111111111111111'
88+
$env:GITHUB_WORKSPACE = Join-Path -Path $TestDrive -ChildPath 'workspace'
89+
$env:PSMODULE_PUBLISH_PSMODULE_INPUT_Name = 'TestModule'
90+
$env:PSMODULE_PUBLISH_PSMODULE_INPUT_ModulePath = '.PSModule/module'
91+
$env:PSMODULE_PUBLISH_PSMODULE_INPUT_PSGALLERY_API_KEY = 'anonymous-fixture'
92+
$env:PSMODULE_PUBLISH_PSMODULE_INPUT_PullRequest = ''
93+
$env:PSMODULE_PUBLISH_PSMODULE_INPUT_ReleaseTag = 'v1.2.4'
94+
$env:PSMODULE_PUBLISH_PSMODULE_INPUT_CommitSha = $env:GITHUB_SHA
95+
$env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf = 'false'
96+
97+
$state = @{
98+
Calls = [Collections.Generic.List[string]]::new()
99+
GalleryWrites = 0
100+
GitHubWrites = 0
101+
Tag = $null
102+
TagObject = $null
103+
Release = $null
104+
Package = $null
105+
ApiError = ''
106+
}
107+
$script:state = $state
108+
109+
Set-Item -Path function:global:Find-PSResource -Value {
110+
[CmdletBinding()]
111+
param(
112+
[string] $Name,
113+
[string] $Version,
114+
[string] $Repository
115+
)
116+
if ($Name -cne 'TestModule' -or $Version -cne '1.2.4' -or $Repository -cne 'PSGallery') {
117+
throw "Unexpected exact package lookup: [$Name] [$Version] [$Repository]."
118+
}
119+
$state.Calls.Add('gallery-read')
120+
if ($null -ne $state.Package) {
121+
return $state.Package
122+
}
123+
Write-Error -Message 'The exact fixture package is absent.' -ErrorId PackageNotFound -Category ObjectNotFound
124+
}.GetNewClosure()
125+
126+
Set-Item -Path function:global:Publish-PSResource -Value {
127+
$state.Calls.Add('gallery-write')
128+
$state.GalleryWrites++
129+
}.GetNewClosure()
130+
131+
Set-Item -Path function:global:Invoke-WebRequest -Value {
132+
throw 'Package bytes must come from an explicitly injected local fixture, never a network request.'
133+
}
134+
135+
Set-Item -Path function:global:gh -Value {
136+
[CmdletBinding()]
137+
param(
138+
[Parameter(ValueFromRemainingArguments)]
139+
[string[]] $Arguments
140+
)
141+
$state.Calls.Add("github: $($Arguments -join ' ')")
142+
$global:LASTEXITCODE = 0
143+
if ($Arguments[0] -eq 'api' -and $Arguments -notcontains 'POST' -and $Arguments -notcontains 'PATCH') {
144+
if ($state.ApiError) {
145+
$global:LASTEXITCODE = 1
146+
return $state.ApiError
147+
}
148+
if ($Arguments[1] -like '*/git/ref/tags/*') {
149+
if ($null -ne $state.Tag) {
150+
return (@{ object = $state.Tag } | ConvertTo-Json -Depth 10 -Compress)
151+
}
152+
} elseif ($Arguments[1] -like '*/git/tags/*') {
153+
return ($state.TagObject | ConvertTo-Json -Depth 10 -Compress)
154+
} elseif ($Arguments[1] -like '*/releases/tags/*') {
155+
if ($null -ne $state.Release) {
156+
return ($state.Release | ConvertTo-Json -Depth 10 -Compress)
157+
}
158+
} else {
159+
throw "Unexpected read in publication fixture: $($Arguments -join ' ')."
160+
}
161+
$global:LASTEXITCODE = 1
162+
return 'gh: Not Found (HTTP 404)'
163+
}
164+
$state.GitHubWrites++
165+
throw "Unexpected mutation in a rejected publication fixture: $($Arguments -join ' ')."
166+
}.GetNewClosure()
167+
}
168+
169+
It 'rejects a conflicting standalone tag before a Gallery write or a GitHub mutation' {
170+
$script:state.Tag = @{
171+
type = 'commit'
172+
sha = '2222222222222222222222222222222222222222'
173+
}
174+
175+
$failure = $null
176+
try { & $script:publishScriptPath 1>$null 6>$null } catch { $failure = $_ }
177+
178+
$script:state.GalleryWrites | Should -Be 0
179+
$script:state.GitHubWrites | Should -Be 0
180+
$failure | Should -Not -BeNullOrEmpty
181+
$failure.Exception.Message | Should -Match 'source|belongs|ownership'
182+
}
183+
184+
It 'rejects different release asset bytes before a Gallery write or a GitHub mutation' {
185+
$script:state.Tag = @{ type = 'commit'; sha = $env:GITHUB_SHA }
186+
$asset = @{
187+
name = 'TestModule-1.2.4.zip'
188+
digest = 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
189+
}
190+
$script:state.Release = @{
191+
id = 10
192+
tag_name = 'v1.2.4'
193+
html_url = 'https://github.com/PSModule/TestModule/releases/tag/v1.2.4'
194+
prerelease = $false
195+
assets = @($asset)
196+
}
197+
198+
$failure = $null
199+
try { & $script:publishScriptPath 1>$null 6>$null } catch { $failure = $_ }
200+
201+
$script:state.GalleryWrites | Should -Be 0
202+
$script:state.GitHubWrites | Should -Be 0
203+
$failure | Should -Not -BeNullOrEmpty
204+
$failure.Exception.Message | Should -Match 'asset|contents|proof|ownership'
205+
}
206+
207+
It 'does not confuse GitHub API failure with an absent release or tag' {
208+
$script:state.ApiError = 'gh: Service Unavailable (HTTP 503)'
209+
210+
$failure = $null
211+
try { & $script:publishScriptPath 1>$null 6>$null } catch { $failure = $_ }
212+
213+
$script:state.GalleryWrites | Should -Be 0
214+
$script:state.GitHubWrites | Should -Be 0
215+
$failure | Should -Not -BeNullOrEmpty
216+
$failure.Exception.Message | Should -Match '503|API|read|lookup'
217+
}
218+
219+
It 'rejects a Gallery-only package with no source and artifact proof' {
220+
$script:state.Package = [pscustomobject]@{ Name = 'TestModule'; Version = '1.2.4' }
221+
222+
$failure = $null
223+
try { & $script:publishScriptPath 1>$null 6>$null } catch { $failure = $_ }
224+
225+
$script:state.GalleryWrites | Should -Be 0
226+
$script:state.GitHubWrites | Should -Be 0
227+
$failure | Should -Not -BeNullOrEmpty
228+
$failure.Exception.Message | Should -Match 'proof|ownership|unverifiable'
229+
}
230+
231+
It 'rejects a same-source tag without artifact and release-context proof' {
232+
$script:state.Tag = @{ type = 'commit'; sha = $env:GITHUB_SHA }
233+
234+
$failure = $null
235+
try { & $script:publishScriptPath 1>$null 6>$null } catch { $failure = $_ }
236+
237+
$script:state.GalleryWrites | Should -Be 0
238+
$script:state.GitHubWrites | Should -Be 0
239+
$failure | Should -Not -BeNullOrEmpty
240+
$failure.Exception.Message | Should -Match 'proof|ownership|unverifiable'
241+
}
242+
243+
It 'rejects an unverifiable annotated tag rather than trusting its release target hint' {
244+
$script:state.Tag = @{ type = 'tag'; sha = '3333333333333333333333333333333333333333' }
245+
$script:state.TagObject = @{
246+
object = @{ type = 'tree'; sha = $env:GITHUB_SHA }
247+
}
248+
249+
$failure = $null
250+
try { & $script:publishScriptPath 1>$null 6>$null } catch { $failure = $_ }
251+
252+
$script:state.GalleryWrites | Should -Be 0
253+
$script:state.GitHubWrites | Should -Be 0
254+
$failure | Should -Not -BeNullOrEmpty
255+
$failure.Exception.Message | Should -Match 'commit|immutable|tag'
256+
}
257+
258+
It 'rejects a missing immutable source before all external mutations' {
259+
$env:PSMODULE_PUBLISH_PSMODULE_INPUT_CommitSha = ''
260+
$env:GITHUB_SHA = ''
261+
262+
$failure = $null
263+
try { & $script:publishScriptPath 1>$null 6>$null } catch { $failure = $_ }
264+
265+
$script:state.GalleryWrites | Should -Be 0
266+
$script:state.GitHubWrites | Should -Be 0
267+
$failure | Should -Not -BeNullOrEmpty
268+
$failure.Exception.Message | Should -Match 'source|CommitSha|GITHUB_SHA'
269+
}
270+
271+
It 'keeps WhatIf free of Gallery and GitHub calls' {
272+
$env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf = 'true'
273+
274+
{ & $script:publishScriptPath 1>$null 6>$null } | Should -Not -Throw
275+
276+
$script:state.GalleryWrites | Should -Be 0
277+
$script:state.GitHubWrites | Should -Be 0
278+
$script:state.Calls.Count | Should -Be 0
279+
}
280+
}

0 commit comments

Comments
 (0)