Repository navigation
Default-branch recovery retains the merged pull request's release intent - #531
Marius Storhaug (MariusStorhaug) wants to merge 6 commits into
Conversation
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>
There was a problem hiding this comment.
🟡 Changes recommended
Dispatch recovery can resolve the wrong commit and delete another PR’s prereleases, while documentation-only pushes can fail unnecessarily.
5 open issues
Document that dispatch recovery only targets the current branch tip · New Match prerelease cleanup to the exact owning PR · New Check for release-worthy changes before requiring a merged PR · New Add a dispatch test for the NoRelease label · New Update release guidance for PR-based dispatch decisions · New
What changed in this PR
This PR addresses #530 by making default-branch manual dispatches resolve the merged pull request associated with the dispatched commit, preserving its version-label intent in the release pipeline.
Changes:
- Shares commit-to-PR resolution between pushes and default-branch dispatches.
- Rejects unmatched merged-PR associations while preserving legitimate direct releases and propagating lookup failures.
- Adds association regressions and composed version-resolution tests without changing public workflow inputs or versioning defaults.
| File | Description |
|---|---|
.github/actions/Resolve-PSModuleVersion/tests/Resolve-PSModuleVersion.Helpers.Tests.ps1 |
Adds dispatch-context and composed version-resolution tests. |
.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 |
Covers association selection, rejection, direct releases, and lookup failures. |
.github/actions/Get-PSModuleSettings/src/main.ps1 |
Wires event context and the association API into the shared resolver. |
.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 |
Adds release-PR resolution and unmatched-association safeguards. |
Review effort: Balanced
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
There was a problem hiding this comment.
🟡 Changes recommended
Release recovery can double-bump versions or overwrite newer artifacts, while ambiguous ref handling can trigger unintended stable releases or delete unrelated prereleases and tags.
4 open issues
5 resolved since last review
Review effort: Balanced
| if ($candidate.targetCommitish -ceq $HeadRef) { | ||
| $candidate |
| Write-Host 'Non-PR event - treating as having important changes' | ||
| } | ||
|
|
||
| # Dispatch resolves the current default-branch tip. Rerun the original workflow for an older merge. |
| $response.Response | ||
| } | ||
| } | ||
| $resolution = Resolve-ReleasePullRequest @resolveParams |
| '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. Rerun the original workflow for the intended merge commit. ' + |



Default-branch recovery retains the merged pull request's configured bump or skip label instead of silently treating its commit as a direct patch release. No caller configuration or invocation edit is required beyond selecting this compatible release; dispatch can recover a merge only while it remains the default branch's tip.
Fixed: Recovery retains the intended release without cleaning another branch
A default-branch push and a recovery dispatch use the same exact merge-SHA association. A matching merged PR supplies its labels and release metadata, including configured skip intent. A legitimate direct release remains supported. Unmatched merged associations stop an important release-capable push or a dispatch; a documentation-only push that cannot publish is not rejected for that association. Lookup failures propagate instead of being interpreted as an empty result.
For recovery, rerun the original workflow's failed jobs to retain its SHA, resolved outputs, and successful build artifacts. A new dispatch uses the current default-branch tip, not an arbitrary older SHA. If the branch has advanced, rerun the original workflow rather than dispatching the newer tip or selecting an old tag; a tag dispatch does not satisfy default-branch recovery. No new recovery-SHA input is introduced.
Prerelease cleanup uses the exact unsanitized GitHub release target branch recorded by
Release-PSModule. It no longer guesses ownership from a substring in the tag. Overlapping branches, sanitized-name collisions, numeric branch/counter collisions, stable releases, and the current published tag are retained unless the existing branch-based cleanup contract establishes eligibility. Normal framework callers need no cleanup setting or payload edit. A custom producer that targets prereleases at an immutable SHA rather than the PR branch does not establish eligibility through this branch-based interface; those records are retained, not guessed from tag spelling.WhatIfremains non-mutating.Replacing
AutoPatchingwithDefaultBumpand changing label defaults belongs to the separately scoped #524. Its breaking adoption actions do not apply to this patch.Technical details
The immutable candidate is
38ff9ff788002cfa442173b78588bead728353a1, compared with current main64ffb1d87f86bd5d487891fdb140b78385a00531. It preserves both original commits, reconciles the current base through ordinary commits, and includes the tightly coupled cleanup correction found during review. No released source, tag, Gallery version, label history, or ruleset is changed.Get-PSModuleSettingsdetects important push files before resolving release-bearing association. Actual-controller tests prove the compare endpoint runs before commit-to-PR lookup, a documentation-only unmatched association succeeds without publication, and the same important push fails. Ten composed push/dispatch cases retain major, minor, patch, direct, and skip intent through the real association and decision helpers.Cleanup reads every GitHub release page and selects only exact
target_commitishmatches. The existing producer's--target <PR head branch>interface is verified inRelease-PSModule/src/release.ps1; tag text is not treated as ownership evidence. Three controller regressions reproduce the old cross-branch deletions before the fix and pass with the exact-target selector. The newly covered review defects produce four failing cases before correction. Restoring the original push-only gate separately produces nine failing regressions.Local evidence: 93 focused tests and the full 108-test action suite pass; shuffled parallel containers are exercised. PowerShell analysis is clean and the Zensical documentation build reports no issues. Logging uses the full GitHub cmdlet, and Pester contexts are module-qualified to avoid dependency-dependent alias diagnostics rather than relying on unmatched suppressions. Validation uses synthetic fixtures and local command replacements; it does not publish or delete real releases.
Compatibility evidence: caller inputs, settings mappings, secrets, permissions, runtimes, and direct-release policy are unchanged. No external integration template is changed. The repository's default and with-manifest fixture workflows retain their existing interfaces; current-head hosted fixture checks and the new explicit Copilot review round remain review evidence, not a claim that unmerged source is delivered.
All five findings from the first explicit Copilot round are addressed: tip-only recovery guidance, exact cleanup target, important-change gating, skip coverage, and public dispatch documentation. The complete summary was read; it contained no additional suppressed findings. A new round and current-head hosted checks are required before readiness. Main's live rules expose no
required_status_checksrule, so enforcement is a genuine remaining gate, not something this documentation fixes.Scoped issue convergence: #530 is delivered here. #529 is related recovery context, not an additional closing claim. Intended order is this patch first, then #524 after refreshing and validating its composed settings changes. Existing artifact-isolation and later authoring/skills work remain separately owned.
Related references
Release decisions
release:patchintent. The currently released native self-release reader is still Release-GHRepository v2.0.3 and reads the unchanged legacyPatch/Fixmappings; those agree on patch while the owned-label caller follow-up is pending.release:patch. Existing non-owned compatibility labels are retained; no label cleanup is performed. The draft is not ready while current-head review/checks or required release-decision enforcement remain open.