Skip to content

🪲 [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 minor should release 1.4.0 from 1.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
  1. PSWEE PR 🚀 [Feature]: Enter the Matrix #12, labelled Minor, merged as f8c59e6. Latest GitHub release and Gallery version were both 1.3.0.
  2. The push run for f8c59e6 (run 33551501626) was cancelled one second after starting, because that caller still used cancel-in-progress: true with a github.ref-only concurrency key.
  3. Recovery was attempted as a workflow_dispatch on main. The Plan job logged "Using direct default-branch release context with the default patch bump" and resolved 1.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:

  1. 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.
  2. Capture the resolved-version evidence from the Plan job.
  3. Fix in Process-PSModule with unit coverage in Get-PSModuleSettings and Resolve-PSModuleVersion tests.
  4. Retarget the test module's caller at the fix branch and confirm the label is honoured.
  5. Revert the caller to @v8 once 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

Activity

  1. MariusStorhaug commented on Sep 2, 2026

    @MariusStorhaug
    MemberAuthor

    Reproduced

    Confirmed on MariusStorhaug/MariusTestModule. Pull request #64, labelled Minor, merged as 705564d. Latest release was v0.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 AssociatedPullRequest Bump Resolved version
    push 33611936528 64 Minor v0.5.0 ✅
    workflow_dispatch 33612007719 (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 off main whose only change forces WhatIf: true on all three publish steps in Publish-Module.yml. Publish-PSResource, gh release create, and prerelease cleanup are all inside if ($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.ps1 for both events:

    push workflow_dispatch
    $commitSha abc123 abc123
    $isManualDispatchToDefaultBranch False True
    gate $isPush -and $commitSha True False

    $commitSha is populated from GITHUB_SHA and $isManualDispatchToDefaultBranch is already True one 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 $pullRequest null, Context.PullRequest serialises as null. Get-GitHubPullRequest then takes its IsPushToDefaultBranch -or IsManualDispatchToDefaultBranch branch and returns Labels = @() with IsDirectRelease = $true — the "Using direct default-branch release context with the default patch bump" line above. In Resolve-ReleaseDecision, empty labels make $majorRelease and $minorRelease false, so $patchRelease becomes true via $Configuration.AutoPatching -or $isDirectStableRelease. Calling it directly with each context:

    PullRequest argument Major Minor Patch
    Labels = @(); IsDirectRelease = $true False 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-PullRequestForPush needs no change. The GitHub commit association endpoint returns PR #64 for 705564d with base.ref = main, merged_at set, and merge_commit_sha exactly equal to 705564d. 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 629ea31 returning 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.
  2. MariusStorhaug commented on Sep 2, 2026

    @MariusStorhaug
    MemberAuthor

    Post-fix validation

    Validated against the same commit as the reproduction, so the only variable is the fix.

    MariusStorhaug/MariusTestModule main stayed at 705564d — the merge commit of Minor-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 705564d 705564d
    Event workflow_dispatch workflow_dispatch
    AssociatedPullRequest (empty) 64
    Bump 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, because WhatIf was forced on for both runs.

    For reference, the push run on the same commit against the unfixed code already resolved v0.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
    705564d push 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_sha is 705564d fails 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.yml configuration (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 $isPush 3
    No-pull-request guard disabled 2
    Both 4

    Remaining work

    Step 5 of the plan: revert the test module's caller to @v8 once the fix ships in a tag, and delete the temporary WhatIf branches.

  3. MariusStorhaug commented on Sep 2, 2026

    @MariusStorhaug
    MemberAuthor

    Cleanup done

    MariusStorhaug/MariusTestModule is back on @v8 (#65, merged). The restore run resolved ReleaseType: None with HasImportantChanges: false, so it triggered no release, and the test module is not left pinned to a feature branch.

    The temporary WhatIf branches used for the reproduction and validation are deleted. Neither run published anything: the Gallery and the latest GitHub release are both still 0.4.13.

    #531 carries only the fix and its tests, with no WhatIf changes. It stays in draft pending review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions