Skip to content

Release decisions use owned labels and effective settings - #109

Open
Marius Storhaug (MariusStorhaug) wants to merge 25 commits into
mainfrom
namespaced-release-labels
Open

Marius Storhaug (MariusStorhaug) wants to merge 25 commits into
mainfrom
namespaced-release-labels

Conversation

@MariusStorhaug

@MariusStorhaug Marius Storhaug (MariusStorhaug) commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Repository and workflow maintainers must replace legacy release labels and AutoPatching before selecting this release. Owned labels now override a validated DefaultBump, whose documented default remains patch; explicit CallerOverrides can 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, and PatchLabels are removed from the action interface. Repository configuration containing those keys now fails instead of silently losing its intent. DefaultBump accepts exactly lowercase patch, minor, or major; 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-release spelling do not select a bump. Without an owned bump or skip, the effective default applies. Multiple bump labels, or release:skip combined with another owned release label, fail.

Owned label Effect
release:patch Override the valid effective default with a patch bump.
release:minor Override it with a minor bump.
release:major Override it with a major bump.
release:prerelease Select prerelease mode for an open PR, using its explicit or effective default bump.
release:skip Successfully validate without resolving or publishing a version.

Existing v2 references are unchanged. Apply this release-wide adoption path to callers using v2:

  1. Inspect the release job's actual reader reference and effective legacy label settings before changing them. A reader pinned to v2 does not recognize an owned major label by default. While the breaking migration PR is still processed by that reader, retain its supported legacy major signal and add release:major for the new reader. Under the v2 defaults this is major plus release:major. A skipped transition similarly needs the old NoRelease signal plus release:skip. These are temporary signals on the active migration PR, not a historical relabeling campaign.
  2. Wait for the producer's normal human review and authorized release process to publish the breaking engine update. Then select that release's full immutable source SHA in the caller. Do not use this draft PR head, a draft tag, or an unverified moving major reference as a production dependency. No intermediate installation of every earlier version is required; the old-reader bootstrap and producer publication must precede the caller reference change.
  3. Replace AutoPatching: true with DefaultBump: patch, or omit DefaultBump to inherit patch. Choose minor or major only when that is the intended default. There is no none or date bump. Where AutoPatching: false previously suppressed unlabeled publication, apply release:skip to the affected active PR instead. Remove the four label-list keys and translate their active decisions to the owned set.
  4. Add issues: write alongside the existing release/comment permissions. Subscribe the release workflow to opened, reopened, synchronize, labeled, unlabeled, and closed for its release target, without workflow path filters that bypass decision validation. A privileged pull_request_target writer 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.
  5. The first non-dry run provisions and reconciles the five owned labels. If the migration PR needs a label before that run, provision the needed canonical labels through the repository's authorized setup path. Keep unrelated labels; no bulk deletion or historical label migration is required.
  6. Run the existing action validation read-only with CallerOverrides setting WhatIf: true. Confirm that inherited/default, explicit bump, prerelease, skip, conflicting-label, and invalid-setting cases produce the expected decision and source. Keep UsePRBodyAsReleaseNotes and UsePRTitleAsNotesHeading true to preserve the complete authored title/body record; their existing opt-out settings remain supported in this bounded resolver delivery.
with:
  DefaultBump: patch
  CallerOverrides: |
    WhatIf: true

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.3 source 750abed0ecab02394554b610dc3aa191e75e3248. The development candidate is b8767c42b5140ea66e865cbd0142cd2f7c2cbde7; 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 honors release: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. Its Auto-Release workflow 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.

Changed surface Standards checked Framework docs checked Result
Owned labels, defaults and decision helpers PowerShell, Functions, Error Handling, Testing Release Management settings/label contract in MSXOrg/docs#209 Fixed in this PR
Action inputs and trusted/read-only caller workflows GitHub Actions, YAML, Security Native action contract and release-route validation Fixed in this PR; runner pinning remains #105
Required decision enforcement Definition of Ready for Review, release-decision check contract Protected main-branch rule evaluation Exception - #112

Implementation, local validation, and caller migration evidence are complete. The hosted review at 89b7766 identified two cleanup-matching defects and unnecessary version parsing on skip; all three were reproduced with failing tests, fixed in b8767c4, 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 b8767c4 reports 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

