Skip to content

Commit 189d3eb

Browse files
Marius StorhaugCopilot
authored andcommitted
refactor: make release pull request resolution unit-testable
Extract the gate, selection, and no-pull-request decision from main.ps1 into Resolve-ReleasePullRequest, with the GitHub lookup injected as a script block. The composed logic that produced the wrong version is now covered directly rather than re-implemented in a test. Add Get-DiscardedReleasePullRequest so a merged default-branch pull request that does not match the released commit fails the run instead of silently applying a patch bump. Open pull requests and non-default-branch merges are ignored, so a direct push and a feature-branch push are unaffected. Each new test was verified to fail against the specific defect it guards. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 27dedd6 commit 189d3eb

4 files changed

Lines changed: 409 additions & 80 deletions

File tree

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

Lines changed: 106 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -109,45 +109,6 @@ function Select-PullRequestForPush {
109109
Select-Object -First 1
110110
}
111111

112-
function Test-ShouldResolveAssociatedPullRequest {
113-
<#
114-
.SYNOPSIS
115-
Decides whether a commit's associated pull request must be resolved from the GitHub API.
116-
117-
.DESCRIPTION
118-
A push to any branch resolves its associated pull request so a default-branch release
119-
honours the merged pull request's version label. A manual dispatch on the default branch is
120-
the documented recovery route for a failed or cancelled release run and targets the same
121-
merge commit, so it must resolve the same pull request. Excluding it left the pull request
122-
null and silently downgraded a labelled Major or Minor release to a Patch bump.
123-
124-
.OUTPUTS
125-
Boolean. True when the association lookup must run.
126-
127-
.EXAMPLE
128-
Test-ShouldResolveAssociatedPullRequest -IsPush $false -IsManualDispatchToDefaultBranch $true -CommitSha 'abc123'
129-
130-
Returns $true, because a default-branch dispatch releases the same commit a push would.
131-
#>
132-
[CmdletBinding()]
133-
[OutputType([bool])]
134-
param(
135-
# Whether the workflow was triggered by a push event.
136-
[Parameter()]
137-
[bool] $IsPush,
138-
139-
# Whether the workflow was manually dispatched against the default branch.
140-
[Parameter()]
141-
[bool] $IsManualDispatchToDefaultBranch,
142-
143-
# The commit the workflow is resolving a release for.
144-
[Parameter()]
145-
[string] $CommitSha
146-
)
147-
148-
($IsPush -or $IsManualDispatchToDefaultBranch) -and -not [string]::IsNullOrWhiteSpace($CommitSha)
149-
}
150-
151112
function Get-DiscardedReleasePullRequest {
152113
<#
153114
.SYNOPSIS
@@ -206,6 +167,112 @@ function Get-DiscardedReleasePullRequest {
206167
}
207168
}
208169

