Repository navigation
🪲 [Fix]: Manual dispatch on the default branch discards the merged pull request's version label #530
Description
A workflow_dispatch run on the default branch resolves no associated pull request, so the merged pull request's version label is discarded and the version silently falls back to a Patch bump. Recovering a failed release by manual dispatch therefore publishes the wrong version, and PowerShell Gallery versions cannot be reclaimed once taken.
Impact
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 is ignored:
- A merged pull request labelled
minorshould release1.4.0from1.3.0. - A manual dispatch resolves no pull request, finds no version label, and applies AutoPatching.
- The run publishes
1.3.1.
1.3.1 then permanently occupies the Gallery. The intended 1.4.0 can still be released afterwards, but the incorrect version cannot be removed, and consumers who installed in between receive a release that was never intended to exist.
This is silent. The run succeeds, and the only signal is a log line stating a patch bump was assumed.
Reproduction
Observed in PSModule/PSWEE while diagnosing #528:
| Correct version | 1.4.0 |
| Version attempted | 1.3.1 |
- PSWEE PR 🚀 [Feature]: Enter the Matrix #12, labelled
Minor, merged asf8c59e6. Latest GitHub release and Gallery version were both1.3.0. - The
pushrun forf8c59e6(run33551501626) was cancelled one second after starting, because that caller still usedcancel-in-progress: truewith agithub.ref-only concurrency key. - Recovery was attempted as a
workflow_dispatchonmain. The Plan job logged "Using direct default-branch release context with the default patch bump" and resolved1.3.1.
The cancelled push is a separate caller-configuration problem. This issue is only about step 3: once a recovery dispatch happens, the version resolves incorrectly.
Cause
.github/actions/Get-PSModuleSettings/src/main.ps1 gates pull request association on $isPush:
$isPush = $eventName -eq 'push'
$isManualDispatch = $eventName -eq 'workflow_dispatch'
$commitSha = if ($isPush) { $eventData.After ?? $env:GITHUB_SHA } else { $env:GITHUB_SHA }
if ($isPush -and $commitSha) {
# /repos/{owner}/{repo}/commits/{sha}/pulls -> Select-PullRequestForPush
}$commitSha is populated for a manual dispatch (from GITHUB_SHA), and $isManualDispatchToDefaultBranch is computed on the line above, so the event is already recognised. Only the lookup is skipped.
With $pullRequest left null, Resolve-ReleaseDecision sees no labels. $majorRelease and $minorRelease are both false, so the $patchRelease fallback applies via AutoPatching or $isDirectStableRelease, and the run resolves a patch bump.
Expected behaviour
A workflow_dispatch on the default branch should resolve the pull request associated with the dispatched commit and honour its version label, exactly as the equivalent push does. The same Select-PullRequestForPush selection logic applies — the commit is a merge commit on the default branch in both cases.
If no pull request can be resolved, the run should fail loudly rather than silently assuming Patch. Publishing a wrong version to the Gallery is unrecoverable, so an explicit failure is strictly better than a silent downgrade.
Suggested approach
Widen the gate to cover a manual dispatch targeting the default branch, so the same association and selection path runs:
if (($isPush -or $isManualDispatchToDefaultBranch) -and $commitSha) {Then decide the no-pull-request case deliberately. For a manual dispatch on the default branch, resolving no pull request most likely means the commit was pushed directly, so the existing direct-release path is legitimate. The distinction worth encoding is "no pull request exists" (proceed) versus "a pull request exists but was not consulted" (the current defect).
Validation plan
MariusStorhaug/MariusTestModule will be used as the consuming repository, following the approach that validated #528 and #529:
- Reproduce the defect on the test module against the current
v8, showing a labelled merged pull request whose label is discarded by a manual dispatch. - Capture the resolved-version evidence from the Plan job.
- Fix in
Process-PSModulewith unit coverage inGet-PSModuleSettingsandResolve-PSModuleVersiontests. - Retarget the test module's caller at the fix branch and confirm the label is honoured.
- Revert the caller to
@v8once the fix ships in a tag.
Note that step 1 must avoid publishing an incorrect stable version to the Gallery, since that cannot be undone. The reproduction will use the prerelease label or WhatIf so the wrong resolved version is observable without being published.
Related
- Analysis originally recorded on 🪲[Bug]: Publish Module is failing #528 (that issue tracked and fixed the Gallery existence probe; this defect was found alongside it and is separate).
- Fixed in 🪲 [Fix]: New module versions publish to the PowerShell Gallery #529:
Find-PSResourceprobe made a first-time publication fatal. That is the failure which forced the manual recovery dispatch in the PSWEE case, and therefore exposed this defect.
Activity
🧪 Reproduce Process-PSModule#530: dispatch discards the version label (…
MariusStorhaug commentedon Sep 2, 2026
Reproduced
Confirmed on
MariusStorhaug/MariusTestModule. Pull request #64, labelledMinor, merged as705564d. Latest release wasv0.4.13, so the correct release is 0.5.0.Both runs below are on the same commit
705564d, with the same label. Only the event differs:AssociatedPullRequestpush64workflow_dispatchPlan log from the push run:
Plan log from the dispatch run on the identical commit:
Nothing was published. The test module's caller was pointed at
PSModule/Process-PSModule@repro/dispatch-label-whatif, a temporary branch offmainwhose only change forcesWhatIf: trueon all three publish steps inPublish-Module.yml.Publish-PSResource,gh release create, and prerelease cleanup are all insideif ($whatIf)branches, so no Gallery version and no GitHub release could be created. Verified before triggering anything.Mechanism
The cause in the issue body is correct. Confirmed against the real code, not inferred.
1. The event is fully recognised; only the lookup is skipped. Running the exact expressions from
Get-PSModuleSettings/src/main.ps1for both events:pushworkflow_dispatch$commitShaabc123abc123$isManualDispatchToDefaultBranchFalseTrue$isPush -and $commitShaTrueFalse$commitShais populated fromGITHUB_SHAand$isManualDispatchToDefaultBranchis alreadyTrueone line above the gate. The dispatch is recognised, and its commit is known. The association call is simply never made.2. A null pull request becomes a patch bump. With
$pullRequestnull,Context.PullRequestserialises as null.Get-GitHubPullRequestthen takes itsIsPushToDefaultBranch -or IsManualDispatchToDefaultBranchbranch and returnsLabels = @()withIsDirectRelease = $true— the "Using direct default-branch release context with the default patch bump" line above. InResolve-ReleaseDecision, empty labels make$majorReleaseand$minorReleasefalse, so$patchReleasebecomes true via$Configuration.AutoPatching -or $isDirectStableRelease. Calling it directly with each context:PullRequestargumentLabels = @(); IsDirectRelease = $trueLabels = @('Minor')The label never reaches the decision, so there is nothing to distinguish a recovery dispatch from a direct push.
3.
Select-PullRequestForPushneeds no change. The GitHub commit association endpoint returns PR #64 for705564dwithbase.ref = main,merged_atset, andmerge_commit_shaexactly equal to705564d. All three of its criteria are satisfied, including for a squash merge. Widening the gate is sufficient; the selection logic is already correct for this commit.Fix
Open as a draft in #531.
The gate is widened to
($isPush -or $isManualDispatchToDefaultBranch) -and $commitSha, as suggested.On the no-pull-request case, I followed the evidence rather than the issue body. The body proposes failing loudly whenever no pull request resolves; that would break a legitimate direct push to the default branch, which has no label to honour and correctly releases a patch bump. The distinction the issue itself identifies is the right one, so that is what is encoded: the run fails only when a pull request merged into the default branch is associated with the commit but does not match it. That commit does carry a version label, and applying a patch bump there publishes a version nobody asked for.
Two cases are deliberately not failures, both verified against the live API:
629ea31returning PR #64 while still open. These carry no release intent, so treating them as failures would break every direct default-branch push made while a pull request is open.MariusStorhaug commentedon Sep 2, 2026
Post-fix validation
Validated against the same commit as the reproduction, so the only variable is the fix.
MariusStorhaug/MariusTestModulemainstayed at705564d— the merge commit ofMinor-labelled pull request #64 — for both dispatch runs. The caller reference was repointed from the reproduction branch to the fix. Same commit, same event, same label:705564d705564dworkflow_dispatchworkflow_dispatchAssociatedPullRequest64v0.4.14❌v0.5.0✅Plan log after the fix:
The run completed successfully, and nothing was published: the Gallery and the latest GitHub release are both still
0.4.13, becauseWhatIfwas forced on for both runs.For reference, the
pushrun on the same commit against the unfixed code already resolvedv0.5.0(33611936528). The dispatch now matches the push, which is the expected behaviour in the issue.Guard behaviour against live API data
The new failure path was exercised with real responses from the commit association endpoint, not fixtures:
705564d(merge commit of #64)main705564dmain629ea31(head of #64, before merge)629ea31(head of #64, after merge)mainmerge_commit_shais705564d68696b6(merge commit of #529)mainThe last row of the failure case is the point of the guard. A dispatch on a commit that belongs to a merged pull request but is not its merge commit cannot determine the version label, so it stops with an actionable message instead of publishing an unintended patch version:
Unit coverage
Ten new tests across the two actions. The action suite passes locally under the
Test-Actions.ymlconfiguration (90 tests) and in CI on #531, with all 52 checks green.Each test was verified to fail against the specific defect it guards, by reverting each behaviour independently rather than removing the new functions:
$isPushRemaining work
Step 5 of the plan: revert the test module's caller to
@v8once the fix ships in a tag, and delete the temporaryWhatIfbranches.♻️ Restore Process-PSModule caller to @v8 (#65)
MariusStorhaug commentedon Sep 2, 2026
Cleanup done
MariusStorhaug/MariusTestModuleis back on@v8(#65, merged). The restore run resolvedReleaseType: NonewithHasImportantChanges: false, so it triggered no release, and the test module is not left pinned to a feature branch.The temporary
WhatIfbranches used for the reproduction and validation are deleted. Neither run published anything: the Gallery and the latest GitHub release are both still0.4.13.#531 carries only the fix and its tests, with no
WhatIfchanges. It stays in draft pending review.