Skip to content

🪲 [Fix]: Manual dispatch on the default branch honours the merged pull request's version label - #531

Open
Marius Storhaug (MariusStorhaug) wants to merge 2 commits into
mainfrom
fix-dispatch-version-label
Open

🪲 [Fix]: Manual dispatch on the default branch honours the merged pull request's version label#531
Marius Storhaug (MariusStorhaug) wants to merge 2 commits into
mainfrom
fix-dispatch-version-label

Conversation

@MariusStorhaug

@MariusStorhaug Marius Storhaug (MariusStorhaug) commented Sep 2, 2026

Copy link
Copy Markdown
Member

A manual dispatch on the default branch now resolves the merged pull request for the dispatched commit and applies its version label, instead of discarding the label and silently releasing a patch bump.

Fixed: a recovery dispatch releases the intended version

Manual dispatch on the default branch is the documented recovery route when a release run fails or is cancelled. In that exact scenario the label that determines the version bump was ignored, and the run published a version nobody asked for. A PowerShell Gallery version cannot be reclaimed once taken.

Validated on MariusStorhaug/MariusTestModule. The same commit 705564d, the merge commit of a Minor-labelled pull request, with the latest release at v0.4.13:

Before After
Associated pull request none #64
Bump Patch Minor
Resolved version v0.4.14 v0.5.0

A push of the same commit already resolved v0.5.0, so a dispatch now behaves the same as the push it replaces.

No repository configuration changes are needed. A workflow that previously resolved the wrong version on a recovery dispatch resolves the intended version on the next run.

A dispatch on a commit pushed directly to the default branch still releases the default patch bump, because there is no label to honour.

Added: a version label that cannot be determined now fails the run

When the commit being released belongs to a pull request that was merged into the default branch but is not that pull request's merge commit, the version label cannot be determined. The run now stops with an actionable message instead of assuming a patch bump:

Commit [629ea31...] 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: #64 was merged into [main] with merge commit [705564d...]. 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.

A loud failure is strictly better than a silently wrong version that cannot be withdrawn.


