Repository navigation
Release decisions use owned labels and effective settings - #109
Marius Storhaug (MariusStorhaug) wants to merge 25 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a strict, “fail-closed” release-decision model based on a canonical, action-owned release: label set, removing legacy/bare label influence and eliminating default/automatic patching behavior.
Changes:
- Adds a PowerShell helper module to define canonical release labels and to resolve/validate release decisions from PR labels.
- Updates the main action logic to (a) provision canonical labels each run and (b) drive release/prerelease/skip decisions exclusively via the resolver.
- Updates documentation, action inputs, and workflows to reflect the new owned-label contract and required events/permissions; adds unit tests for the resolver.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/Release-GHRepository.Helpers.Tests.ps1 |
Adds Pester coverage for canonical label definitions and release-decision resolution/rejection rules. |
src/Release-GHRepository.Helpers.psm1 |
Introduces canonical label definitions and the strict Resolve-ReleaseDecision validator. |
src/main.ps1 |
Provisions canonical labels and replaces legacy label/auto-patching logic with the resolver output. |
README.md |
Documents the owned-label contract, workflow requirements, and v2→v3 migration steps. |
action.yml |
Removes legacy label/auto-patching inputs and clarifies Prerelease input semantics. |
.github/workflows/Release.yml |
Ensures label transitions are evaluated (unlabeled) and grants issues: write for label provisioning. |
.github/workflows/Action-Test.yml |
Adds Pester unit tests and expands PR event types to include unlabeled. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/main.ps1:154
$createPrereleasecan become true for merged/closed pull requests when the PR targets a non-default branch (because$closedPullRequestonly covers closed-but-not-merged). That would create prereleases from closed PRs, conflicting with the intended “open pull request prerelease” behavior.
$createPrerelease = $releaseDecision.Prerelease -and -not $createRelease -and -not $closedPullRequest
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
.github/workflows/Release.yml:19
pull_request_targetsetsgithub.refto the base/default branch (e.g.refs/heads/main), so using${{ github.workflow }}-${{ github.ref }}collapses all PR runs into the same concurrency group. Withcancel-in-progress: true, activity on one PR can cancel an in-progress run for a different PR, which can interrupt release/prerelease creation or cleanup. Use a PR-scoped key (e.g. PR number) to isolate runs per PR (matching the README guidance).
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
Release guidance now defines one owned `release:` label set: `release:patch`, `release:minor`, `release:major`, `release:pre-release`, and `release:skip`. Release automation ignores bare and unrelated labels, so only explicit release decisions affect published versions. ## Changed: One release label contract Release Management, Automation Labels, PR Format, Natural Language examples, and the VS Code Extension Framework now use the same namespaced values. Prereleases use `release:pre-release` with one bump label, while non-releasing changes use `release:skip` alone. ## Changed: Ad hoc releases are optional A labeled pull request merge is the required release path. Implementations may add `workflow_dispatch` only when they need an explicit ad hoc mechanism; it must use the same release controls and require its decision and release-note context as inputs. Empty pull requests and direct pushes are not release workarounds. --- <details> <summary>Technical details</summary> - Replaced `release:none` and the unnamespaced `Prerelease` label. - Defined valid and invalid combinations for the five owned release labels. - Made `workflow_dispatch` an optional implementation extension rather than a mandatory release interface. - Preserved the mainline removal of dependency-tool label guidance while resolving the branch update. | Changed surface | Standards checked | Framework docs checked | Result | | --- | --- | --- | --- | | `src/docs/Capabilities/release-management/**` | Natural Language, Markdown | Release Management | Aligned | | Release-label references under `src/docs/**` | Natural Language, Markdown | Automation Labels, PR Format, VS Code Extension Framework | Aligned | </details> <details> <summary>Relevant issues (or links)</summary> ### Related work - References PSModule/Release-GHRepository#109 - References PSModule/Process-PSModule#524 </details> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/main.ps1:106
- When
gh label createfails during canonical label provisioning, the thrown error doesn’t include theghexit code (unlike othergh/gitfailures in this file). Including$LASTEXITCODEmakes operational failures much easier to diagnose from workflow logs.
gh @arguments
if ($LASTEXITCODE -ne 0) {
throw "Failed to provision the canonical label [$($definition.Name)]."
}
src/Release-GHRepository.Helpers.psm1:263
Resolve-PullRequestReleaseContextderivesPrereleaseNameby stripping non-alphanumerics from the head ref, but it never validates the result. A branch name containing no[A-Za-z0-9]characters would produce an empty prerelease identifier and can later break SemVer/prerelease creation inmain.ps1. Validate the derived prerelease name and fail with a clear error before returning the context.
[PSCustomObject]@{
HeadRef = $headRef
PrereleaseName = $headRef -replace '[^a-zA-Z0-9]'
PrereleaseTarget = $headSha
}
There was a problem hiding this comment.
🟡 Changes recommended
The prerelease-cleanup selection in src/main.ps1 can match and delete non-prerelease (stable) releases because it uses a broad substring tag filter without restricting to isPrerelease.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Approval is blocked by two critical prerelease-cleanup matching bugs that can delete the wrong tags and a moderate skip-path issue that can fail validation unnecessarily.
3 open issues
1 resolved since last review
Review effort: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Human assessment is needed because critical hidden findings indicate the stable-release workflow may fail during authenticated tag pushes despite successfully creating the GitHub Release.
No open issues 🎉
3 resolved since last review
Review effort: Lite
|
Assessed the latest review summary's authenticated-tag-push concern against the actual default bootstrap. The read-only checkout does not leave Git unauthenticated in this path: GitHub-Script v1.9.0 calls Connect-GitHub, and GitHub 0.43.1 initializes the workflow installation context, then configures authenticated Git URL rewrites before the script runs. The actual final-head action job reports GitHub 0.43.1. Two anonymous bootstrap checks pass: they exercise the real context/Git-config functions with API, vault, CLI-login and Git-write boundaries mocked, then use real No code change is needed to enable persisted checkout credentials merely to silence this summary concern. The three reported resolver/cleanup/skip issues are fixed and resolved, and all final-head CI is green. The summary assessment is documented rather than called an automatic approval; the PR remains draft with its real decision-enforcement prerequisite still open. |


