Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,170 @@ function Select-PullRequestForPush {
Select-Object -First 1
}

function Get-DiscardedReleasePullRequest {
<#
.SYNOPSIS
Reports merged default-branch pull requests whose version label would be discarded.

.DESCRIPTION
Select-PullRequestForPush returns nothing both when a commit has no associated pull request
and when every associated pull request fails its criteria. Only the first case is safe: a
commit pushed directly to the default branch has no label to honour, so the direct-release
path applies the default patch bump.

The unsafe case is a pull request that was merged into the default branch, and therefore
carries the version label that was meant to drive the release, but was not selected because
its merge commit does not match the commit being released. Falling back to a patch bump
there publishes a version nobody asked for, and a PowerShell Gallery version cannot be
reclaimed, so the caller must fail instead.

Pull requests that are not merged are ignored. The GitHub commit association endpoint also
returns open pull requests whose branch contains the commit, which is expected and carries
no release intent.

.OUTPUTS
String, one description per merged pull request that was rejected. Nothing when the
associated pull requests carry no release intent.

.EXAMPLE
Get-DiscardedReleasePullRequest -PullRequest $associated -DefaultBranch main -CommitSha $sha

Returns '#412 was merged into [main] with merge commit [abc123]'.
#>
[CmdletBinding()]
[OutputType([string])]
param(
# The pull requests the GitHub API associated with the commit.
[Parameter()]
[object[]] $PullRequest,

# The repository default branch a release must target.
[Parameter(Mandatory)]
[string] $DefaultBranch,

# The commit the workflow is resolving a release for.
[Parameter(Mandatory)]
[string] $CommitSha
)

foreach ($candidate in ($PullRequest | Where-Object { $null -ne $_ })) {
$isMergedToDefaultBranch = (
$candidate.Base.Ref -eq $DefaultBranch -and
-not [string]::IsNullOrWhiteSpace($candidate.merged_at)
)
if (-not $isMergedToDefaultBranch) { continue }
if ($candidate.merge_commit_sha -eq $CommitSha) { continue }

"#$($candidate.Number) was merged into [$DefaultBranch] with merge commit [$($candidate.merge_commit_sha)]"
}
}