170+
function Resolve-ReleasePullRequest {
171+
<#
172+
.SYNOPSIS
173+
Resolves the pull request whose version label drives the release for a commit.
174+
175+
.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.
181+
182+
When no pull request is selected, the outcome depends on why. A commit pushed directly to
183+
the default branch has no label to honour, so the release proceeds with the default patch
184+
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.
187+
188+
.OUTPUTS
189+
PSCustomObject with Resolved, indicating whether the lookup ran, and PullRequest, which is
190+
null when the commit has no associated release pull request.
191+
192+
.EXAMPLE
193+
Resolve-ReleasePullRequest -EventName workflow_dispatch -CommitSha $sha -DefaultBranch main `
194+
-IsManualDispatchToDefaultBranch $true -GetAssociatedPullRequest { param($Sha) $pulls }
195+
196+
Resolves the merged pull request for a recovery dispatch so its version label is honoured.
197+
#>
198+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '',
199+
Justification = 'Parameters are used inside a LogGroup script block.')]
200+
[CmdletBinding()]
201+
[OutputType([PSCustomObject])]
202+
param(
203+
# The name of the GitHub event that triggered the workflow.
204+
[Parameter(Mandatory)]
205+
[string] $EventName,
206+
207+
# The commit the workflow is resolving a release for.
208+
[Parameter()]
209+
[AllowEmptyString()]
210+
[AllowNull()]
211+
[string] $CommitSha,
212+
213+
# The repository default branch a release must target.
214+
[Parameter(Mandatory)]
215+
[string] $DefaultBranch,
216+
217+
# Whether the workflow was triggered by a push to the default branch.
218+
[Parameter()]
219+
[bool] $IsPushToDefaultBranch,
220+
221+
# Whether the workflow was manually dispatched against the default branch.
222+
[Parameter()]
223+
[bool] $IsManualDispatchToDefaultBranch,
224+
225+
# Returns the pull requests GitHub associates with a commit. Takes the commit SHA.
226+
[Parameter(Mandatory)]
227+
[scriptblock] $GetAssociatedPullRequest
228+
)
229+
230+
$isPush = $EventName -eq 'push'
231+
$shouldResolve = (
232+
($isPush -or $IsManualDispatchToDefaultBranch) -and
233+
-not [string]::IsNullOrWhiteSpace($CommitSha)
234+
)
235+
if (-not $shouldResolve) {
236+
return [pscustomobject]@{ Resolved = $false; PullRequest = $null }
237+
}
238+
239+
LogGroup "Resolve pull request for commit [$CommitSha]" {
240+
$associated = @((& $GetAssociatedPullRequest $CommitSha) | Where-Object { $null -ne $_ })
241+
$pullRequest = Select-PullRequestForPush -PullRequest $associated `
242+
-DefaultBranch $DefaultBranch `
243+
-CommitSha $CommitSha
244+
245+
if ($pullRequest) {
246+
Write-Host "Resolved pull request #$($pullRequest.Number) from commit [$CommitSha]."
247+
return [pscustomobject]@{ Resolved = $true; PullRequest = $pullRequest }
248+
}
249+
250+
# Only a release-bearing event can publish a wrong version. A push to a feature branch has
251+
# no release to get wrong, and its commit is legitimately claimed by an open pull request.
252+
$isReleaseEvent = $IsPushToDefaultBranch -or $IsManualDispatchToDefaultBranch
253+
$discarded = if ($isReleaseEvent) {
254+
@(Get-DiscardedReleasePullRequest -PullRequest $associated `
255+
-DefaultBranch $DefaultBranch `
256+
-CommitSha $CommitSha)
257+
} else {
258+
@()
259+
}
260+
if ($discarded.Count -gt 0) {
261+
throw (
262+
"Commit [$CommitSha] cannot be released because its version label cannot be determined. " +
263+
'The following merged pull request(s) are associated with it but none matches the commit ' +
264+
"being released: $($discarded -join '; '). " +
265+
'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.'
268+
)
269+
}
270+
271+
Write-Host "::notice::No pull request is associated with commit [$CommitSha]."
272+
[pscustomobject]@{ Resolved = $true; PullRequest = $null }
273+
}
274+
}
275+
209276
function Get-FilesFromGitTree {
210277
<#
211278
.SYNOPSIS

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

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -247,51 +247,24 @@ LogGroup 'Calculate Job Run Conditions:' {
247247
# resolve the same pull request and honour the same version label. Gating this lookup on
248248
# $isPush alone left $pullRequest null for a dispatch, which silently downgraded a labelled
249249
# Major or Minor release to a Patch bump through the AutoPatching fallback.
250-
$shouldResolvePullRequest = Test-ShouldResolveAssociatedPullRequest -IsPush $isPush `
251-
-IsManualDispatchToDefaultBranch $isManualDispatchToDefaultBranch `
252-
-CommitSha $commitSha
253-
if ($shouldResolvePullRequest) {
254-
LogGroup "Resolve pull request for commit [$commitSha]" {
250+
$resolveParams = @{
251+
EventName = $eventName
252+
CommitSha = $commitSha
253+
DefaultBranch = $defaultBranch
254+
IsPushToDefaultBranch = $isPushToDefaultBranch
255+
IsManualDispatchToDefaultBranch = $isManualDispatchToDefaultBranch
256+
GetAssociatedPullRequest = {
257+
param($Sha)
255258
$owner = $env:GITHUB_REPOSITORY_OWNER
256259
$repo = $env:GITHUB_REPOSITORY_NAME
257-
$response = Invoke-GitHubAPI -ApiEndpoint "/repos/$owner/$repo/commits/$commitSha/pulls" -Method GET
258-
$associatedPullRequests = @($response.Response | Where-Object { $null -ne $_ })
259-
$pullRequest = Select-PullRequestForPush -PullRequest $associatedPullRequests `
260-
-DefaultBranch $defaultBranch `
261-
-CommitSha $commitSha
262-
263-
if ($pullRequest) {
264-
Write-Host "Resolved pull request #$($pullRequest.Number) from commit [$commitSha]."
265-
} else {
266-
# Distinguish 'no pull request carries release intent' from 'a merged default-branch
267-
# pull request exists but was not selected'. The first is a legitimate direct push to
268-
# the default branch, which the direct-release path handles with the default patch
269-
# bump. The second means the commit carries a version label that would be discarded,
270-
# and a wrong version published to the PowerShell Gallery cannot be reclaimed, so
271-
# fail loudly instead. Only release-bearing events are checked; a push to a feature
272-
# branch has no release to get wrong.
273-
$isReleaseEvent = $isPushToDefaultBranch -or $isManualDispatchToDefaultBranch
274-
$discardedPullRequests = if ($isReleaseEvent) {
275-
@(Get-DiscardedReleasePullRequest -PullRequest $associatedPullRequests `
276-
-DefaultBranch $defaultBranch `
277-
-CommitSha $commitSha)
278-
} else {
279-
@()
280-
}
281-
if ($discardedPullRequests.Count -gt 0) {
282-
throw (
283-
"Commit [$commitSha] cannot be released because its version label cannot be determined. " +
284-
'The following merged pull request(s) are associated with it but none matches the commit ' +
285-
"being released: $($discardedPullRequests -join '; '). " +
286-
'Refusing to fall back to a patch bump, because a wrong version published to the ' +
287-
'PowerShell Gallery cannot be reclaimed. Re-run the workflow against the merge commit ' +
288-
'of the pull request you intend to release.'
289-
)
290-
}
291-
Write-Host "::notice::No pull request is associated with commit [$commitSha]."
292-
}
260+
$response = Invoke-GitHubAPI -ApiEndpoint "/repos/$owner/$repo/commits/$Sha/pulls" -Method GET
261+
$response.Response
293262
}
294263
}
264+
$resolution = Resolve-ReleasePullRequest @resolveParams
265+
if ($resolution.Resolved) {
266+
$pullRequest = $resolution.PullRequest
267+
}
295268

296269
$pullRequestIsMerged = if ($null -eq $pullRequest) {
297270
$false

0 commit comments

Comments
 (0)