CallerOverrides is 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 (or ConfigurationFile), then explicit CallerOverrides. An omitted {} mapping inherits repository settings. An owned bump label still overrides a valid effective DefaultBump. 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.

with:
  CallerOverrides: |
    DefaultBump: minor
    AutoCleanup: false
    VersionPrefix: ''
with:
  CallerOverrides: '{"DefaultBump":"minor","AutoCleanup":false,"VersionPrefix":""}'

Explicit false and empty supported text values are preserved. Booleans accept YAML Boolean values or case-insensitive strings containing exactly true or false; numeric and arbitrary truthy values are rejected. Only VersionPrefix and the existing DatePrereleaseFormat accept 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. DatePrereleaseFormat remains a prerelease-suffix formatter, not date versioning.

The file and override mapping accept the runtime release-setting names, excluding CallerOverrides and ConfigurationFile themselves. Debug, Verbose, Version, Prerelease, and WorkingDirectory remain separate dependency/runtime inputs. In particular, Prerelease selects 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 DefaultBump source. The decision resolver retains Bump, Prerelease, Skip, and DefaultBumpApplied, adding BumpSource. 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 valid WhatIf: false file setting. All write commands are denied in these tests.

Changed surface Standards checked Framework docs checked Result
CallerOverrides, configuration parsing and decision provenance PowerShell, Functions, Error Handling, GitHub Actions Documented settings/caller precedence in MSXOrg/docs#209 Fixed in this PR
Input reference and usage guidance Natural Language, Markdown, documentation model Native action input contract Fixed in this PR; release-specific adoption remains in this PR record

No 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. WhatIf previews 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.

Changed surface Standards checked Framework docs checked Result
Prerelease context and cleanup helpers/controller PowerShell, Error Handling, Testing Existing native prerelease contract Fixed in this PR
Publishing concurrency and trusted trigger GitHub Actions, least privilege, serialized mutation Native repository publication route Fixed in this PR; remaining runner pinning is #105
Related references
Release decisions
  • Release: Yes - this repository's main route delivers a versioned GitHub Action, and supported release-label/input contracts change.
  • Version bump: Major - the applied release:major overrides the valid patch default because legacy controls are removed and invalid settings now fail.
  • Mode: Stable - the default-branch repository release route is the stable authority; this PR does not request prerelease mode.
  • Labels: release:major

The 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .github/workflows/Action-Test.yml Resolved
Comment thread .github/workflows/Release.yml Resolved
Comment thread src/main.ps1 Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • $createPrerelease can become true for merged/closed pull requests when the PR targets a non-default branch (because $closedPullRequest only 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

Comment thread .github/workflows/Release.yml Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_target sets github.ref to the base/default branch (e.g. refs/heads/main), so using ${{ github.workflow }}-${{ github.ref }} collapses all PR runs into the same concurrency group. With cancel-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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Marius Storhaug (MariusStorhaug) added a commit to MSXOrg/docs that referenced this pull request Aug 29, 2026
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>
@MariusStorhaug
Marius Storhaug (MariusStorhaug) marked this pull request as ready for review August 29, 2026 11:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 create fails during canonical label provisioning, the thrown error doesn’t include the gh exit code (unlike other gh/git failures in this file). Including $LASTEXITCODE makes 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-PullRequestReleaseContext derives PrereleaseName by 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 in main.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
    }

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread src/main.ps1 Resolved
@MariusStorhaug Marius Storhaug (MariusStorhaug) changed the title 🌟 [Major]: Release decisions use owned labels Release decisions use owned labels and effective settings Oct 31, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread src/Release-GHRepository.Helpers.psm1 Resolved
Comment thread src/Release-GHRepository.Helpers.psm1 Resolved
Comment thread src/main.ps1 Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

@MariusStorhaug

Copy link
Copy Markdown
Member Author

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 git ls-remote --get-url to prove authenticated URL expansion without contacting a remote. No real credential, global configuration, release, tag, permission or policy is changed by those checks. The active immutable-tag rule protects three-component tags; it does not target the moving major/minor aliases.

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.

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

Labels

release:major Publish a major release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resolve repository releases from owned decisions and effective settings

2 participants