Repository navigation
🪲 [Fix]: Manual dispatch on the default branch discards the merged pull request's version label #530
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Sep 2, 2026 - added a commit that references this issue
on Sep 2, 2026 MariusStorhaug commented
on Sep 2, 2026 MemberAuthorMore actionsReproduced
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:Event Run AssociatedPullRequestBump Resolved version push33611936528 64Minor v0.5.0 ✅ workflow_dispatch33612007719 (empty) Patch v0.4.14 ❌ Plan log from the push run:
Resolved pull request #64 from commit [705564d8a3634bed2ea91f55cbb8093081cb6714]. IsPushToDefaultBranch : True AssociatedPullRequest : 64 Minor : True New version: [v0.5.0]Plan log from the dispatch run on the identical commit:
GITHUB_EVENT_NAME : workflow_dispatch IsManualDispatchToDefaultBranch : True AssociatedPullRequest : Using direct default-branch release context with the default patch bump. Minor : False Patch : True New version: [v0.4.14]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$isManualDispatchToDefaultBranchFalseTruegate $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:PullRequestargumentMajor Minor Patch Labels = @(); IsDirectRelease = $trueFalse False True Labels = @('Minor')False True False 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:
- Open pull requests. The association endpoint also returns open pull requests whose branch contains the commit — confirmed for
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. - Pushes to non-default branches. They pass through the same gate but have no release to get wrong.
- Open pull requests. The association endpoint also returns open pull requests whose branch contains the commit — confirmed for
MariusStorhaug commented
on Sep 2, 2026 MemberAuthorMore actionsPost-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:Before After Run 33612007719 33613240982 Commit 705564d705564dEvent workflow_dispatchworkflow_dispatchAssociatedPullRequest(empty) 64Bump Patch Minor Resolved version v0.4.14❌v0.5.0✅Plan log after the fix:
Resolved pull request #64 from commit [705564d8a3634bed2ea91f55cbb8093081cb6714]. GITHUB_EVENT_NAME : workflow_dispatch IsManualDispatchToDefaultBranch : True AssociatedPullRequest : 64 ReleaseType : Release Minor : True Patch : False New version: [v0.5.0]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:
Commit Event Associated Outcome 705564d(merge commit of #64)dispatch on main#64 merged, matching resolves #64 705564dpush to main#64 merged, matching resolves #64 629ea31(head of #64, before merge)push to feature branch #64 proceeds, no pull request 629ea31(head of #64, after merge)dispatch on main#64 merged, merge_commit_shais705564dfails loudly 68696b6(merge commit of #529)dispatch on main#529 merged, matching resolves #529 The 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:
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.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:
Reverted behaviour Failing tests Gate narrowed back to $isPush3 No-pull-request guard disabled 2 Both 4 Remaining work
Step 5 of the plan: revert the test module's caller to
@v8once the fix ships in a tag, and delete the temporaryWhatIfbranches.- added a commit that references this issue
on Sep 2, 2026 MariusStorhaug commented
on Sep 2, 2026 MemberAuthorMore actionsCleanup 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.
A
workflow_dispatchrun 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:
minorshould release1.4.0from1.3.0.1.3.1.1.3.1then permanently occupies the Gallery. The intended1.4.0can 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/PSWEEwhile diagnosing #528:Minor, merged asf8c59e6. Latest GitHub release and Gallery version were both1.3.0.pushrun forf8c59e6(run33551501626) was cancelled one second after starting, because that caller still usedcancel-in-progress: truewith agithub.ref-only concurrency key.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.ps1gates pull request association on$isPush:$commitShais populated for a manual dispatch (fromGITHUB_SHA), and$isManualDispatchToDefaultBranchis computed on the line above, so the event is already recognised. Only the lookup is skipped.With
$pullRequestleft null,Resolve-ReleaseDecisionsees no labels.$majorReleaseand$minorReleaseare both false, so the$patchReleasefallback applies viaAutoPatchingor$isDirectStableRelease, and the run resolves a patch bump.Expected behaviour
A
workflow_dispatchon the default branch should resolve the pull request associated with the dispatched commit and honour its version label, exactly as the equivalentpushdoes. The sameSelect-PullRequestForPushselection 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:
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/MariusTestModulewill be used as the consuming repository, following the approach that validated #528 and #529:v8, showing a labelled merged pull request whose label is discarded by a manual dispatch.Process-PSModulewith unit coverage inGet-PSModuleSettingsandResolve-PSModuleVersiontests.@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
prereleaselabel orWhatIfso the wrong resolved version is observable without being published.Related
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.