Skip to content

Commit 38ff9ff

Browse files
Check release-bearing changes before association and document safe recovery
1 parent 86c4f1b commit 38ff9ff

8 files changed

Lines changed: 357 additions & 142 deletions

File tree

‎.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -173,17 +173,16 @@ function Resolve-ReleasePullRequest {
173173
Resolves the pull request whose version label drives the release for a commit.
174174
175175
.DESCRIPTION
176-
A push resolves the pull request associated with the pushed commit so a default-branch
177-
release honours the merged pull request's version label. A manual dispatch on the default
178-
branch is the documented recovery route for a failed or cancelled release run and targets
179-
the same merge commit, so it must resolve the same pull request. Excluding it left the pull
180-
request unresolved, and the version silently fell back to a patch bump through AutoPatching.
176+
A push and a default-branch dispatch resolve the exact merged pull request for the released
177+
commit. Dispatch uses the current branch tip, so it can recover an intended merge only while
178+
that merge is still the tip. Rerun the original workflow's failed jobs for an older commit.
181179
182180
When no pull request is selected, the outcome depends on why. A commit pushed directly to
183181
the default branch has no label to honour, so the release proceeds with the default patch
184182
bump. A commit associated with a merged default-branch pull request that does not match it
185-
does carry a label, and applying a patch bump would publish a version nobody asked for. A
186-
PowerShell Gallery version cannot be reclaimed, so that case throws instead.
183+
does carry a label, and applying a patch bump would publish a version nobody asked for.
184+
That case throws for an important default-branch push or a recovery dispatch, but not for a
185+
documentation-only push that cannot publish.
187186
188187
.OUTPUTS
189188
PSCustomObject with Resolved, indicating whether the lookup ran, and PullRequest, which is
@@ -193,7 +192,7 @@ function Resolve-ReleasePullRequest {
193192
Resolve-ReleasePullRequest -EventName workflow_dispatch -CommitSha $sha -DefaultBranch main `
194193
-IsManualDispatchToDefaultBranch $true -GetAssociatedPullRequest { param($Sha) $pulls }
195194
196-
Resolves the merged pull request for a recovery dispatch so its version label is honoured.
195+
Resolves the merged pull request for a current-tip recovery dispatch so its label is honored.
197196
#>
198197
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '',
199198
Justification = 'Parameters are used inside a LogGroup script block.')]
@@ -222,6 +221,10 @@ function Resolve-ReleasePullRequest {
222221
[Parameter()]
223222
[bool] $IsManualDispatchToDefaultBranch,
224223

224+
# Whether a default-branch push changes a configured important file.
225+
[Parameter()]
226+
[bool] $HasImportantChanges = $true,
227+
225228
# Returns the pull requests GitHub associates with a commit. Takes the commit SHA.
226229
[Parameter(Mandatory)]
227230
[scriptblock] $GetAssociatedPullRequest
@@ -236,7 +239,7 @@ function Resolve-ReleasePullRequest {
236239
return [pscustomobject]@{ Resolved = $false; PullRequest = $null }
237240
}
238241

239-
LogGroup "Resolve pull request for commit [$CommitSha]" {
242+
Set-GitHubLogGroup "Resolve pull request for commit [$CommitSha]" {
240243
$associated = @((& $GetAssociatedPullRequest $CommitSha) | Where-Object { $null -ne $_ })
241244
$pullRequest = Select-PullRequestForPush -PullRequest $associated `
242245
-DefaultBranch $DefaultBranch `
@@ -249,7 +252,7 @@ function Resolve-ReleasePullRequest {
249252

250253
# Only a release-bearing event can publish a wrong version. A push to a feature branch has
251254
# no release to get wrong, and its commit is legitimately claimed by an open pull request.
252-
$isReleaseEvent = $IsPushToDefaultBranch -or $IsManualDispatchToDefaultBranch
255+
$isReleaseEvent = ($IsPushToDefaultBranch -and $HasImportantChanges) -or $IsManualDispatchToDefaultBranch
253256
$discarded = if ($isReleaseEvent) {
254257
@(Get-DiscardedReleasePullRequest -PullRequest $associated `
255258
-DefaultBranch $DefaultBranch `
@@ -263,8 +266,8 @@ function Resolve-ReleasePullRequest {
263266
'The following merged pull request(s) are associated with it but none matches the commit ' +
264267
"being released: $($discarded -join '; '). " +
265268
'Refusing to fall back to a patch bump, because a wrong version published to the ' +
266-
'PowerShell Gallery cannot be reclaimed. Re-run the workflow against the merge commit ' +
267-
'of the pull request you intend to release.'
269+
'PowerShell Gallery cannot be reclaimed. Rerun the original workflow for the intended merge commit. ' +
270+
'A default-branch dispatch can recover that commit only while it is still the branch tip.'
268271
)
269272
}
270273

‎.github/actions/Get-PSModuleSettings/src/main.ps1‎

Lines changed: 102 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
'powershell-yaml', 'Hashtable' | Install-PSResource -Repository PSGallery -TrustRepository
1+
[CmdletBinding()]
2+
param()
3+
4+
$ErrorActionPreference = 'Stop'
5+
6+
'powershell-yaml', 'Hashtable' | Install-PSResource -Repository PSGallery -TrustRepository
27
Import-Module -Name "$PSScriptRoot/Get-PSModuleSettings.Helpers.psm1" -Force
38

49
$name = $env:PSMODULE_GET_SETTINGS_INPUT_Name
@@ -10,7 +15,7 @@ $prerelease = $env:PSMODULE_GET_SETTINGS_INPUT_Prerelease
1015
$workingDirectory = $env:PSMODULE_GET_SETTINGS_INPUT_WorkingDirectory
1116
$importantFilePatternsInput = $env:PSMODULE_GET_SETTINGS_INPUT_ImportantFilePatterns
1217

13-
LogGroup 'Inputs' {
18+
Set-GitHubLogGroup 'Inputs' {
1419
[pscustomobject]@{
1520
PWD = (Get-Location).Path
1621
Name = $name
@@ -19,7 +24,7 @@ LogGroup 'Inputs' {
1924
}
2025

2126
if (![string]::IsNullOrEmpty($settingsPath) -and (Test-Path -Path $settingsPath)) {
22-
LogGroup 'Import settings' {
27+
Set-GitHubLogGroup 'Import settings' {
2328
$settingsFile = Get-Item -Path $settingsPath
2429
$relativeSettingsPath = $settingsFile | Resolve-Path -Relative
2530
Write-Host "Importing settings from [$relativeSettingsPath]"
@@ -43,7 +48,7 @@ if (![string]::IsNullOrEmpty($settingsPath) -and (Test-Path -Path $settingsPath)
4348
}
4449
}
4550

46-
LogGroup 'Validate settings against schema' {
51+
Set-GitHubLogGroup 'Validate settings against schema' {
4752
$schemaPath = Join-Path $PSScriptRoot 'Settings.schema.json'
4853
if (Test-Path -Path $schemaPath) {
4954
Write-Host 'Validating settings against schema...'
@@ -73,7 +78,7 @@ if (![string]::IsNullOrEmpty($settingsPath) -and (Test-Path -Path $settingsPath)
7378
$settings = @{}
7479
}
7580

76-
LogGroup 'Name' {
81+
Set-GitHubLogGroup 'Name' {
7782
[pscustomobject]@{
7883
InputName = $name
7984
SettingsName = $settings.Name
@@ -91,7 +96,7 @@ LogGroup 'Name' {
9196
}
9297
}
9398

94-
LogGroup 'ImportantFilePatterns' {
99+
Set-GitHubLogGroup 'ImportantFilePatterns' {
95100
$defaultImportantFilePatterns = @('^src/', '^README\.md$')
96101
if ($null -ne $settings.ImportantFilePatterns) {
97102
$importantFilePatterns = @($settings.ImportantFilePatterns)
@@ -223,10 +228,10 @@ $settings | Add-Member -MemberType NoteProperty -Name Prerelease -Value $prerele
223228
$settings | Add-Member -MemberType NoteProperty -Name WorkingDirectory -Value $workingDirectory
224229

225230
# Calculate job run conditions
226-
LogGroup 'Calculate Job Run Conditions:' {
231+
Set-GitHubLogGroup 'Calculate Job Run Conditions:' {
227232
$eventData = Get-GitHubEventData -ErrorAction Stop
228233

229-
LogGroup 'GitHub Event Data' {
234+
Set-GitHubLogGroup 'GitHub Event Data' {
230235
$eventData | ConvertTo-Json -Depth 10 | Out-String
231236
}
232237

@@ -242,102 +247,20 @@ LogGroup 'Calculate Job Run Conditions:' {
242247
$isManualDispatchToDefaultBranch = $isManualDispatch -and $workflowRef -eq $defaultBranch
243248
$pullRequest = $eventData.PullRequest
244249

245-
# A manual dispatch on the default branch is the documented recovery route for a failed or
246-
# cancelled release run. It targets the same merge commit as the push it replaces, so it must
247-
# resolve the same pull request and honour the same version label. Gating this lookup on
248-
# $isPush alone left $pullRequest null for a dispatch, which silently downgraded a labelled
249-
# Major or Minor release to a Patch bump through the AutoPatching fallback.
250-
$resolveParams = @{
251-
EventName = $eventName
252-
CommitSha = $commitSha
253-
DefaultBranch = $defaultBranch
254-
IsPushToDefaultBranch = $isPushToDefaultBranch
255-
IsManualDispatchToDefaultBranch = $isManualDispatchToDefaultBranch
256-
GetAssociatedPullRequest = {
257-
param($Sha)
258-
$owner = $env:GITHUB_REPOSITORY_OWNER
259-
$repo = $env:GITHUB_REPOSITORY_NAME
260-
$response = Invoke-GitHubAPI -ApiEndpoint "/repos/$owner/$repo/commits/$Sha/pulls" -Method GET
261-
$response.Response
262-
}
263-
}
264-
$resolution = Resolve-ReleasePullRequest @resolveParams
265-
if ($resolution.Resolved) {
266-
$pullRequest = $resolution.PullRequest
267-
}
268-
269-
$pullRequestIsMerged = if ($null -eq $pullRequest) {
270-
$false
271-
} elseif ($null -ne $pullRequest.Merged) {
272-
[bool]$pullRequest.Merged
273-
} else {
274-
-not [string]::IsNullOrWhiteSpace($pullRequest.merged_at)
275-
}
276250
$pullRequestIsClosed = $null -ne $pullRequest -and $pullRequest.State -eq 'closed'
277251
$isOpenOrUpdatedPR = (
278252
$eventName -eq 'pull_request' -and
279253
-not $pullRequestIsClosed -and
280254
$pullRequestAction -in @('opened', 'reopened', 'synchronize', 'labeled', 'unlabeled')
281255
)
282-
$targetBranch = if ($pullRequest) { $pullRequest.Base.Ref } elseif ($isPush) { $pushBranch } else { $workflowRef }
283-
$isTargetDefaultBranch = $targetBranch -eq $defaultBranch
284-
$pullRequestContext = if ($pullRequest) {
285-
[pscustomobject]@{
286-
Number = $pullRequest.Number
287-
Title = $pullRequest.Title
288-
Body = $pullRequest.Body
289-
HeadRef = $pullRequest.Head.Ref
290-
BaseRef = $pullRequest.Base.Ref
291-
Labels = @($pullRequest.Labels.Name)
292-
Merged = $pullRequestIsMerged
293-
Closed = $pullRequestIsClosed
294-
MergeCommitSha = $pullRequest.merge_commit_sha
295-
HtmlUrl = $pullRequest.html_url
296-
}
297-
} else {
298-
$null
299-
}
300-
301-
$settings | Add-Member -MemberType NoteProperty -Name Context -Value ([pscustomobject]@{
302-
EventName = $eventName
303-
EventAction = $pullRequestAction
304-
CommitSha = $commitSha
305-
Ref = if ($isPush) { $eventData.Ref } else { $env:GITHUB_REF }
306-
DefaultBranch = $defaultBranch
307-
IsPushToDefaultBranch = $isPushToDefaultBranch
308-
IsManualDispatchToDefaultBranch = $isManualDispatchToDefaultBranch
309-
PullRequest = $pullRequestContext
310-
}) -Force
311-
312-
Write-Host 'GitHub event inputs:'
313-
[pscustomobject]@{
314-
GITHUB_EVENT_NAME = $eventName
315-
GITHUB_EVENT_ACTION = $pullRequestAction
316-
GITHUB_EVENT_PULL_REQUEST_MERGED = $pullRequestIsMerged
317-
GITHUB_EVENT_PULL_REQUEST_CLOSED = $pullRequestIsClosed
318-
CommitSha = $commitSha
319-
PushBranch = $pushBranch
320-
TargetBranch = $targetBranch
321-
DefaultBranch = $defaultBranch
322-
IsTargetDefaultBranch = $isTargetDefaultBranch
323-
IsPushToDefaultBranch = $isPushToDefaultBranch
324-
IsManualDispatchToDefaultBranch = $isManualDispatchToDefaultBranch
325-
AssociatedPullRequest = $pullRequestContext.Number
326-
} | Format-List | Out-String
327-
328-
# Check if a prerelease label exists on the PR
329-
$prereleaseLabels = $settings.Publish.Module.PrereleaseLabels -split ',' | ForEach-Object { $_.Trim() }
330-
$prLabels = @($pullRequestContext.Labels)
331-
$hasPrereleaseLabel = ($prLabels | Where-Object { $prereleaseLabels -contains $_ }).Count -gt 0
332-
333256
# Check if important files have changed in the PR
334257
# Important files are determined by the configured ImportantFilePatterns setting
335258
$hasImportantChanges = $false
336-
if ($eventName -eq 'pull_request' -and $pullRequestContext.Number) {
337-
LogGroup 'Check for Important File Changes' {
259+
if ($eventName -eq 'pull_request' -and $pullRequest.Number) {
260+
Set-GitHubLogGroup 'Check for Important File Changes' {
338261
$owner = $env:GITHUB_REPOSITORY_OWNER
339262
$repo = $env:GITHUB_REPOSITORY_NAME
340-
$prNumber = $pullRequestContext.Number
263+
$prNumber = $pullRequest.Number
341264

342265
Write-Host "Fetching changed files for PR #$prNumber..."
343266
$changedFiles = Invoke-GitHubAPI -ApiEndpoint "/repos/$owner/$repo/pulls/$prNumber/files" -Method GET |
@@ -408,7 +331,7 @@ If you believe this is incorrect, please verify that your changes are in the cor
408331
}
409332
}
410333
} elseif ($isPushToDefaultBranch) {
411-
LogGroup 'Check for Important File Changes' {
334+
Set-GitHubLogGroup 'Check for Important File Changes' {
412335
$beforeCommitSha = $eventData.Before
413336
$owner = $env:GITHUB_REPOSITORY_OWNER
414337
$repo = $env:GITHUB_REPOSITORY_NAME
@@ -448,6 +371,85 @@ If you believe this is incorrect, please verify that your changes are in the cor
448371
Write-Host 'Non-PR event - treating as having important changes'
449372
}
450373

374+
# Dispatch resolves the current default-branch tip. Rerun the original workflow for an older merge.
375+
$resolveParams = @{
376+
EventName = $eventName
377+
CommitSha = $commitSha
378+
DefaultBranch = $defaultBranch
379+
IsPushToDefaultBranch = $isPushToDefaultBranch
380+
IsManualDispatchToDefaultBranch = $isManualDispatchToDefaultBranch
381+
HasImportantChanges = $hasImportantChanges
382+
GetAssociatedPullRequest = {
383+
param($Sha)
384+
$owner = $env:GITHUB_REPOSITORY_OWNER
385+
$repo = $env:GITHUB_REPOSITORY_NAME
386+
$response = Invoke-GitHubAPI -ApiEndpoint "/repos/$owner/$repo/commits/$Sha/pulls" -Method GET
387+
$response.Response
388+
}
389+
}
390+
$resolution = Resolve-ReleasePullRequest @resolveParams
391+
if ($resolution.Resolved) {
392+
$pullRequest = $resolution.PullRequest
393+
}
394+
395+
$pullRequestIsMerged = if ($null -eq $pullRequest) {
396+
$false
397+
} elseif ($null -ne $pullRequest.Merged) {
398+
[bool]$pullRequest.Merged
399+
} else {
400+
-not [string]::IsNullOrWhiteSpace($pullRequest.merged_at)
401+
}
402+
$pullRequestIsClosed = $null -ne $pullRequest -and $pullRequest.State -eq 'closed'
403+
$targetBranch = if ($pullRequest) { $pullRequest.Base.Ref } elseif ($isPush) { $pushBranch } else { $workflowRef }
404+
$isTargetDefaultBranch = $targetBranch -eq $defaultBranch
405+
$pullRequestContext = if ($pullRequest) {
406+
[pscustomobject]@{
407+
Number = $pullRequest.Number
408+
Title = $pullRequest.Title
409+
Body = $pullRequest.Body
410+
HeadRef = $pullRequest.Head.Ref
411+
BaseRef = $pullRequest.Base.Ref
412+
Labels = @($pullRequest.Labels.Name)
413+
Merged = $pullRequestIsMerged
414+
Closed = $pullRequestIsClosed
415+
MergeCommitSha = $pullRequest.merge_commit_sha
416+
HtmlUrl = $pullRequest.html_url
417+
}
418+
} else {
419+
$null
420+
}
421+
422+
$settings | Add-Member -MemberType NoteProperty -Name Context -Value ([pscustomobject]@{
423+
EventName = $eventName
424+
EventAction = $pullRequestAction
425+
CommitSha = $commitSha
426+
Ref = if ($isPush) { $eventData.Ref } else { $env:GITHUB_REF }
427+
DefaultBranch = $defaultBranch
428+
IsPushToDefaultBranch = $isPushToDefaultBranch
429+
IsManualDispatchToDefaultBranch = $isManualDispatchToDefaultBranch
430+
PullRequest = $pullRequestContext
431+
}) -Force
432+
433+
Write-Host 'GitHub event inputs:'
434+
[pscustomobject]@{
435+
GITHUB_EVENT_NAME = $eventName
436+
GITHUB_EVENT_ACTION = $pullRequestAction
437+
GITHUB_EVENT_PULL_REQUEST_MERGED = $pullRequestIsMerged
438+
GITHUB_EVENT_PULL_REQUEST_CLOSED = $pullRequestIsClosed
439+
CommitSha = $commitSha
440+
PushBranch = $pushBranch
441+
TargetBranch = $targetBranch
442+
DefaultBranch = $defaultBranch
443+
IsTargetDefaultBranch = $isTargetDefaultBranch
444+
IsPushToDefaultBranch = $isPushToDefaultBranch
445+
IsManualDispatchToDefaultBranch = $isManualDispatchToDefaultBranch
446+
AssociatedPullRequest = $pullRequestContext.Number
447+
} | Format-List | Out-String
448+
449+
$prereleaseLabels = $settings.Publish.Module.PrereleaseLabels -split ',' | ForEach-Object { $_.Trim() }
450+
$prLabels = @($pullRequestContext.Labels)
451+
$hasPrereleaseLabel = ($prLabels | Where-Object { $prereleaseLabels -contains $_ }).Count -gt 0
452+
451453
$routing = Resolve-WorkflowEventRouting -EventName $eventName `
452454
-EventAction $pullRequestAction `
453455
-PullRequestIsMerged $pullRequestIsMerged `
@@ -490,7 +492,7 @@ if ($settings.Test.Skip) {
490492
$macOS = [PSCustomObject]@{ RunsOn = 'macos-latest'; OSName = 'macOS' }
491493
$windows = [PSCustomObject]@{ RunsOn = 'windows-latest'; OSName = 'Windows' }
492494

493-
LogGroup 'Source Code Test Suites:' {
495+
Set-GitHubLogGroup 'Source Code Test Suites:' {
494496
$sourceCodeTestSuites = if ($settings.Test.SourceCode.Skip) {
495497
Write-Host 'Skipping all source code tests.'
496498
$null
@@ -504,7 +506,7 @@ if ($settings.Test.Skip) {
504506
$sourceCodeTestSuites | Format-Table -AutoSize | Out-String
505507
}
506508

507-
LogGroup 'PSModule Test Suites:' {
509+
Set-GitHubLogGroup 'PSModule Test Suites:' {
508510
$psModuleTestSuites = if ($settings.Test.PSModule.Skip) {
509511
Write-Host 'Skipping all PSModule tests.'
510512
$null
@@ -518,7 +520,7 @@ if ($settings.Test.Skip) {
518520
$psModuleTestSuites | Format-Table -AutoSize | Out-String
519521
}
520522

521-
LogGroup 'Module Local Test Suites:' {
523+
Set-GitHubLogGroup 'Module Local Test Suites:' {
522524
$moduleTestSuites = if ($settings.Test.Module.Skip) {
523525
Write-Host 'Skipping all module tests.'
524526
$null
@@ -638,7 +640,7 @@ $settings.Test.PSModule | Add-Member -MemberType NoteProperty -Name Suites -Valu
638640
$settings.Test.Module | Add-Member -MemberType NoteProperty -Name Suites -Value $moduleTestSuites -Force
639641

640642
# Calculate job-specific conditions and add to settings
641-
LogGroup 'Calculate Job Run Conditions:' {
643+
Set-GitHubLogGroup 'Calculate Job Run Conditions:' {
642644
$shouldAutoCleanup = $routing.ShouldRunCleanup -and ($settings.Publish.Module.AutoCleanup -eq $true)
643645

644646
# Update Publish.Module with computed release values
@@ -730,7 +732,7 @@ LogGroup 'Calculate Job Run Conditions:' {
730732
} | Format-List | Out-String
731733
}
732734

733-
LogGroup 'Final settings' {
735+
Set-GitHubLogGroup 'Final settings' {
734736
switch -Regex ($settingsFile.Extension) {
735737
'.yaml|.yml' {
736738
Write-Host ($settings | ConvertTo-Yaml | Out-String)
@@ -745,7 +747,7 @@ LogGroup 'Final settings' {
745747
Set-GitHubOutput -Name Settings -Value ($settings | ConvertTo-Json -Depth 10)
746748
}
747749

748-
LogGroup 'Validate output settings against schema' {
750+
Set-GitHubLogGroup 'Validate output settings against schema' {
749751
$schemaPath = Join-Path $PSScriptRoot 'Settings.schema.json'
750752
if (Test-Path -Path $schemaPath) {
751753
Write-Host 'Validating output settings against schema...'

0 commit comments

Comments
 (0)