function Resolve-ReleasePullRequest {
<#
.SYNOPSIS
Resolves the pull request whose version label drives the release for a commit.

.DESCRIPTION
A push resolves the pull request associated with the pushed commit so a default-branch
release honours the merged pull request's version label. A manual dispatch on the default
branch is the documented recovery route for a failed or cancelled release run and targets
the same merge commit, so it must resolve the same pull request. Excluding it left the pull
request unresolved, and the version silently fell back to a patch bump through AutoPatching.

When no pull request is selected, the outcome depends on why. A commit pushed directly to
the default branch has no label to honour, so the release proceeds with the default patch
bump. A commit associated with a merged default-branch pull request that does not match it
does carry a label, and applying a patch bump would publish a version nobody asked for. A
PowerShell Gallery version cannot be reclaimed, so that case throws instead.

.OUTPUTS
PSCustomObject with Resolved, indicating whether the lookup ran, and PullRequest, which is
null when the commit has no associated release pull request.

.EXAMPLE
Resolve-ReleasePullRequest -EventName workflow_dispatch -CommitSha $sha -DefaultBranch main `
-IsManualDispatchToDefaultBranch $true -GetAssociatedPullRequest { param($Sha) $pulls }

Resolves the merged pull request for a recovery dispatch so its version label is honoured.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '',
Justification = 'Parameters are used inside a LogGroup script block.')]
[CmdletBinding()]
[OutputType([PSCustomObject])]
param(
# The name of the GitHub event that triggered the workflow.
[Parameter(Mandatory)]
[string] $EventName,

# The commit the workflow is resolving a release for.
[Parameter()]
[AllowEmptyString()]
[AllowNull()]
[string] $CommitSha,

# The repository default branch a release must target.
[Parameter(Mandatory)]
[string] $DefaultBranch,

# Whether the workflow was triggered by a push to the default branch.
[Parameter()]
[bool] $IsPushToDefaultBranch,

# Whether the workflow was manually dispatched against the default branch.
[Parameter()]
[bool] $IsManualDispatchToDefaultBranch,

# Returns the pull requests GitHub associates with a commit. Takes the commit SHA.
[Parameter(Mandatory)]
[scriptblock] $GetAssociatedPullRequest
)

$isPush = $EventName -eq 'push'
$shouldResolve = (
($isPush -or $IsManualDispatchToDefaultBranch) -and
-not [string]::IsNullOrWhiteSpace($CommitSha)
)
if (-not $shouldResolve) {
return [pscustomobject]@{ Resolved = $false; PullRequest = $null }
}

LogGroup "Resolve pull request for commit [$CommitSha]" {
$associated = @((& $GetAssociatedPullRequest $CommitSha) | Where-Object { $null -ne $_ })
$pullRequest = Select-PullRequestForPush -PullRequest $associated `
-DefaultBranch $DefaultBranch `
-CommitSha $CommitSha

if ($pullRequest) {
Write-Host "Resolved pull request #$($pullRequest.Number) from commit [$CommitSha]."
return [pscustomobject]@{ Resolved = $true; PullRequest = $pullRequest }
}

# Only a release-bearing event can publish a wrong version. A push to a feature branch has
# no release to get wrong, and its commit is legitimately claimed by an open pull request.
$isReleaseEvent = $IsPushToDefaultBranch -or $IsManualDispatchToDefaultBranch
$discarded = if ($isReleaseEvent) {
@(Get-DiscardedReleasePullRequest -PullRequest $associated `
-DefaultBranch $DefaultBranch `
-CommitSha $CommitSha)
} else {
@()
}
if ($discarded.Count -gt 0) {
throw (
"Commit [$CommitSha] cannot be released because its version label cannot be determined. " +
'The following merged pull request(s) are associated with it but none matches the commit ' +
"being released: $($discarded -join '; '). " +
'Refusing to fall back to a patch bump, because a wrong version published to the ' +
'PowerShell Gallery cannot be reclaimed. Re-run the workflow against the merge commit ' +
'of the pull request you intend to release.'
)
}

Write-Host "::notice::No pull request is associated with commit [$CommitSha]."
[pscustomobject]@{ Resolved = $true; PullRequest = $null }
}
}

function Get-FilesFromGitTree {
<#
.SYNOPSIS
Expand Down
32 changes: 19 additions & 13 deletions .github/actions/Get-PSModuleSettings/src/main.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -242,23 +242,29 @@ LogGroup 'Calculate Job Run Conditions:' {
$isManualDispatchToDefaultBranch = $isManualDispatch -and $workflowRef -eq $defaultBranch
$pullRequest = $eventData.PullRequest

if ($isPush -and $commitSha) {
LogGroup "Resolve pull request for commit [$commitSha]" {
# A manual dispatch on the default branch is the documented recovery route for a failed or
# cancelled release run. It targets the same merge commit as the push it replaces, so it must
# resolve the same pull request and honour the same version label. Gating this lookup on
# $isPush alone left $pullRequest null for a dispatch, which silently downgraded a labelled
# Major or Minor release to a Patch bump through the AutoPatching fallback.
$resolveParams = @{
EventName = $eventName
CommitSha = $commitSha
DefaultBranch = $defaultBranch
IsPushToDefaultBranch = $isPushToDefaultBranch
IsManualDispatchToDefaultBranch = $isManualDispatchToDefaultBranch
GetAssociatedPullRequest = {
param($Sha)
$owner = $env:GITHUB_REPOSITORY_OWNER
$repo = $env:GITHUB_REPOSITORY_NAME
$response = Invoke-GitHubAPI -ApiEndpoint "/repos/$owner/$repo/commits/$commitSha/pulls" -Method GET
$associatedPullRequests = @($response.Response)
$pullRequest = Select-PullRequestForPush -PullRequest $associatedPullRequests `
-DefaultBranch $defaultBranch `
-CommitSha $commitSha

if ($pullRequest) {
Write-Host "Resolved pull request #$($pullRequest.Number) from commit [$commitSha]."
} else {
Write-Host "::notice::No pull request is associated with commit [$commitSha]."
}
$response = Invoke-GitHubAPI -ApiEndpoint "/repos/$owner/$repo/commits/$Sha/pulls" -Method GET
$response.Response
}
}
$resolution = Resolve-ReleasePullRequest @resolveParams
if ($resolution.Resolved) {
$pullRequest = $resolution.PullRequest
}

$pullRequestIsMerged = if ($null -eq $pullRequest) {
$false
Expand Down
Loading
Loading