Repository and workflow maintainers must replace legacy release labels and
AutoPatchingbefore selecting this release. Owned labels now override a validatedDefaultBump, whose documented default remainspatch; explicitCallerOverridescan select invocation-specific settings without GitHub-injected defaults masking repository settings.The caller migration and old-publisher bootstrap requirements are included below. This is a breaking engine update, not the later push-authoritative publisher delivery, and its draft source is not a production dependency.
Breaking: Owned release controls replace legacy labels and automatic patching
AutoPatching,IgnoreLabels,MajorLabels,MinorLabels, andPatchLabelsare removed from the action interface. Repository configuration containing those keys now fails instead of silently losing its intent.DefaultBumpaccepts exactly lowercasepatch,minor, ormajor; a valid inherited patch default is policy, not recovery from invalid input.Only the five labels below select release behavior. Bare labels, dependency-version labels, unknown namespaces, noncanonical casing, and the retired
release:pre-releasespelling do not select a bump. Without an owned bump or skip, the effective default applies. Multiple bump labels, orrelease:skipcombined with another owned release label, fail.release:patchrelease:minorrelease:majorrelease:prereleaserelease:skipExisting v2 references are unchanged. Apply this release-wide adoption path to callers using v2:
release:majorfor the new reader. Under the v2 defaults this ismajorplusrelease:major. A skipped transition similarly needs the oldNoReleasesignal plusrelease:skip. These are temporary signals on the active migration PR, not a historical relabeling campaign.AutoPatching: truewithDefaultBump: patch, or omitDefaultBumpto inherit patch. Chooseminorormajoronly when that is the intended default. There is nononeor date bump. WhereAutoPatching: falsepreviously suppressed unlabeled publication, applyrelease:skipto the affected active PR instead. Remove the four label-list keys and translate their active decisions to the owned set.issues: writealongside the existing release/comment permissions. Subscribe the release workflow toopened,reopened,synchronize,labeled,unlabeled, andclosedfor its release target, without workflow path filters that bypass decision validation. A privilegedpull_request_targetwriter must check out trusted base code, never an untrusted PR head. Validate candidate code in a separate read-only PR workflow. Queue publication without cancellation; serialize merged releases by target branch while isolating open-PR prereleases by PR number.CallerOverridessettingWhatIf: true. Confirm that inherited/default, explicit bump, prerelease, skip, conflicting-label, and invalid-setting cases produce the expected decision and source. KeepUsePRBodyAsReleaseNotesandUsePRTitleAsNotesHeadingtrue to preserve the complete authored title/body record; their existing opt-out settings remain supported in this bounded resolver delivery.Stable publication remains PR-state-driven in this delivery. Do not add push/manual stable-publication triggers or relabel historical merged PRs expecting validation-only behavior. Exact pushed-source authority, recovery/manual contexts, publication provenance, and controlled alias advancement are separate work in #108.
Technical details
The consumer-change baseline is the released
v2.0.3source750abed0ecab02394554b610dc3aa191e75e3248. The development candidate isb8767c42b5140ea66e865cbd0142cd2f7c2cbde7; it is evidence for this change, not an authorized released dependency. The actual publication version, source, version base, and consumer-change baseline are resolved by the release process, not predicted here.The existing resolver coverage is retained, including all 32 owned-label subsets across all three defaults. The repository suite now has 150 passing cases. Eight anonymous bootstrap experiments execute the immutable v2.0.3 controller and the candidate with real YAML/SemVer dependencies and write-denying CLI mocks: owned-major-only resolves patch under the old defaults; dual major resolves major under both readers; old skip needs
NoRelease, while the new reader honorsrelease:skip. No release, tag, or historical label is changed by those experiments.The repository's own post-merge writer checks out the trusted default branch and uses its local action. This makes the merged owned-label reader available for the producer's breaking decision. A separately pinned framework caller remains on its old reader until an authorized immutable engine release is adopted; the new label alone does not bootstrap that caller.
The integration template baseline inspected is
PSModule/Template-Action@634ca503a47a39ca26d72fa59c784da8118ae835. ItsAuto-Releaseworkflow still selects the old reader, lacks the new label permission/event contract, and is not a verified unchanged template for this release. A separate template/caller delivery is required; no compatible updated template commit is claimed here. The exact caller changes are specified above rather than left for consumers to infer from that template.Implementation, local validation, and caller migration evidence are complete. The hosted review at
89b7766identified two cleanup-matching defects and unnecessary version parsing on skip; all three were reproduced with failing tests, fixed inb8767c4, and replied to/resolved. Final-head hosted CI is green, including Pester, the real action check, lint/security checks and CodeQL.The fresh review at
b8767c4reports no open issues, but its complete summary asks for closer assessment of authenticated tag pushes. The default-bootstrap concern was evaluated against the actual GitHub 0.43.1 dependency in CI: account/context initialization calls Git authentication configuration before publication. Two passing anonymous checks exercise that real path with API/vault/writes mocked and prove native Git URL expansion without contacting a remote. The assessment is recorded with immutable dependency sources, not called an automatic approval or used to weaken checkout security.The PR stays draft because its native delivery issue is blocked by #112; main has no required-status-check rule, and that enforcement was not bypassed.
The scoped issue-convergence sweep checked the release-related open issues. This PR delivers #111. It does not deliver push authority in #108 or #61, the remaining runner work in #105, or governance enforcement in #112; those remain separate.
Related references
New: Explicit caller overrides preserve repository defaults and invocation intent
CallerOverridesis a string input containing a YAML or JSON mapping of explicit release-setting overrides. It is the supported invocation-over-repository channel. GitHub injects scalar action defaults even when a caller omits them, so individual scalar inputs retain their existing lower-than-file precedence instead of pretending to reveal whether the user explicitly supplied a default value.Effective settings resolve as shared defaults declared in
action.yml, scalar base inputs, repository settings from.github\auto-release.yml(orConfigurationFile), then explicitCallerOverrides. An omitted{}mapping inherits repository settings. An owned bump label still overrides a valid effectiveDefaultBump. The action reports the effective setting/bump source.For a caller that must select minor while its repository normally selects major, use a block scalar or a JSON string, not a YAML object directly under
with:. Existing callers that need no invocation override can omit this input.Explicit false and empty supported text values are preserved. Booleans accept YAML Boolean values or case-insensitive strings containing exactly
trueorfalse; numeric and arbitrary truthy values are rejected. OnlyVersionPrefixand the existingDatePrereleaseFormataccept empty strings. Null, unknown, removed, or malformed values fail in every supplied scope even when a narrower scope or an owned label supplies a valid value. Remove the invalid lower-priority value rather than trying to mask it with an override.DatePrereleaseFormatremains a prerelease-suffix formatter, not date versioning.The file and override mapping accept the runtime release-setting names, excluding
CallerOverridesandConfigurationFilethemselves.Debug,Verbose,Version,Prerelease, andWorkingDirectoryremain separate dependency/runtime inputs. In particular,Prereleaseselects a prerelease GitHub module dependency; repository prerelease mode uses the owned label.Technical details
This result uses the same released v2.0.3/source baseline and development candidate identified in the breaking section. The typed override channel was selected explicitly for this delivery; it introduces no new orchestration framework or second bump algorithm.
The settings resolver validates each layer before applying it and returns the effective
DefaultBumpsource. The decision resolver retainsBump,Prerelease,Skip, andDefaultBumpApplied, addingBumpSource. Real-controller tests verify YAML and JSON overrides, inherited repository values despite injected scalar defaults, false/empty values, explicit-label precedence, source reporting, unmasked invalid inputs, and forced read-only validation over a validWhatIf: falsefile setting. All write commands are denied in these tests.CallerOverrides, configuration parsing and decision provenanceNo consumer edit is needed for the unchanged supported scalar settings/defaults when no invocation-specific override is required. A template using the old reader cannot yet consume this new input; its required release-reference/permission/event migration is covered in the breaking section.
Related references
Fixed: Prerelease publication and cleanup use validated source context
Open-PR prereleases target the exact validated 40-character head commit, not a potentially missing or colliding base-repository branch name. The normalized branch name still names the prerelease. A branch without an ASCII prerelease identifier fails only when prerelease mode requires it; valid stable/skipped contexts do not fail because an unused prerelease name is empty.
Cleanup locates the first numeric version core and compares its following prerelease identifier case-sensitively. Only the exact escaped identifier ending the tag or followed by a dot is eligible; later embedded hyphens and later version-like segments cannot select a candidate. Stable releases, invalid/non-version cores, longer identifiers, and regular-expression lookalikes are excluded. Empty and hyphenated version prefixes retain cleanup support. Closed skipped PRs still receive configured cleanup without parsing the latest version, even when that unrelated latest tag is malformed; stable GitHub Releases are never cleanup candidates.
Publication runs queue without cancellation. Merged-PR publication shares a target-branch concurrency key; open-PR prerelease runs remain isolated by PR number. This fixes the race introduced by making every publication PR-specific without bundling the later push-authority publisher.
Select this authorized release to obtain these fixes; no naming, note, or cleanup setting changes are required beyond the caller migration above.
WhatIfpreviews operations without executing label, release, tag, or comment writes.Technical details
The baseline and candidate are the same as the preceding results. Cases cover fork-style/colliding head refs, missing/mutable/malformed SHAs, exact cleanup boundaries, numeric stable versions, regex escaping, open/closed prerelease gating, a Git-validated non-ASCII branch in a skipped context, and the actual publishing workflow's no-cancellation/branch-serialization settings. PowerShell analysis, actionlint, Markdown lint, and the repository tests pass locally; final hosted CI/review is evaluated separately.
Branch-derived prerelease naming is intentionally retained. PR-unique prerelease identity, broader retention/recovery, the full durable/RC lifecycle, and exact stable-source/publication-envelope enforcement are not claimed by this change.
Related references
Release decisions
release:majoroverrides the valid patch default because legacy controls are removed and invalid settings now fail.release:majorThe release process resolves the final version, tag, immutable published source, version-computation base, and consumer-change baseline at publication. No final numeric version or production dependency is assigned to this draft candidate.