Technical details
  • .github/actions/Get-PSModuleSettings/src/main.ps1 — pull request association was gated on if ($isPush -and $commitSha). For a workflow_dispatch the gate was false even though both facts it needed were already true one line above: $commitSha is populated from GITHUB_SHA for every non-push event, and $isManualDispatchToDefaultBranch was already computed. The event was fully recognised; only the lookup was skipped. With $pullRequest left null, Context.PullRequest serialised as null, Get-GitHubPullRequest took its IsPushToDefaultBranch -or IsManualDispatchToDefaultBranch branch and returned Labels = @() with IsDirectRelease = $true, and Resolve-ReleaseDecision then set $patchRelease through $Configuration.AutoPatching -or $isDirectStableRelease. The label never reached the decision, so a recovery dispatch was indistinguishable from a direct push. The gate is now ($isPush -or $IsManualDispatchToDefaultBranch).
  • Select-PullRequestForPush needed no change. The commit association endpoint returns the pull request for a squash-merge commit with base.ref equal to the default branch, merged_at set, and merge_commit_sha exactly equal to the released commit, so all three of its criteria already hold. Confirmed against the live API for 705564d (#64) and 68696b6 (🪲 [Fix]: New module versions publish to the PowerShell Gallery #529).
  • The no-pull-request case is decided deliberately, and not as the issue first proposed. Failing whenever no pull request resolves would break a legitimate direct push to the default branch, which has no label to honour and correctly releases a patch bump. The distinction encoded instead is the one the issue itself identifies: no pull request carries release intent (proceed) versus a merged default-branch pull request exists but does not match the commit (fail). New Get-DiscardedReleasePullRequest reports only the second case.
  • Two cases are deliberately not failures, both verified against live API responses rather than assumed. The association endpoint also returns open pull requests whose branch contains the commit — confirmed for 629ea31 returning #64 while still open — so treating those as failures would break every direct default-branch push made while a pull request is open. Pushes to non-default branches pass through the same gate but have no release to get wrong, so the guard is scoped to $isPushToDefaultBranch -or $isManualDispatchToDefaultBranch.
  • .github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 — the gate, the selection, and the no-pull-request decision were inline in main.ps1, which needs a large environment and a live API to run, so the composed logic that produced the wrong version could not be tested. They are now Resolve-ReleasePullRequest, with the GitHub lookup injected as a [scriptblock]. The behaviour is covered directly instead of being re-implemented in a test, which would have proved nothing about the shipped code. The LogGroup boundary is safe here: Set-GitHubLogGroup dot-sources its script block, verified empirically, so assignments and throw propagate to the caller.
  • @($response.Response) became @($response.Response | Where-Object { $null -ne $_ }). @($null) has Count 1, not 0, so an empty association response produced a single-element array containing $null. Select-PullRequestForPush tolerated it, but the new guard iterates the same collection and would otherwise inspect a null element. Covered by a test that returns $null from the lookup.
  • .github/actions/Get-PSModuleSettings/tests/ and .github/actions/Resolve-PSModuleVersion/tests/ — ten new tests. Each was verified to fail against the specific defect it guards, by reverting each behaviour independently rather than by deleting the new functions, so the assertions are known to catch behaviour and not absence: narrowing the gate back to $isPush fails 3, disabling the no-pull-request guard fails 2, and both together fail 4. The version-action test asserts both versions the two possible dispatch contexts produce, v0.4.14 and v0.5.0, so it pins the actual consequence rather than an intermediate flag.
  • Test traps from 🪲 [Fix]: New module versions publish to the PowerShell Gallery #529 were avoided. Shims are not used here, so Set-Item function:global:X teardown is not involved. Where a test needs to observe whether the injected lookup ran, it uses a hashtable captured by GetNewClosure() rather than a $script: flag, because a flag set inside a script block invoked with & does not propagate out. One authoring trap was hit and fixed during development: { ... }.GetNewClosure() passed as an argument inside a Should -Throw script block is evaluated in the wrong scope and does not capture, so the closure is now assigned to a variable first.
  • The full action suite passes locally under the configuration Test-Actions.yml builds, including Run.Parallel and Run.Shuffle: 90 tests, 0 failures. PSScriptAnalyzer is clean against .github/linters/.powershell-psscriptanalyzer.psd1.
  • Validation used temporary branches that force WhatIf: true on all three publish steps in Publish-Module.yml, so no Gallery version and no GitHub release could be created by any reproduction or validation run. Confirmed before triggering anything, and confirmed after: the Gallery and the latest release are both still 0.4.13. Those branches are not part of this pull request, and are deleted once the fix ships.
Changed surface Standards checked Framework docs checked Result
.github/actions/Get-PSModuleSettings/src/** (PowerShell) Coding standards, error handling Plan stage contract Aligned
.github/actions/Get-PSModuleSettings/tests/** (Pester) Pester test standards Action test layout Aligned
.github/actions/Resolve-PSModuleVersion/tests/** (Pester) Pester test standards Action test layout Aligned
Relevant issues (or links)

Related work

Marius Storhaug and others added 2 commits September 2, 2026 11:08
A workflow_dispatch on the default branch is the documented recovery route for
a failed or cancelled release run. Pull request association was gated on
$isPush, so a dispatch resolved no pull request, saw no version label, and
silently applied the AutoPatching patch fallback.

Widen the gate to cover a manual dispatch on the default branch, so the same
association and selection path runs for the same merge commit.

Also fail loudly when a merged default-branch pull request is associated with
the commit but does not match it, instead of falling back to a patch bump. A
wrong version published to the PowerShell Gallery cannot be reclaimed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🪲 [Fix]: Manual dispatch on the default branch discards the merged pull request's version label

1 participant