@@ -180,6 +180,170 @@ function Select-PullRequestForPush {
180180 Select-Object - First 1
181181}
182182
183+ function Get-DiscardedReleasePullRequest {
184+ <#
185+ . SYNOPSIS
186+ Reports merged default-branch pull requests whose version label would be discarded.
187+
188+ . DESCRIPTION
189+ Select-PullRequestForPush returns nothing both when a commit has no associated pull request
190+ and when every associated pull request fails its criteria. Only the first case is safe: a
191+ commit pushed directly to the default branch has no label to honour, so the direct-release
192+ path applies the default patch bump.
193+
194+ The unsafe case is a pull request that was merged into the default branch, and therefore
195+ carries the version label that was meant to drive the release, but was not selected because
196+ its merge commit does not match the commit being released. Falling back to a patch bump
197+ there publishes a version nobody asked for, and a PowerShell Gallery version cannot be
198+ reclaimed, so the caller must fail instead.
199+
200+ Pull requests that are not merged are ignored. The GitHub commit association endpoint also
201+ returns open pull requests whose branch contains the commit, which is expected and carries
202+ no release intent.
203+
204+ . OUTPUTS
205+ String, one description per merged pull request that was rejected. Nothing when the
206+ associated pull requests carry no release intent.
207+
208+ . EXAMPLE
209+ Get-DiscardedReleasePullRequest -PullRequest $associated -DefaultBranch main -CommitSha $sha
210+
211+ Returns '#412 was merged into [main] with merge commit [abc123]'.
212+ #>
213+ [CmdletBinding ()]
214+ [OutputType ([string ])]
215+ param (
216+ # The pull requests the GitHub API associated with the commit.
217+ [Parameter ()]
218+ [object []] $PullRequest ,
219+
220+ # The repository default branch a release must target.
221+ [Parameter (Mandatory )]
222+ [string ] $DefaultBranch ,
223+
224+ # The commit the workflow is resolving a release for.
225+ [Parameter (Mandatory )]
226+ [string ] $CommitSha
227+ )
228+
229+ foreach ($candidate in ($PullRequest | Where-Object { $null -ne $_ })) {
230+ $isMergedToDefaultBranch = (
231+ $candidate.Base.Ref -eq $DefaultBranch -and
232+ -not [string ]::IsNullOrWhiteSpace($candidate.merged_at )
233+ )
234+ if (-not $isMergedToDefaultBranch ) { continue }
235+ if ($candidate.merge_commit_sha -eq $CommitSha ) { continue }
236+
237+ " #$ ( $candidate.Number ) was merged into [$DefaultBranch ] with merge commit [$ ( $candidate.merge_commit_sha ) ]"
238+ }
239+ }
240+
241+ function Resolve-ReleasePullRequest {
242+ <#
243+ . SYNOPSIS
244+ Resolves the pull request whose version label drives the release for a commit.
245+
246+ . DESCRIPTION
247+ A push resolves the pull request associated with the pushed commit so a default-branch
248+ release honours the merged pull request's version label. A manual dispatch on the default
249+ branch is the documented recovery route for a failed or cancelled release run and targets
250+ the same merge commit, so it must resolve the same pull request. Excluding it left the pull
251+ request unresolved, and the version silently fell back to a patch bump through AutoPatching.
252+
253+ When no pull request is selected, the outcome depends on why. A commit pushed directly to
254+ the default branch has no label to honour, so the release proceeds with the default patch
255+ bump. A commit associated with a merged default-branch pull request that does not match it
256+ does carry a label, and applying a patch bump would publish a version nobody asked for. A
257+ PowerShell Gallery version cannot be reclaimed, so that case throws instead.
258+
259+ . OUTPUTS
260+ PSCustomObject with Resolved, indicating whether the lookup ran, and PullRequest, which is
261+ null when the commit has no associated release pull request.
262+
263+ . EXAMPLE
264+ Resolve-ReleasePullRequest -EventName workflow_dispatch -CommitSha $sha -DefaultBranch main `
265+ -IsManualDispatchToDefaultBranch $true -GetAssociatedPullRequest { param($Sha) $pulls }
266+
267+ Resolves the merged pull request for a recovery dispatch so its version label is honoured.
268+ #>
269+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute (' PSReviewUnusedParameter' , ' ' ,
270+ Justification = ' Parameters are used inside a LogGroup script block.' )]
271+ [CmdletBinding ()]
272+ [OutputType ([PSCustomObject ])]
273+ param (
274+ # The name of the GitHub event that triggered the workflow.
275+ [Parameter (Mandatory )]
276+ [string ] $EventName ,
277+
278+ # The commit the workflow is resolving a release for.
279+ [Parameter ()]
280+ [AllowEmptyString ()]
281+ [AllowNull ()]
282+ [string ] $CommitSha ,
283+
284+ # The repository default branch a release must target.
285+ [Parameter (Mandatory )]
286+ [string ] $DefaultBranch ,
287+
288+ # Whether the workflow was triggered by a push to the default branch.
289+ [Parameter ()]
290+ [bool ] $IsPushToDefaultBranch ,
291+
292+ # Whether the workflow was manually dispatched against the default branch.
293+ [Parameter ()]
294+ [bool ] $IsManualDispatchToDefaultBranch ,
295+
296+ # Returns the pull requests GitHub associates with a commit. Takes the commit SHA.
297+ [Parameter (Mandatory )]
298+ [scriptblock ] $GetAssociatedPullRequest
299+ )
300+
301+ $isPush = $EventName -eq ' push'
302+ $shouldResolve = (
303+ ($isPush -or $IsManualDispatchToDefaultBranch ) -and
304+ -not [string ]::IsNullOrWhiteSpace($CommitSha )
305+ )
306+ if (-not $shouldResolve ) {
307+ return [pscustomobject ]@ { Resolved = $false ; PullRequest = $null }
308+ }
309+
310+ LogGroup " Resolve pull request for commit [$CommitSha ]" {
311+ $associated = @ ((& $GetAssociatedPullRequest $CommitSha ) | Where-Object { $null -ne $_ })
312+ $pullRequest = Select-PullRequestForPush - PullRequest $associated `
313+ - DefaultBranch $DefaultBranch `
314+ - CommitSha $CommitSha
315+
316+ if ($pullRequest ) {
317+ Write-Host " Resolved pull request #$ ( $pullRequest.Number ) from commit [$CommitSha ]."
318+ return [pscustomobject ]@ { Resolved = $true ; PullRequest = $pullRequest }
319+ }
320+
321+ # Only a release-bearing event can publish a wrong version. A push to a feature branch has
322+ # no release to get wrong, and its commit is legitimately claimed by an open pull request.
323+ $isReleaseEvent = $IsPushToDefaultBranch -or $IsManualDispatchToDefaultBranch
324+ $discarded = if ($isReleaseEvent ) {
325+ @ (Get-DiscardedReleasePullRequest - PullRequest $associated `
326+ - DefaultBranch $DefaultBranch `
327+ - CommitSha $CommitSha )
328+ } else {
329+ @ ()
330+ }
331+ if ($discarded.Count -gt 0 ) {
332+ throw (
333+ " Commit [$CommitSha ] cannot be released because its version label cannot be determined. " +
334+ ' The following merged pull request(s) are associated with it but none matches the commit ' +
335+ " being released: $ ( $discarded -join ' ; ' ) . " +
336+ ' Refusing to fall back to a patch bump, because a wrong version published to the ' +
337+ ' PowerShell Gallery cannot be reclaimed. Re-run the workflow against the merge commit ' +
338+ ' of the pull request you intend to release.'
339+ )
340+ }
341+
342+ Write-Host " ::notice::No pull request is associated with commit [$CommitSha ]."
343+ [pscustomobject ]@ { Resolved = $true ; PullRequest = $null }
344+ }
345+ }
346+
183347function Get-FilesFromGitTree {
184348 <#
185349 . SYNOPSIS
0 commit comments