diff --git a/src/docs/Capabilities/index.md b/src/docs/Capabilities/index.md index b3b57f5..98f7b4b 100644 --- a/src/docs/Capabilities/index.md +++ b/src/docs/Capabilities/index.md @@ -20,7 +20,7 @@ the same spec-and-design shape as any other capability. | Section | Description | | --- | --- | -| [Release Management](release-management/index.md) | How a source change becomes a versioned, immutable artifact, driven entirely on the GitHub platform. | +| [Release Management](release-management/index.md) | Durable version resolution, publication, recovery, withdrawal, and consumer update policy for immutable releases. | | [Repository Governance](repository-governance/index.md) | How every repository in an organization is classified, protected, and continuously reconciled against the controls its classification declares. | | [Dependency Updates](dependency-updates/index.md) | How a repository's pinned dependencies are kept current and secure through automated update pull requests. | | [Merge Automation](merge-automation/index.md) | How a pull request's required status checks become the machine-readable signal that drives automated approval and merge — green merges, red holds, nothing bypasses the gate. | diff --git a/src/docs/Capabilities/release-management/accept-moved-release-tags.md b/src/docs/Capabilities/release-management/accept-moved-release-tags.md new file mode 100644 index 0000000..35b26c8 --- /dev/null +++ b/src/docs/Capabilities/release-management/accept-moved-release-tags.md @@ -0,0 +1,100 @@ +--- +title: Accept Moved Release Tags +description: Refresh an owned moving release alias locally and configure Git to keep it current. +--- + +# Accept moved release tags + +Use this guide when an MSX-owned producer intentionally publishes a moving Git +tag such as `v3` or `v3.4` and your local clone still resolves it to an older +release. + +Do not use this procedure for exact version tags such as `v3.4.2`. Exact version +tags are immutable. A moved exact tag is a producer integrity incident, not a +normal update. + +## Why an ordinary fetch can leave a stale tag + +Git protects existing local tags from being overwritten. An ordinary +`git fetch` can therefore update branches while silently leaving a moving tag at +its old object. Fetching all tags explicitly can instead report: + +```text +! [rejected] v3 -> v3 (would clobber existing tag) +``` + +Both outcomes leave the consumer on the previous release until it accepts the +producer-owned alias movement explicitly. + +## Refresh tags once + +Before this command, preserve or rename any local-only tags you need. Pruning +tags removes local tags that do not exist on the remote, and force permits the +remote's tag values to replace local values. + +```bash +git fetch --tags --force --prune --prune-tags +``` + +This refreshes moved aliases and removes tags deleted from the configured +remote. Review the fetch output before building or resolving a dependency. + +## Keep owned aliases current + +For a clone that should continuously accept the `origin` repository's controlled +tag aliases, inspect the current fetch refspec: + +```bash +git config --get-all remote.origin.fetch +``` + +If the output does not already contain `+refs/tags/*:refs/tags/*`, add it once: + +```bash +git config --add remote.origin.fetch '+refs/tags/*:refs/tags/*' +git config fetch.prune true +git config fetch.pruneTags true +``` + +The leading `+` allows the remote's tag namespace to update local tags +non-fast-forward. The prune settings remove local tag references no longer +present on the remote. Use this configuration only when `origin` is inside the +consumer's permitted trust boundary and the producer owns the moving aliases. + +After configuration, a normal fetch from `origin` keeps branches and tags +aligned: + +```bash +git fetch --prune origin +``` + +## Verify an alias + +Compare the local tag object with the remote tag object: + +```bash +git rev-parse refs/tags/v3 +git ls-remote --refs origin refs/tags/v3 +``` + +The first hash must equal the hash at the start of the second command's output. +Repeat with the actual alias, such as `v3.4`. + +Then resolve the exact release and immutable source before use: + +```bash +git show --no-patch --decorate refs/tags/v3 +``` + +A CI job can perform the same comparison and fail when the local alias is stale. +Consumers outside the producer's trust boundary must use an exact immutable +version or commit SHA instead of configuring automatic tag movement. + +## Where this connects + +- [Release Management design](design.md#current-version-discovery-and-aliases) — + when an owned alias can move. +- [Publishing Targets](design-publishing-targets.md#github-releases) — GitHub + exact tags, moving aliases, and release records. +- [GitHub Actions](../../Coding-Standards/GitHub-Actions.md#pin-actions-according-to-ownership) — + when an owned major tag is permitted. diff --git a/src/docs/Capabilities/release-management/design-publishing-targets.md b/src/docs/Capabilities/release-management/design-publishing-targets.md index 0b96812..908a745 100644 --- a/src/docs/Capabilities/release-management/design-publishing-targets.md +++ b/src/docs/Capabilities/release-management/design-publishing-targets.md @@ -1,71 +1,278 @@ --- title: Publishing Targets -description: The contract every publishing destination documents, with GitHub Releases as the reference target. +description: Destination contracts for version mapping, prereleases, immutability, withdrawal, aliases, constraints, and release records. --- # Release Management — Publishing Targets -A **publishing target** is any destination that accepts a versioned artifact and serves it to consumers. The [release pipeline](design.md#the-pipeline) publishes to targets through one contract, so the process is the same whether a repository has one destination or five. +A **publishing target** accepts a versioned artifact or release record and makes +it available to consumers. The [release lifecycle](design.md#build-verify-and-publish) +passes every target the same frozen version, retained artifact, note envelope, +and release-intent identity. -This page holds the contract and the targets that satisfy it. It is the boundary that lets a new destination be added without touching the [spec](spec.md). +Targets differ in native version syntax, prerelease channels, deletion, and +consumer resolution. This page makes those differences explicit so adding a +destination does not change the [release-management specification](spec.md). -## The contract +## Publishing-target contract -A target is described by six answers. They are the questions the release process needs answered in order to publish safely, and they are the questions that differ between destinations: +Every target documents seven dimensions: | Dimension | What it settles | | --- | --- | -| **Version scheme** | the exact string form a version takes, and what the target accepts as valid | -| **Prerelease representation** | how a prerelease is expressed, and how the target sorts it relative to stable versions | -| **Immutability** | whether a published version can be replaced, and what happens on a repeated publish of the same version | -| **Unpublish** | whether a version can be withdrawn, what withdrawal does to existing consumers, and whether the version number becomes reusable | -| **Floating tags** | whether the target supports mutable pointers such as `latest`, and how they are moved | -| **Release record** | where the durable, linkable evidence of the release lives | +| **Version scheme** | The native coordinate and its one-to-one mapping from canonical SemVer. | +| **Prerelease representation** | How prerelease identity and ordering map to the target's syntax or channel. | +| **Immutability** | Which reference cannot change and what a repeated publication does. | +| **Withdrawal** | The strongest supported hide, unlist, yank, or delete operation and whether existing consumers retain access. | +| **Alias families** | Whether `latest`, major, or minor moving references can be represented and reconciled. | +| **Version constraints** | Whether consumers can express native ranges or need a producer-controlled alias. | +| **Release record** | Where the durable, linkable destination evidence is stored. | -A target MUST document all six before it is used. An undocumented dimension is a surprise waiting for the first failed release — most often around immutability, where publishing the same version twice is a success on one target and a hard error on another. +A target MUST answer all seven before it is enabled. It MUST also define: + +- how an idempotent retry verifies that an existing coordinate identifies the + recorded artifact; +- whether target-native state can be read back after publication; +- how canonical stable or prerelease state maps to target-native state; and +- which evidence proves publication or withdrawal. + +Distinct canonical releases MUST NOT map to one native coordinate. A collision +or stable/prerelease mismatch fails before publication. Target deletion never +frees a canonical version or prerelease identifier for reuse. ## Target summary -| Target | Version scheme | Prerelease | Immutable | Unpublish | Floating tags | Release record | -| --- | --- | --- | --- | --- | --- | --- | -| **GitHub Releases** | `vMAJOR.MINOR.PATCH` git tag | SemVer suffix, flagged as prerelease | tag and assets are treated as immutable | delete is possible; treated as exceptional | yes — git tags | the Release itself | -| **PowerShell Gallery** | `MAJOR.MINOR.PATCH` module version | SemVer suffix on the module version | yes — a version is published once | unlist only; the version is never reusable | no | the gallery listing | -| **VS Code Marketplace** | `MAJOR.MINOR.PATCH` extension version | separate prerelease channel on the same version line | yes | unpublish removes the extension version | channel acts as the pointer | the marketplace listing | -| **NuGet** | `MAJOR.MINOR.PATCH` package version | SemVer suffix on the package version | yes | unlist only; the version is never reusable | no | the package listing | -| **Container registry** | `:` plus a content digest | SemVer suffix in the tag | the **digest** is immutable; the tag is not | tag or manifest deletion | yes — mutable tags | the digest | +| Target | Version scheme | Prerelease | Immutable reference | Withdrawal | Alias families | Native constraints | Release record | +| --- | --- | --- | --- | --- | --- | --- | --- | +| **GitHub Releases** | Exact `vMAJOR.MINOR.PATCH` or prerelease tag | SemVer suffix and native prerelease flag | Protected exact tag, source commit, and asset digest | Delete or mark unavailable; coordinate remains reserved | `latest`, major, minor as optional Git tags; native Latest separately | No range resolver | GitHub Release and durable release intent | +| **PowerShell Gallery** | `MAJOR.MINOR.PATCH` module version | SemVer suffix | Published module version | Unlist; downloads already held remain; version is not reusable | None | NuGet ranges through PSResourceGet; mapped manifest constraints | Gallery listing joined from GitHub Release | +| **NuGet** | `MAJOR.MINOR.PATCH` package version | SemVer suffix | Published package version and package hash | Unlist or deprecate; version is not reusable | None | NuGet version ranges | Package listing joined from GitHub Release | +| **VS Code Marketplace** | Marketplace-compatible extension version mapped from canonical SemVer | Separate prerelease channel and flag | Published extension version | Unpublish where permitted; version is not reusable | Native stable/prerelease channel selection, not release aliases | No consumer-selected SemVer range | Marketplace listing joined from GitHub Release | +| **Container registry** | Version tag plus content digest | SemVer suffix in exact tag | Manifest or image digest | Delete tag or manifest where permitted; reservation remains | `latest`, major, minor as optional tags | No range resolver | Registry digest joined from GitHub Release | + +Two rules apply to every row: + +- **Coordinates are single-use.** Withdrawal changes availability or + recommendation, not ownership. A corrected artifact always receives a new + version. +- **Completion is an outcome, not a claim of transactional publication.** One + target may expose content before another fails. Durable per-target progress + keeps the release incomplete until all required targets succeed, then lets a + retry resume safely. + +## GitHub Releases + +GitHub Releases is the reference target and the cross-target join point. Every +repository governed by this capability creates a GitHub Release; a repository +with no external artifact publishes there only. External target coordinates and +evidence are linked from the same release record. + +### GitHub version and prerelease mapping + +Stable versions use an exact tag: + +```text +vMAJOR.MINOR.PATCH +``` + +Prereleases preserve the canonical suffix: + +```text +vMAJOR.MINOR.PATCH-series.N +``` + +The Release's native prerelease flag MUST agree with the canonical version. +GitHub's native **Latest** selection is advertising state, not release authority. +It is updated only after durable completion and MUST NOT include prereleases or +withdrawn releases. + +### GitHub immutability + +An exact version tag identifies the fixed source revision. Assets are uploaded +from the retained artifact and verified by fingerprint. A repeated publish is +successful only when the existing tag, assets, and release record match the +frozen release intent. + +Repositories enable GitHub's immutable-release protection where the platform +supports it. Shared automation reconciles and verifies that setting rather than +assuming repository policy remained unchanged. Until setting reconciliation is +implemented, exact tags and assets remain immutable by MSX policy but lack the +full intended platform enforcement. + +### GitHub withdrawal + +GitHub permits a Release and tag to be deleted, but deletion does not erase +clones, caches, downloads, or durable lifecycle history. The withdrawal record +therefore remains authoritative and the version remains permanently reserved. +Deletion is used only where the approved withdrawal operation and repository +policy require it. + +Replaying a withdrawn intent does not recreate the Release or tag. Current +discovery and native Latest are reconciled to the greatest remaining eligible +completed stable release. + +### GitHub aliases and constraints + +Optional Git tags represent the closed `latest`, major, and minor alias families. +They are separate from exact version tags and can move only through +[alias reconciliation](design.md#current-version-discovery-and-aliases). +Consumers must explicitly accept moved owned tags; see +[Accept moved release tags](accept-moved-release-tags.md). + +Git references do not resolve SemVer range expressions. A major or minor +boundary therefore requires the corresponding producer alias. External +consumers use an exact commit SHA or another immutable fingerprint rather than a +moving tag. + +### GitHub release record + +The GitHub Release contains the contributor-authored note, publication envelope, +fixed source, and links to every destination coordinate. The durable release +intent remains the authority for lifecycle state, including partial publication, +retirement, withdrawal, alias reconciliation, and announcements. + +## PowerShell Gallery + +### PowerShell Gallery version and prerelease mapping + +The module manifest and gallery coordinate use canonical SemVer without the +leading `v`. A prerelease suffix is retained where the gallery and module +manifest permit it. Mapping validation runs before publication so a native +version cannot collide with another canonical release. + +### PowerShell Gallery immutability and withdrawal + +A published module version is immutable. Retrying succeeds only when the +existing listing and package hash identify the recorded artifact. The gallery +supports unlisting rather than reclaiming a version; existing consumers and +caches can still hold it, and the coordinate remains permanently reserved. + +### PowerShell Gallery aliases and constraints + +The gallery does not offer producer-controlled moving release aliases. +PSResourceGet accepts NuGet version ranges, and module manifests map supported +bounds into their native fields. Consumers follow +[PowerShell Version Constraints](../../Coding-Standards/PowerShell/Version-Constraints.md) +rather than placing a range-like string in a field that accepts only one +version. + +### PowerShell Gallery release record + +The gallery listing and package hash are recorded as destination evidence. The +GitHub Release links the gallery coordinate to the canonical source, notes, and +cross-target release intent. + +## NuGet + +### NuGet version and prerelease mapping + +The package version uses canonical SemVer without a leading `v`, including its +prerelease suffix. The adapter rejects any normalization or server behavior that +would make two canonical identifiers address one native package version. + +### NuGet immutability and withdrawal + +A published package version is immutable and single-use. An idempotent retry +checks the existing package hash. NuGet unlisting or deprecation changes +discovery and guidance but does not remove packages already restored, and the +version remains reserved. + +### NuGet aliases and constraints + +NuGet has no producer-controlled moving alias for package versions. Consumers +express supported movement with native +[NuGet version ranges](../../Coding-Standards/PowerShell/Version-Constraints.md). +An exact package version remains the immutable release coordinate. + +### NuGet release record + +The package listing, content hash, and withdrawal or deprecation state are +destination evidence. The GitHub Release is the human-readable join point. + +## VS Code Marketplace + +The [VS Code Extension Framework design](../vscode-extension-framework/design.md) +owns the detailed VSIX and marketplace behavior. + +### VS Code Marketplace version and prerelease mapping + +Stable extension versions map directly to the marketplace-compatible version in +the VSIX manifest. The marketplace represents prereleases through its separate +prerelease channel and `--pre-release` publication flag, including the +framework's odd-minor convention. The adapter records the canonical prerelease +and native extension version together and MUST guarantee a one-to-one mapping. + +### VS Code Marketplace immutability and withdrawal + +A published extension version is immutable and single-use. Marketplace +unpublication is applied where permitted, but cannot recall installed VSIX +files or make the canonical version reusable. + +### VS Code Marketplace aliases and constraints + +Stable and prerelease marketplace channels influence update discovery but are +not the release-management `latest`, major, or minor alias families. The +marketplace does not expose consumer-selected SemVer ranges for extension +updates. Consumers that need an exact immutable artifact use the VSIX attached +to the GitHub Release. + +### VS Code Marketplace release record + +The marketplace listing and native channel state are destination evidence. The +GitHub Release always contains the same VSIX and joins the canonical release to +optional Marketplace or Open VSX publication. + +## Container registries + +### Container registry version and prerelease mapping + +An exact canonical version maps to an image tag, while the published manifest or +image digest identifies immutable content. Prerelease suffixes remain part of +the exact version tag. -Two patterns run through the table and shape how consumers are told to pin: +### Container registry immutability and withdrawal -- **Version numbers are single-use.** On every target above, a published version number is spent. Withdrawal removes availability, not the reservation. A fix is therefore always a new version — never a re-publish of the old one, which is the same conclusion the pipeline reaches from [build-once](design.md#the-pipeline). -- **Only content addresses are truly immutable.** Where a target offers both a name and a digest, the digest is the reference and the name is the convenience. +Registry tags are mutable; digests are not. Publication records both and treats +the digest as artifact identity. An idempotent retry succeeds only when the +exact version tag resolves to the recorded digest. Deleting a tag or manifest +does not free the canonical release version or prove that cached content +disappeared. -## GitHub Releases — the reference target +### Container registry aliases and constraints -GitHub Releases is the reference implementation: every repository governed by this capability publishes there, and a repository with no external artifact publishes there *only*. A target-specific concern is described relative to this one. +Optional tags can implement the `latest`, major, and minor alias families. +Reconciliation updates each enabled tag to the digest of the greatest eligible +matching release. Container references do not evaluate SemVer ranges, so +consumers use a controlled producer alias inside the allowed trust boundary or +pin a digest. -- **Version scheme.** A git tag `vMAJOR.MINOR.PATCH` on the release-branch commit. The tag is the artifact for Action, workflow, and source-distributed module repositories. -- **Prerelease.** The SemVer prerelease suffix, with the Release marked as a prerelease so it is excluded from *latest*. -- **Immutability.** The tag points at one commit and is not moved. Assets are uploaded once. A published version is never rewritten in place. -- **Unpublish.** A Release and its tag can be deleted, but doing so breaks consumers that resolved it, so it is reserved for a release that must not exist — a leaked secret, a legal removal — and the version number is not reused. -- **Floating tags.** Supported as additional git tags, subject to the [floating-tag rules](design.md#floating-tags). -- **Release record.** The Release itself: the version as its name, the release note as its body, and the immutable reference to whatever was published elsewhere. +### Container registry release record -Because every release produces a GitHub Release, it is also the **join point** across targets: a release published to a registry or marketplace records its reference there, so one link answers *what shipped, in what version, and where it went*. +The registry digest and exact tag are destination evidence. The GitHub Release +links that digest to source, notes, verification, and lifecycle state. ## Adding a target -1. Document the six contract dimensions above, in the summary table. -2. Confirm the target's immutability and prerelease behaviour are compatible with [SemVer](https://semver.org/) ordering. Where the target's native convention differs, the mapping is stated rather than assumed. -3. Add the publish step. It receives the already-built artifact and the - already-resolved version, and it MUST be idempotent: publishing a version the - target already holds is a success only when its immutable identity matches the - artifact being retried. A different artifact at the same version is an error. -4. Include the target in the [all-or-nothing](design.md#publishing-targets) set, so a version cannot be present on some destinations and absent from others. +1. Document all seven contract dimensions and add the target to the summary. +2. Define a total, collision-free mapping from canonical stable and prerelease + versions to native coordinates and flags. +3. Define the immutable identity and read-back check used by idempotent retry. +4. Define the strongest supported withdrawal behavior and its limitations. +5. State which, if any, alias families and native constraints consumers can use. +6. Define publication, withdrawal, and reconciliation evidence. +7. Add the adapter to the required destination set so durable completion and + phase-aware recovery include it. -The spec does not change. That is the purpose of the contract. +The target receives an already built and verified artifact. It does not choose a +version, rebuild content, rewrite the note snapshot, or mark the overall release +complete by itself. ## Where this connects -- [Spec](spec.md) — the requirements this design serves. -- [Design](design.md) — the pipeline that publishes to these targets. -- [Security](../../Coding-Standards/Security.md#supply-chain) — why consumers pin to immutable references. +- [Spec](spec.md) — the normative release and target requirements. +- [Design](design.md) — durable state, publication, recovery, discovery, and + consumer policy. +- [Accept moved release tags](accept-moved-release-tags.md) — local Git + configuration for consumers of owned aliases. +- [Security](../../Coding-Standards/Security.md#supply-chain) — immutable + references and supply-chain controls. diff --git a/src/docs/Capabilities/release-management/design.md b/src/docs/Capabilities/release-management/design.md index ff0a24f..e08ce09 100644 --- a/src/docs/Capabilities/release-management/design.md +++ b/src/docs/Capabilities/release-management/design.md @@ -1,397 +1,566 @@ --- title: Design -description: How release management is built — a shared reusable workflow that resolves an explicit or configured SemVer bump, builds once, and publishes. +description: Durable release intents, lifecycle transitions, recovery, withdrawal, aliases, announcements, and implementation coverage. --- # Release Management — Design -The behaviour in the [spec](spec.md) is delivered by a **shared reusable release -workflow**. A repository opts in with a short caller workflow and a small -`.github/release.config.yml`. The workflow supplies the shared mechanics; an -explicit label or an intentionally configured default supplies the release level. - -## Branching model - -A **release branch** is any branch configured as a release target, each with a -**release type** — `stable` or `prerelease`. - -- **Single branch (zero-config).** One release branch (the default branch) - produces stable releases. Prereleases are opt-in via a PR label. -- **Multi-branch.** `dev` (prerelease) collects PRs and publishes a prerelease - on every merge; `main` (stable) receives `dev`. Merging `dev → main` computes - the stable version from the **latest stable release** plus the merge PR's - resolved bump — the prerelease counter does not carry over. -- **One production authority.** At most one branch is `release-type: stable`; - every other release branch is `prerelease`. The single stable branch - (typically `main`) owns the production version — a prerelease branch can never - cut a stable release. -- **Bundled releases.** A **staging branch** collects feature PRs; merging it to - a release branch produces **exactly one** release for all bundled changes. +This page realizes the [release-management specification](spec.md) while keeping +repositories on one shared, GitHub-native release path. It describes the +**intended lifecycle** first, then identifies which parts the current shared +automation baseline implements. The specification remains normative even where +the implementation crosswalk records a gap. + +## Design principles + +1. **The release intent is authoritative.** Tags, releases, workflow runs, and + destination listings are observations that must agree with durable state. +2. **Resolve once, then resume.** A retry continues frozen work; it never turns + current repository metadata into a different release under the same identity. +3. **Build once.** Verification and every destination receive the same retained + bytes. +4. **Complete everywhere before advertising anywhere.** Current discovery, + stable aliases, and completion announcements follow durable completion. +5. **Roll forward.** Failure, retirement, and withdrawal preserve history and + reservations. Corrections use a new approved version. +6. **Consumers choose policy within a trust boundary.** Producers expose only + the controlled references required to implement that policy. + +## Release authority and interfaces {#branching-model} + +Exactly one configured stable line owns stable publication. A repository can +develop on several branches, but only a merge or approved manual request whose +fixed source is on that line can create a stable release. + +| Interface | Purpose | Release authority | +| --- | --- | --- | +| Pull-request decision check | Validates the owned release decision before merge. | None; it validates intent. | +| Merge to the stable line | Normal request for a release of the accumulated range. | Reviewed pull request and branch policy. | +| Direct push | Validates source and leaves the change pending. | None; it never inherits `DefaultBump`. | +| Manual release request | Releases an explicit stable-line source and accumulated range. | Explicit bump, complete notes, and approval for that range. | +| Retry request | Resumes one durable release intent. | The original frozen approval and intent. | +| Retirement request | Ends recovery of one incomplete intent. | Explicit maintainer authorization. | +| Withdrawal request | Makes one completed release ineligible. | Explicit maintainer authorization. | +| Announcement retry | Redelivers an unacknowledged completion message. | The completed release record. | + +The merge path remains the default because it keeps the compatibility decision, +consumer evidence, source review, and release-note source together. Manual +release exists for approved accumulated change, not as a way around review. + +The [Branching and Merging](../../Ways-of-Working/Branching-and-Merging.md) +standard owns standing development-to-stable promotion. Release management +starts when reviewed source reaches the configured stable line; it does not +duplicate the promotion process. + +## Durable release intent + +Resolve creates or loads one durable release intent before Build. Its stable +identity makes duplicate delivery and retries idempotent. + +| Field | Frozen or progressive | Purpose | +| --- | --- | --- | +| Intent identity and idempotency key | Frozen | Identifies one logical release independently of a workflow run. | +| Trigger and approval evidence | Frozen | Shows who or what authorized the release and included range. | +| Stable line, fixed source, and range baseline | Frozen | Defines the exact source history being released. | +| Aggregate decision and its source | Frozen | Records major, minor, patch, or skip and whether the fallback was used. | +| Canonical version and reservation state | Frozen after Resolve | Prevents later work from taking the same coordinate. | +| Release-note snapshot and provenance | Frozen | Preserves the reviewed contributor-authored account. | +| Required destinations and announcement routes | Frozen | Prevents configuration drift during recovery. | +| Lifecycle stage and failure detail | Progressive | Records the last successful boundary and actionable failure. | +| Artifact location and cryptographic fingerprint | Frozen after Build | Binds verification and publication to identical bytes. | +| Verification evidence | Progressive, append-only | Proves which retained artifact passed which checks. | +| Publication progress by destination | Progressive, append-only | Supports idempotent multi-target resume. | +| Alias reconciliation progress | Progressive, append-only | Records advertising after completion or withdrawal. | +| Announcement progress by destination | Progressive, append-only | Supports at-least-once delivery without republishing. | +| Retirement or withdrawal evidence | Append-only | Preserves a terminal operator decision without rewriting history. | + +The durable store can be implemented with any shared platform that provides +atomic writes, immutable history, and lookup by repository and intent. A +workflow run summary is useful evidence but is not sufficient storage: runs can +expire, retries use new run identifiers, and one run cannot safely coordinate +all later lifecycle operations. + +### Lifecycle states -```yaml -# .github/release.config.yml -release-branches: - - branch: main - release-type: stable - - branch: dev - release-type: prerelease +```mermaid +stateDiagram-v2 + [*] --> Resolved + Resolved --> Built + Built --> Verified + Verified --> Publishing + Publishing --> Publishing: Record one destination + Publishing --> Complete: All required destinations recorded + Resolved --> Resolved: Build attempt failed + Built --> Built: Verification or recovery failed + Verified --> Verified: Publication attempt failed + Publishing --> Publishing: Publication attempt failed + Resolved --> Superseded: No successful build or external visibility + Built --> Retired: Explicitly unrecoverable + Verified --> Retired: Explicitly unrecoverable + Publishing --> Retired: Explicitly unrecoverable + Complete --> Withdrawn: Explicit withdrawal ``` -## The pipeline +A failed attempt does not create a second logical state machine. It records the +failure against the last successful lifecycle boundary. `Superseded` applies +only to work that never produced a successful build and never exposed a release +coordinate. `Retired` preserves a permanently reserved coordinate for an +incomplete release. `Withdrawn` preserves completion while changing eligibility. -Every release runs the same four stages in order. The stage boundaries exist to -make **build-once** enforceable — each stage may only consume what the previous -stage produced. +## Resolve -```mermaid -flowchart LR - resolve["Resolve
version decided"] --> build["Build
artifact created once"] - build --> test["Test
same artifact validated"] - test --> publish["Publish
same artifact released"] +Resolve is the only stage that selects source, scope, decision, version, note +snapshot, destinations, and announcement routes. Every later stage consumes +those fields. + +### Identify the accumulated range + +For a new stable request: + +1. Find the source revision of the greatest completed stable release that is an + ancestor of the fixed source. If no completed stable release exists, use the + beginning of repository history. +2. Enumerate every source change after that baseline through the fixed source. +3. Associate reviewed pull requests and their owned release decisions where + possible. Keep skipped changes in the range. +4. Calculate the highest known increment in the range: major, then minor, then + patch. +5. Require one reviewed aggregate decision that is no lower than that minimum + and explicitly covers any skipped or direct change whose compatibility impact + is otherwise unknown. +6. Freeze the full range and account for every source revision in this or an + earlier durable release intent. + +The current merged pull request normally supplies the aggregate approval. If +unreviewed direct changes make that approval incomplete, Resolve stops and +requires a manual release request covering the range. `DefaultBump` applies only +when the current associated pull request has no explicit decision and the range +otherwise has complete reviewed evidence. + +An unbuilt, unexposed pending request can be coalesced into a later approved +range and marked `Superseded`. A built or externally visible request cannot be +coalesced; it must complete or be retired before later work advances. + +### Resolve the owned instruction set + +Owned labels are the contributor-facing vocabulary. Their exact definitions and +conflicts live in [Automation Labels](../../Ways-of-Working/Automation-Labels.md). + +| Marker | Resolve effect | +| --- | --- | +| `release:major` | Selects a major aggregate increment. | +| `release:minor` | Selects a minor aggregate increment. | +| `release:patch` | Selects a patch aggregate increment. | +| `release:skip` | Validates and accumulates the change without immediate publication. | +| `release:pre-release` | Publishes an ordinary prerelease series for the next stable candidate. | +| `release:rc` | Publishes the next constant-identifier release candidate for the next stable candidate. | +| `release:announce` | Enables configured completion announcements for the frozen request. | + +Exactly one of major, minor, patch, or skip can own the decision. An ordinary +prerelease or RC is a modifier on a major, minor, or patch request; neither can +modify skip, and the two prerelease modes conflict with one another. +`release:announce` does not select a version or authorize publication. + +The required pull-request check recomputes when source, owned markers, or +`.github/release.config.yml` changes. It publishes one named required result and +fails closed for missing, conflicting, invalid, stale, or unsupported input. + +### Required pre-merge decision check {#required-pre-merge-decision-check} + +The decision check is the release resolver in validation mode. It validates the +current pull-request decision, configured fallback, modifier conflicts, source +scope, and whether the known accumulated range can be approved by this request. +It never publishes or reserves a version. Branch policy requires its exact +check name, so absent and stale results block merge as well as explicit failure. + +### Resolve the stable version {#version-computation} + +Version resolution starts from `0.0.0` when there is no stable history. Otherwise +it finds the greatest SemVer among: + +- completed stable releases; and +- permanently reserved stable versions from built, visible, retired, or + withdrawn intents. + +Resolve applies the aggregate increment to that baseline. For example, a first +patch release is `0.0.1`, a first minor release is `0.1.0`, and a deliberate +first stable major release is `1.0.0`. + +The resolved version is persisted as a provisional reservation before Build. +The reservation becomes permanent after the first successful build or any +external visibility. This separates safe coalescing of untouched work from the +non-negotiable rule that one visible or built coordinate always means one source +and one artifact. + +### Resolve prereleases and release candidates + +A prerelease applies its aggregate increment to the stable baseline and permanent +stable reservations, then appends a native identifier that sorts before the +normal version: + +```text +-. ``` -| Stage | Produces | Invariant | -| --- | --- | --- | -| **Resolve** | the version | the version is known before anything is built, so it can be baked in | -| **Build** | the artifact | the artifact is created exactly **once**, carrying its version | -| **Test** | a verdict | validation runs against the built artifact, not a rebuild of its source | -| **Publish** | released versions | the artifact is transferred unchanged to every target | - -Two consequences follow, and they are the point of the model: - -- **The version is identity, not metadata.** Because Resolve precedes Build, the - version is embedded in the artifact rather than attached to it. A manifest - version, an image label, and the tag agree because they came from one decision. -- **Recovery preserves artifact identity.** Retrying validation or publication of - an unchanged, already-built artifact reuses that artifact and its resolved - version. A correction that changes the output is a new release: it resolves a - new version and builds new bytes. An artifact is never patched, re-tagged, or - rebuilt under an existing version — that would publish something other than what - was tested. - -## Version computation - -For PR-driven releases, the shared resolver reads the owned labels and optional -`DefaultBump` in `.github/release.config.yml`. The setting accepts `patch`, -`minor`, or `major`; omitting it does not supply a level. For example, this -configuration explicitly chooses patch releases when no bump label is provided: +An ordinary prerelease uses the shared workflow's validated series identifier. +A dedicated release candidate always uses `rc`: -```yaml -# .github/release.config.yml -DefaultBump: patch +```text +2.5.0-rc.1 +2.5.0-rc.2 +2.5.0-rc.3 ``` -| Label | Meaning | Valid combination | -| --- | --- | --- | -| `release:patch` | Resolve the next patch version, overriding the configured default. | Alone or with `release:pre-release`. | -| `release:minor` | Resolve the next minor version, overriding the configured default. | Alone or with `release:pre-release`. | -| `release:major` | Resolve the next major version, overriding the configured default. | Alone or with `release:pre-release`. | -| `release:pre-release` | Publish the open pull request as a prerelease using the resolved bump. | With one explicit bump or a configured default; never with `release:skip`. | -| `release:skip` | Run validation without resolving or publishing a version. | Alone. | - -Resolve the decision in this order: - -1. Validate `DefaultBump` when present and reject conflicting owned labels. - An invalid setting is an error even when an explicit label is supplied. - Bare and unrelated labels do not participate. -2. Honor a valid `release:skip` as the explicit no-release decision and stop bump - resolution. -3. Use the single owned bump label when present; otherwise use the configured - `DefaultBump`. Record the chosen level and whether the label or setting - supplied it. -4. If neither supplies a level, fail with a missing-decision error that tells the - author to select a bump, configure the default, or choose `release:skip`. - There is no built-in patch fallback. Prerelease mode does not supply a bump. - -| PR input | `DefaultBump` | Decision-check result | +The counter is the next unreserved numeric value for that core and series. It is +stored unpadded so SemVer numeric ordering remains correct. The normal core +`2.5.0` is not consumed by any prerelease, but every published prerelease +identifier remains bound to its original content even after cleanup. + +Each publishing target validates its native prerelease flag against the +canonical version. A stable canonical version cannot be sent through a native +prerelease channel, and a canonical prerelease cannot be represented as stable. + +## Build, Verify, and Publish + +The lifecycle retains the familiar four-stage pipeline: + +```text +Resolve -> Build -> Verify -> Publish +``` + +`Verify` is the specification's target-neutral name for the existing Test stage. +Workflow job names can continue to use `Test` while the stage verifies the +frozen artifact. + +### Build + +Build receives the fixed source and resolved version. It produces one artifact, +stores it in retained release storage, calculates a cryptographic fingerprint, +and records both before any verification or publication. + +Build does not discover a newer branch tip, query labels again, edit the note +snapshot, or publish. If Build fails before a complete artifact is recorded, it +can retry with the frozen inputs. Once Build succeeds, no later phase can rebuild +or mutate the artifact under that version. + +### Verify + +Verify downloads the retained artifact by intent and confirms its fingerprint +before testing it. Source-level checks can run earlier, but release evidence must +show that the artifact intended for publication passed the required target, +integration, signing, and policy checks. + +Successful verification records the fingerprint, check identity, result, and +evidence location. A retry can reuse valid frozen evidence or repeat a +non-mutating check against the same bytes. + +### Publish + +Publish creates the destination-native artifact and release record from the +retained bytes and frozen note envelope. Every destination follows +[Publishing Targets](design-publishing-targets.md). + +For each required destination, Publish: + +1. Looks for a durable successful publication record. +2. If one exists, verifies that the destination coordinate still identifies the + recorded fingerprint and reuses it. +3. If none exists, verifies that the coordinate is free or already identifies + the same artifact. +4. Publishes the retained bytes and canonical metadata. +5. Reads the destination back where supported. +6. Records the coordinate, fingerprint, release-record location, and evidence. + +The release becomes `Complete` only when every frozen required destination has a +successful durable record. Native Latest state, moving aliases, and +announcements run after that transition. A partial release remains +`Publishing`, even if one destination makes its coordinate externally visible. + +## Phase-aware recovery + +A retry names the release-intent identity. The system loads durable state and +chooses the first unfinished boundary: + +| Recorded boundary | Retry action | +| --- | --- | +| `Resolved` with no successful artifact | Run Build again from frozen inputs. | +| `Built` | Restore the retained artifact, verify its fingerprint, and run Verify. | +| `Verified` | Restore the retained artifact and begin Publish. | +| `Publishing` | Verify recorded destinations and publish only unfinished ones. | +| `Complete` | Return the completed outcome; reconcile only independently retryable advertising work. | +| `Retired` | Return the retirement outcome without rebuilding or publishing. | +| `Withdrawn` | Return the withdrawal outcome without recreating or announcing the release. | + +Recovery never asks current labels, configuration, branch head, tag order, or +edited pull-request text to redefine frozen intent. Configuration can gain a new +destination only for a new release. It cannot silently broaden an in-flight +release. + +If retained storage is missing, the operator can restore a copy only when its +cryptographic fingerprint matches the recorded successful build. Otherwise the +intent must remain failed or be explicitly retired. Rebuilding similar source is +not proof of identical bytes and is forbidden under the reserved version. + +## Retirement and withdrawal + +Retirement and withdrawal solve different problems: + +| Operation | Applies to | Effect | | --- | --- | --- | -| `release:major` | `patch` or absent | Pass: explicit major overrides the default. | -| No owned release labels | `minor` | Pass: configured minor; record the setting as the source. | -| No release decision | Absent | Fail: missing decision; merge blocked. | -| `release:skip` alone | Valid or absent | Pass: no release; no bump is required. | -| `release:pre-release` alone | `patch` | Pass: configured patch in prerelease mode. | -| `release:pre-release` alone | Absent | Fail: mode does not supply a bump; merge blocked. | -| Multiple bump labels, or skip with another owned release label | Any | Fail: conflicting decisions; no fallback. | -| Any | Invalid value | Fail: invalid configuration; no fallback. | - -- **First release** starts from a baseline (`v0.1.0` or `v1.0.0`). Pre-`1.0.0` - breaking changes are `release:minor` per [SemVer §4](https://semver.org/#spec-item-4); - `release:major` is never auto-detected pre-`1.0.0`. -- The tag is created on the commit now at the head of the release branch — - squash, merge-commit, and rebase strategies alike. - -### Required pre-merge decision check - -PR CI runs the resolver read-only against the candidate release settings and -current owned labels, without creating tags, releases, or published artifacts. -An existing version-resolution check may own this validation; do not duplicate -the resolver. The check reports the effective decision and its source, not a -promised final stable version. - -The validator runs for every PR targeting a release branch, including changes -that will not publish. It re-runs when source, release labels, or release settings -change, so a stale result is not evidence for different inputs. Missing, -invalid, or conflicting decisions produce a failed check with an actionable -error. A valid skip reports success with a no-release result; path filters do -not skip the validator. - -Configure the check's exact name as required in the protected branch's ruleset -or branch protection, following [Merge Automation](../merge-automation/spec.md). -Manual merge and auto-merge both wait for it: failure, pending execution, and -absence block merge. A warning or an advisory, unrequired check is insufficient. -Human review still assesses whether the resolved level matches the audience -impact; CI validates the deterministic decision contract. - -The release run validates its actual inputs again before Resolve and Build. -Pre-merge validation does not replace release-time validation, but a known -missing decision is never deferred until after merge. - -### Optional ad hoc releases - -The standard release path is a pull request with a validated decision merged into a release -branch. `workflow_dispatch` is an optional extension, not part of the minimum -implementation. An implementation SHOULD omit it unless its product has a real -need to release already-reviewed content outside the merge flow. - -Where an ad hoc path exists, it requires an explicit bump, source ref, complete -release-note context meeting the [release evidence contract](#release-notes), and -reason. It resolves the source ref to an immutable commit and enters -the same Resolve → Build → Test → Publish pipeline as a merged pull request. It -does not infer a bump, bypass validation, rebuild an existing version, or make a -direct push into a release interface. - -Do not create an empty pull request to manufacture a release. It contains no -artifact-affecting change and makes the review trail imply a change that did not -happen. Retrying failed validation or publication is not an ad hoc release -either: rerun the existing release with the same artifact and version under the -[recovery rule](#the-pipeline). - -## Prereleases - -- **Branch-level** — a prerelease-type branch publishes on every push, using the - branch name as the identifier: `v1.3.0-dev.1`, `v1.3.0-dev.2`, … -- **PR-level** — `release:pre-release` with a resolved explicit or configured bump publishes - `v-.`: `base` is the next version from that bump, - `identifier` is the normalized branch name, and `counter` - auto-increments per push. -- Artifact-specific conventions replace the SemVer suffix where they exist - (`-alpha.N` for npm, `.devN` for Python). Release candidates use `-rc.N`, - auto-incrementing. -- **Cleanup** deletes prerelease tags, releases, and artifacts after the PR - closes (configurable); stable releases are never touched. - -## Path filtering - -`.github/release.config.yml` declares `release-paths` as ordered include/exclude -globs (excludes win). The workflow **always runs** so validation executes on -every merge; only the release step is skipped when no artifact-affecting path -changed. - -Derive these paths from the delivered product and its -[audience-facing contracts](../../Ways-of-Working/PR-Format.md#detecting-the-change-type), -not directory names alone. Include callable workflows and build configuration -that changes delivered runtime requirements or behavior. Do not retain an -exclusion that overrides an included consumer interface or artifact input. - -This example represents a workflow producer with a public `reusable.yml` entry -point and its local implementation; each producer lists its own artifact inputs. +| Supersede | Resolved, unbuilt, unexposed intent | Coalesces untouched work into a later reviewed range. | +| Retire | Built or externally visible incomplete intent | Stops recovery, preserves failure and reservation, and leaves the release incomplete. | +| Withdraw | Completed release | Makes the release ineligible while preserving completion history and reservation. | + +An authorized withdrawal writes an append-only withdrawal event, requests the +strongest target-native hide, unlist, yank, or delete operation, and then +reconciles discovery and aliases. Target limitations are recorded rather than +represented as stronger guarantees. For example, a target may permit unlisting +but not deletion of already downloaded content. + +Withdrawal does not delete the release intent, free the version, alter the +artifact, rewrite notes, or imply that consumers already holding the artifact no +longer have it. A fixed release uses a new reviewed version. + +## Current-version discovery and aliases + +Durable completed state is the authority for current-version discovery. The +algorithm: + +1. Select stable intents whose required destinations completed. +2. Exclude retired or withdrawn releases and any release that policy marks + ineligible. +3. Order canonical versions by SemVer precedence. +4. Return the greatest match or an explicit no-current-version result. + +An API, tag, or release lookup failure is an error. It is never converted into a +successful empty result. + +Aliases are optional advertising references layered on top of discovery. The +closed families are: + +- `latest`, one alias for the greatest eligible completed stable version; +- major, one alias such as `v3` for the greatest eligible `3.x.y`; and +- minor, one alias such as `v3.4` for the greatest eligible `3.4.x`. + +Each family defaults off and is enabled independently. Completion and withdrawal +run the same reconciliation algorithm. A normal completion never moves an alias +backward. Withdrawal can move one backward to the greatest remaining eligible +match, and removes it when no match remains. Prereleases never update stable +aliases. + +Moving Git tags require explicit consumer-side fetch behavior. Operators who +consume an owned alias follow [Accept moved release tags](accept-moved-release-tags.md). + +## Completion announcements + +Announcements are a post-completion delivery channel, not part of artifact +publication. Resolve freezes whether announcements are enabled and which +configured destinations apply. Complete freezes the canonical message context: +version, immutable release link, summary, and release-intent identity. + +Each announcement destination has its own delivery record and idempotency key. +The sender records attempts and acknowledgements. If acknowledgement is missing, +it may resend the same logical message; receivers therefore observe +at-least-once delivery and should deduplicate by release identity. + +Announcement failure does not roll back a complete release and does not permit a +new version, build, or publication. It remains an independently retryable +post-completion task. + +## Release notes and evidence {#release-notes} + +Release notes preserve contributor-authored pull-request content rather than +reducing it to a generated commit list. The frozen note snapshot follows +[PR Format](../../Ways-of-Working/PR-Format.md) and covers every reviewed pull +request in the accumulated range, including skipped changes. + +Manual requests supply equivalent reviewed context for direct or otherwise +unaccounted changes. Resolve fails if a source revision in the range has neither +reviewed note context nor an explicit evidence-gap record approved with the +manual request. + +The publication envelope augments, but does not rewrite, the authored note: + +- canonical and destination-native versions; +- previous completed stable baseline; +- fixed source and included range; +- aggregate decision, decision source, and fallback use; +- artifact fingerprint and verification evidence; +- destination coordinates and release-record links; +- note snapshot provenance; and +- completion, retirement, withdrawal, alias, and announcement events. + +A later explanatory correction is another append-only metadata event containing +before and after text, reason, actor, time, and source evidence. It cannot change +the version, source, artifact, decision, or behavior attributed to the release. + +## Release scope + +`Paths` is evaluated over the complete accumulated range. With no explicit +configuration, every path is release-affecting. A repository can narrow scope +only after accounting for direct and transitive artifact inputs. ```yaml -release-paths: - - ".github/workflows/reusable.yml" # public caller contract - - ".github/actions/**" # this workflow's local implementation - - "src/**" +Paths: + - src/** + - module/** + - build/** ``` -## Release notes - -The GitHub Release **name** is the resolved version. Its **body** preserves the -release-bound PR title and complete description, using -[PR Format](../../Ways-of-Working/PR-Format.md#description-structure) as the -authoring contract. Summary, user-facing changes, adoption, release impact, -consumer change records, template evidence, and both ending details blocks stay -intact. There is no parallel JSON/YAML contract and no extraction of only the -user-facing headings. - -### Bind the note to the released source - -1. **Resolve the evidence with the version.** Identify the release-bound PR or - ad hoc context and the immutable source to build. Resolve the version base - and the source comparison baseline; confirm that the consumer record - describes that delta. Capture the applicable title and complete body together - with the PR URL or context reference, source identity, and snapshot time. - Retain that snapshot as release evidence. -2. **Keep identity separate from authored prose.** Resolve the actual publication - coordinates through the existing version pipeline, not a number assigned by - the PR author. Carry them and the snapshot through Build and Test with the - same artifact. An authored statement that coordinates resolve at publication - is not replaced with a manual prediction. -3. **Publish the complete record.** Preserve the captured title and body - unchanged, with a clearly separated publication envelope. Compare the - published authored portion with the snapshot; truncation, summarization, - missing evidence, or a source mismatch is a publication failure, not success. - Hand the same complete record to every note-bearing publishing target and - [Downstream Release Propagation](../downstream-release-propagation/design.md). - -The envelope records these resolved facts without becoming a second authored -release note: - -| Field | Value | -| --- | --- | -| Release identity | Actual version, stable/prerelease mode, tag, immutable source commit, and artifact identity or digest where applicable. | -| Effective decision | The resolved semantic effect and its owned-label or configured-policy source; [version computation](#version-computation) remains authoritative. | -| Version base | The actual version/source used to compute the version, or the explicit initial versioning baseline. | -| Change baseline | The release and immutable source against which the consumer delta is described, plus a source comparison link; explicitly no predecessor for an initial release. | -| Note provenance | Release-bound PR URL or ad hoc context, its associated source identity, and snapshot time. The retained authored snapshot is the content reference, not the PR's later mutable body. | - -Version base and change baseline can differ, particularly for prereleases and -bundled promotion. Recording both avoids presenting a versioning calculation as -proof of the code a consumer crosses. The target template identity and -compatibility evidence come from the authored record; a publisher does not -substitute the latest template or infer historical compatibility from current -documentation. - -### Release-bound records - -| Publication path | Authored record | -| --- | --- | -| Single merged PR | That PR's complete title and description, reconciled with the resolved source comparison. | -| Bundled release | The release-bound integration PR covers every bundled delta from the declared change baseline, not just the most recent feature PR. It links the contributing work as supporting evidence. | -| Optional ad hoc dispatch | Complete reviewed release-note context with the same adoption, consumer-change, template, and release-impact evidence. Record the dispatch source and reason; do not create or imply an empty PR. | -| Prerelease | The PR or integration record appropriate to that published source, captured for that release. Later edits to the final PR do not overwrite the prerelease snapshot or attribute unreleased behavior to it. | - -If the relationship between a record and its source cannot be established, -stop the affected publication and register the evidence gap. The process does -not substitute the newest note, guess a baseline, or treat an empty adoption -section as a no-action result. - -### Correct published metadata without changing history - -A note correction is an audited metadata operation, not another release run: - -1. Establish the release-to-source and PR relationship from immutable source - comparisons and contemporary evidence. Preserve source-specific prerelease - records rather than copying a later final-PR body over them. -2. Capture original and proposed content, reason, evidence links, actor, and - time in a linked audit issue or durable attached artifact. Coordinate active - PR ownership; do not add closing keywords to audit prose. -3. Re-read each target before writing. If another edit changed it, reconcile the - correction rather than overwriting that edit. Apply only the established - PR/release metadata changes and retain their correspondence. -4. Re-read the result and confirm that the correction changes no artifact, - asset, tag, SHA, release decision, or behavior attributed to an old version. - Record unverifiable facts as unresolved gaps instead of inventing actions. - -The audit belongs in GitHub issues and release/PR metadata, not a product -documentation changelog. A correction to bytes still follows the -[new-artifact recovery rule](#the-pipeline); editing notes never bypasses it. - -## Release output - -1. A git tag `vX.Y.Z` on the release-branch commit — always. -2. The published artifact where one lives outside git — a container image - (`:` and `@`), a package in its registry. For Action, - workflow, and module artifacts the tag itself **is** the artifact. -3. A GitHub Release whose name is the version, carrying the note and the - publication envelope, including the tag's resolved source commit and the - immutable artifact identity. - -## Publishing targets - -Publish is the only stage that knows where an artifact goes, and it reaches every -destination through one abstraction: a **publishing target**. A target is any -destination that accepts a versioned artifact and serves it to consumers — the -GitHub Release itself, a package registry, an extension marketplace, a container -registry. - -The release process is written against the target *contract*, never against a -specific target. Each target documents how it answers six questions — version -scheme, prerelease representation and sort order, immutability, unpublish -behaviour, floating-tag support, and where its release record lives — in -[Publishing Targets](design-publishing-targets.md). Adding a destination means -writing that contract and a publish step; it does not change Resolve, Build, -Test, or the spec. - -Where a repository has more than one target, publishing is **all-or-nothing** for -a version: - -- Targets are attempted in a defined order, and each is idempotent — publishing - an already-published version is a success only when it identifies the same - immutable artifact. A version collision with different bytes is an error, so a - re-run completes the set rather than accepting changed output. -- A target that rejects the version fails the release. The version is not - advertised as available until every target holds it. -- A partial publication resumes Publish for the **same** artifact and the same - version. It never resolves a new version to work around a single failed target, - because the targets that already succeeded hold that immutable version. - -## Floating tags - -Floating tags are optional, mutable pointers published alongside the immutable -version tag, for consumers that want to track a line rather than a point: - -| Tag | Points at | Moves when | +Validation still runs when no path is eligible, when the decision is skip, or +when a direct push cannot publish. No universal exclusion exists for docs, +tests, or workflows: a documentation site ships docs, test fixtures can be +packaged, and workflow files can define release behavior. + +## Consumer update policies + +The consumer resolves one of five policies: + +| Policy | Producer mechanism | Durable consumer reference | | --- | --- | --- | -| `latest` | the newest stable version | any stable release | -| `vMAJOR` | the newest stable version in that major | a stable release within that major | -| `vMAJOR.MINOR` | the newest stable patch in that minor | a stable patch within that minor | - -Three rules keep them safe: - -- **Prereleases never move a floating tag.** Only a stable release advances one, - so a floating tag never points at something not promoted for adoption. -- **A floating tag never moves backwards.** It only advances, so a consumer - following it never silently downgrades. -- **Only controlled release automation moves a floating tag.** Humans and ad hoc - workflows do not create or repoint one. The automation publishes the immutable - version first, then moves only the aliases that release is eligible to advance. -- **A major tag stays inside its compatibility line.** `vMAJOR` advances only - for compatible stable patch and minor releases in that major. A breaking - release creates the next major tag and leaves the previous one in place. -- **Floating tags are controlled references only for owned automation.** An - organization- or initiative-owned Action or reusable workflow may be consumed - through its controlled `vMAJOR` tag. External automation and anything requiring - byte-for-byte reproducibility pins to the immutable version, digest, or SHA - ([supply chain](../../Coding-Standards/Security.md#supply-chain)). - -## Serialised releases - -Release runs for the same ref are **serialised** and **queue rather than -cancel** — an in-flight release is never aborted mid-write, since it may be -part-way through creating a tag or pushing an artifact. The shared workflow -declares a concurrency group keyed by workflow and ref, with -`cancel-in-progress` disabled: +| `latest` | Discover the greatest eligible completed stable release. | The discovered immutable version or fingerprint required by trust policy. | +| `lock-major-boundary` | Producer major alias, such as `v3`. | Moving alias only inside the allowed trust boundary. | +| `lock-minor-boundary` | Producer minor alias, such as `v3.4`. | Moving alias only inside the allowed trust boundary. | +| `lock-specific-version` | Exact immutable version. | Exact version. | +| `lock-immutable-fingerprint` | Content digest, commit SHA, or equivalent. | Immutable fingerprint. | + +A requested boundary is unsupported when the producer does not publish the +matching alias; it is never widened silently. When a consumer field accepts only +one opaque reference, a text such as `>=3,<4` is not a range expression. The +producer carries the bound through its controlled alias. + +Moving references are allowed only for producers inside the consumer's declared +trust boundary. Another team is external even when it belongs to the same +company. External dependencies use the strongest immutable reference available. +The [Dependencies standard](../../Coding-Standards/Dependencies.md) owns the +broader pinning and update trade-off. + +Before `1.0.0`, SemVer permits a breaking change in each minor version. A major +alias such as `v0` therefore crosses potentially breaking `0.y.z` releases. A +consumer that wants patch-only movement before `1.0.0` uses a minor alias such +as `v0.4`, not `v0`. + +## Ordering, idempotency, and reconciliation + +Workflow concurrency groups queue same-line work and never cancel an in-flight +publication. Durable reconciliation provides the stronger guarantee that source +order survives missed events, worker replacement, manual retries, and finite +execution queues. + +For each stable line, the reconciler finds source revisions not yet accounted +for by a complete, retired, or superseding intent. It processes the oldest +unsettled range first. Duplicate triggers use an idempotency key derived from the +repository, stable line, fixed source, and request kind, and return the existing +intent rather than creating another. + +This ordering prevents a later fast run from taking a version or release range +that belongs to earlier unresolved work. + +## Configuration + +Repositories keep the existing MSX configuration file: + +```text +.github/release.config.yml +``` + +The current baseline surface remains: ```yaml -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false +ReleaseBranches: + - main +DefaultBump: patch +Paths: + - src/** + - module/** ``` -Serialisation is provided once by the reusable workflow so every repository -inherits it; the mechanism is the -[GitHub Actions standard](../../Coding-Standards/GitHub-Actions.md#concurrency). -The single-stable-branch rule above is what keeps the production version under -one authority — the stable branch is the only ref that ever cuts a production -release, and its runs are serialised like any other. +- `ReleaseBranches` identifies branches evaluated by the existing schema. The + intended lifecycle permits exactly one value to authorize stable publication; + any additional branch can validate or publish prereleases only. +- `DefaultBump` accepts only `major`, `minor`, or `patch` and applies only to an + associated reviewed pull request. +- `Paths` declares artifact-affecting scope. Omission means every path. + +The intended configuration surface additionally needs independently disabled +`latest`, major, and minor aliases; publishing destinations; announcement +destinations; retained-artifact policy; and destination-specific native mapping. +Those fields are not documented as YAML keys until the shared implementation +defines and validates them. Unknown fields and values fail closed; repositories +must not invent local release schema. + +Repositories choose policy. Shared automation owns resolution, durable state, +SemVer calculation, artifact handoff, retry rules, target adapters, discovery, +alias reconciliation, and evidence format. + +## Intended and implemented behavior + +The lifecycle above is the target design. The implementation lives in shared +release automation rather than this documentation repository, and each +repository remains governed by the workflow revision it invokes. The table +records the audited shared baseline that this capability documentation +synchronizes against; an **intended** row MUST NOT be treated as available until +the invoked workflow documents and exposes it. + +| Surface | Baseline coverage | Intended behavior still required | +| --- | --- | --- | +| Pull-request release decision | Implemented for explicit major, minor, patch, skip, ordinary prerelease, and `DefaultBump`. | Aggregate the full unreleased range and preserve the frozen decision source. | +| Direct pushes | Validation and push-triggered processing exist. | Prevent fallback-based publication and require an approved manual aggregate request. | +| Manual stable releases | Not implemented as a release-creation path. | Fixed stable-line source, explicit aggregate increment, complete notes, and range approval. | +| Dedicated RC mode | Not implemented. | `release:rc`, constant `rc` identifier, monotonic counters, and conflict with `release:pre-release`. | +| Durable release intent | Workflow-run evidence exists; resolution is based on current pull-request metadata and tags. | Durable frozen inputs, lifecycle state, idempotency, and permanent version reservations. | +| Build-once recovery | A run can hand one build artifact to later jobs. | Retain successful bytes across runs, verify the fingerprint, and resume by recorded phase without rebuilding. | +| Multi-target completion | Publication can target configured destinations. | Durable per-destination progress and completion only after every required target succeeds. | +| Current-version discovery | Version calculation is tag-oriented. | Select the greatest eligible completed stable intent and exclude incomplete or withdrawn releases. | +| Moving aliases | Existing shared flows can update configured aliases. | Closed opt-in families, monotonic completion updates, and withdrawal-aware reselection or removal. | +| Announcements | Durable announcement delivery is not implemented. | Frozen message context, per-destination journal, idempotency, and at-least-once retry after completion. | +| Retirement and withdrawal | End-to-end lifecycle operations are not implemented. | Preserve failure or completion history, permanent reservations, target-native withdrawal evidence, and replay-safe terminal outcomes. | +| Immutable GitHub Releases | Exact version tags are treated as immutable by policy. | Reconcile and verify the repository's immutable-release setting where GitHub supports it. | +| Published-note correction | Release notes and evidence are published. | Append-only correction records with source evidence and unchanged release identity. | -## Configuration surface +Until the durable lifecycle is implemented, an operator MUST NOT work around a +partial release by rerunning against changed metadata or rebuilding under the +same exposed version. Stop, preserve evidence, and roll forward with a newly +approved version when byte identity cannot be proven. -| Surface | Where | -| --- | --- | -| Release branches + type | `.github/release.config.yml` | -| Optional default bump | `DefaultBump` in `.github/release.config.yml` | -| Explicit bump / prerelease / skip | `release:` PR label | -| Pre-merge decision validation | named PR check required by the branch ruleset or protection | -| Optional ad hoc release | `workflow_dispatch` inputs | -| Path filter | `.github/release.config.yml` | -| Prerelease cleanup toggle | release config / workflow input | -| Publishing targets | reusable-workflow input + GitHub environment; see [Publishing Targets](design-publishing-targets.md) | +## Design decisions + +### The durable intent, not a tag, is the release authority + +Tags can be missing, moved when explicitly used as aliases, or present before all +destinations complete. A durable intent can state which artifact, source, +approval, and destinations the tag is expected to represent and can distinguish +partial publication from completion. + +### Manual release uses the same pipeline + +A separate manual pipeline would create a second version algorithm and weaker +evidence path. Manual requests therefore change only the trigger and approval +source; they still Resolve, Build, Verify, and Publish one retained artifact. + +### Announcements follow completion + +Treating a chat or webhook message as a publishing destination would either +announce partial releases or make a transient message failure roll back a valid +artifact. Separating delivery retains truthful completion and retryable +communication. + +### Withdrawal changes eligibility, not history + +Deleting history would make audit, replay, and version reservation ambiguous. +An append-only withdrawal preserves what happened while letting discovery and +aliases stop recommending the release. ## Where this connects -- [Spec](spec.md) — the requirements this design delivers. -- [Publishing Targets](design-publishing-targets.md) — the contract each destination documents. -- [Downstream Release Propagation](../downstream-release-propagation/design.md) — consumes the release note and immutable reference. -- [GitHub Actions](../../Coding-Standards/GitHub-Actions.md) — how the workflow itself is authored (SHA pins, least privilege, concurrency). -- [Security](../../Coding-Standards/Security.md#supply-chain) — why consumers pin to immutable references. +- [Spec](spec.md) — the normative release contract. +- [Publishing Targets](design-publishing-targets.md) — destination-native + mappings and guarantees. +- [Accept moved release tags](accept-moved-release-tags.md) — consumer recovery + for controlled aliases implemented as Git tags. +- [Automation Labels](../../Ways-of-Working/Automation-Labels.md) — exact marker + meanings and conflicts. +- [GitHub Actions](../../Coding-Standards/GitHub-Actions.md) — immutable external + action pins and controlled owned aliases. +- [PR Format](../../Ways-of-Working/PR-Format.md) — release-note and consumer + evidence source. diff --git a/src/docs/Capabilities/release-management/index.md b/src/docs/Capabilities/release-management/index.md index cf760a7..e4a1338 100644 --- a/src/docs/Capabilities/release-management/index.md +++ b/src/docs/Capabilities/release-management/index.md @@ -1,23 +1,28 @@ --- title: Release Management -description: How a source change becomes a versioned, immutable artifact, driven entirely on the GitHub platform. +description: Durable version resolution, publication, recovery, withdrawal, and consumer update policy for immutable releases. --- # Release Management -Turning a merged change into a versioned, immutable artifact — a container -image, a GitHub Action or reusable workflow, a language package, a Terraform -module — paired with a GitHub Release and a git tag, normally driven by -pull-request labels. An implementation may add a GitHub-native ad hoc release -path when its product needs one. No release CLI, no hand-edited version file, -no tagging ritual. +Turning approved source into a versioned, immutable artifact — a container +image, GitHub Action or reusable workflow, language package, Terraform module, +or documentation site — with durable release state and evidence. The ordinary +path starts from a reviewed merge; an approved manual request can release an +accumulated range. Both paths resolve once, build once, resume by recorded +phase, and advertise only completed eligible releases. + +Contributors express release intent through GitHub. Maintainers can recover, +retire, or withdraw releases without reusing versions or rewriting history, and +consumers select an update policy that matches their trust boundary. | Page | Description | | --- | --- | -| [Spec](spec.md) | Requirements for release management — automatic, policy-driven, versioned releases driven entirely on the GitHub platform. | -| [Design](design.md) | How release management is built — a shared reusable workflow that resolves an explicit or configured SemVer bump, builds once, and publishes. | -| [Publishing Targets](design-publishing-targets.md) | The contract every publishing destination documents, with GitHub Releases as the reference target. | +| [Spec](spec.md) | Requirements for durable, recoverable, policy-driven releases and trustworthy consumer updates. | +| [Design](design.md) | Durable release intents, lifecycle transitions, recovery, withdrawal, aliases, announcements, and implementation coverage. | +| [Publishing Targets](design-publishing-targets.md) | Destination contracts for version mapping, prereleases, immutability, withdrawal, aliases, constraints, and release records. | +| [Accept Moved Release Tags](accept-moved-release-tags.md) | Refresh an owned moving release alias locally and configure Git to keep it current. | diff --git a/src/docs/Capabilities/release-management/spec.md b/src/docs/Capabilities/release-management/spec.md index 67579fd..e3d44fc 100644 --- a/src/docs/Capabilities/release-management/spec.md +++ b/src/docs/Capabilities/release-management/spec.md @@ -1,98 +1,627 @@ --- title: Spec -description: Requirements for release management — automatic, policy-driven, versioned releases driven entirely on the GitHub platform. +description: Requirements for durable, recoverable, policy-driven releases and trustworthy consumer updates. --- # Release Management — Spec ## Premise -A release turns a source change on a release branch into a **versioned, -immutable artifact** that other systems depend on. Merging a pull request *is* -releasing. Releasing MUST be automatic, predictable, and driven entirely on the -GitHub platform — a contributor focuses on the code they contribute, not a -release CLI, a hand-edited version file, or a tagging convention. +Release management turns approved source into a **versioned, immutable release** +that consumers can trust. A release is normally the result of merging reviewed +change, while a controlled manual request can release already-reviewed +accumulated change. Both paths use the same decision, build, verification, +publication, and evidence controls. -### Principles +The capability preserves enough durable state to finish an interrupted release +without rebuilding or assigning its version to different content. Consumers can +discover only completed, eligible stable releases and can select an explicit +update policy that matches their trust boundary. -This capability rests on the [Principles](../../Ways-of-Working/Principles/index.md): +## Problem and importance -- **[Everything as Code](../../Ways-of-Working/Principles/Engineering-Practices.md#everything-as-code).** The release process and version decision are version-controlled, never a GUI action or manual tag. -- **[Decision before change](../../Ways-of-Working/Principles/AI-First-Development.md#decision-before-change).** The pull request is the decision point; its review gate approves the code *and* the release. An owned bump label records a per-change decision; a version-controlled repository default records the policy used when no level is supplied. -- **[Extensible by default](../../Ways-of-Working/Principles/Software-Design.md#extensible-by-default).** The rules are technology-agnostic at the core, with defined extension points per artifact type. A new artifact type supplies a convention and a publish step, not a new process. +A tag or successful workflow run is not enough to prove that a release completed. +Publication can expose one destination before another fails, mutable pull-request +metadata can change before a retry, and a later commit can reach the stable branch +while an earlier artifact is still incomplete. Recomputing from current state can +then publish different bytes under a reserved version, omit accumulated changes +from the notes, or advertise an incomplete release as current. + +Release management makes the release intent durable. It fixes the approved source, +decision, version, notes, artifact identity, required destinations, and stage +progress, then advances that intent safely through publication. This gives +contributors a predictable release path, operators a recoverable process, and +consumers an accurate version and update contract. + +## Users and jobs + +- **A contributor** records the compatibility impact and consumer evidence while + proposing a change, without operating separate release tooling. +- **A reviewer** approves the source, release decision, and complete consumer + effect together. +- **A maintainer** can release approved accumulated change, resume an interrupted + attempt, retire an unrecoverable attempt, or withdraw a completed release + without rewriting history. +- **A consumer** can discover the current completed stable release and choose how + far a dependency may move within the applicable trust boundary. ## Scope -Applies to any repository that produces a versioned artifact on merge to a -release branch. One test decides applicability: **does merging produce a -versioned, immutable output that something else consumes by version?** If yes, -this capability governs the release. If no, there is nothing to release. +In scope: + +- Version resolution, release approval, and publication for repositories that + produce a versioned artifact. +- Stable releases, prereleases, release candidates, manual release requests, + retries, retirement, and withdrawal. +- Release notes, immutable release evidence, announcements, current-version + discovery, and optional moving aliases. +- Consumer update policies and the producer references needed to express them. + +Out of scope: + +- Deploying a released artifact into a runtime environment. +- Processing a release in dependent repositories. +- Repository rulesets and branch protections, except for the release checks they + must require. ## Requirements -- **Semantic versioning.** Versions follow [SemVer 2.0.0](https://semver.org/) (`vMAJOR.MINOR.PATCH`), derived automatically — never written by hand. -- **A resolved PR release decision.** The repository MAY configure `DefaultBump` as `patch`, `minor`, or `major`; invalid values MUST fail. Multiple owned bump labels, or `release:skip` combined with another owned release label, MUST fail. A valid `release:skip` MUST select no release without resolving a bump. For publishing decisions, one owned `release:patch`, `release:minor`, or `release:major` label MUST take precedence over the configured default; without a bump label, a valid `DefaultBump` MUST supply the level; without either source, automation MUST fail with a missing-decision error, never assume `patch`. `release:pre-release` MAY use either the explicit or configured bump; the mode alone does not supply a level. Bare or unrelated labels MUST be ignored. Conventional commit messages are **not** required. -- **A release per merge.** One eligible merged PR with a resolved bump to a release branch is one release, and the PR review gate is the release gate. `release:skip` validates without publishing. This pull-request path is the required release interface. -- **Decision validation blocks merge.** Every PR targeting a release branch MUST receive a named release-decision CI check required by the branch ruleset or protection. Missing decisions, invalid defaults, and conflicting owned labels MUST fail that check before merge, not only during publication. Source, release-label, and release-settings changes MUST re-evaluate the decision. A failing, pending, or absent required result MUST block both manual and automated merge; a log message, warning, or skipped validator is not enforcement. A valid `release:skip` MUST report a successful no-release decision, not skip the check. -- **Ad hoc release is optional.** An implementation MAY expose `workflow_dispatch` when its product needs an ad hoc release outside the merge flow; implementations are not required to support it. A dispatch MUST require an explicit release decision and release-note context, and MUST use the same version, build, validation, immutability, and publication controls as a merged pull request. A direct push MUST NOT be an ad hoc release interface, and an empty pull request MUST NOT be created solely to trigger a release. -- **Version before build.** The version MUST be resolved before the artifact is built, so the version is part of the artifact's identity rather than a label attached afterwards. -- **Build once.** The artifact MUST be built exactly once and MUST NOT be altered after it is built. The same bytes flow through validation and publishing. Rebuilding to publish means the tested artifact and the published artifact are different artifacts. -- **Stable and prerelease.** Every release is either **stable** (the latest version to adopt) or a **prerelease** (testable, not promoted to latest). A prerelease MUST be obtainable from an open pull request carrying `release:pre-release`, using its explicit or configured bump, and/or from a prerelease branch. -- **Serialised releases.** Only one release process runs against a given version of the codebase (the same ref) at a time. A release mutates shared, version-anchored state — the tag, the version counter, the published artifact — so overlapping runs on the same ref MUST NOT race, and an in-flight release is never interrupted. -- **A single production authority.** Exactly one branch is in charge of the production (stable) version, so consumers get one unambiguous latest stable release and two branches can never publish competing production releases. -- **Notes from the contributor's own words.** The GitHub Release name is the version; its body MUST preserve the release-bound pull request title and complete description, or equivalent complete release-note context for an optional ad hoc dispatch. The authored record follows [PR Format](../../Ways-of-Working/PR-Format.md#description-structure); adoption and technical details MUST NOT be omitted or summarized away. -- **Only artifact-affecting changes release.** A change that does not affect the delivered artifact or its supported consumer contracts MUST carry `release:skip` and MUST NOT produce a release — though validation still runs on every merge. Documentation and internal CI configuration qualify only when they meet that condition. An Action or reusable workflow is itself a product for its callers; its interface and behavior MUST NOT be dismissed as internal tooling because of the file path. -- **Immutable references.** Consumers pin to the most immutable reference available — a container digest or a commit SHA — never a mutable tag. -- **Publish through a target contract.** Every publishing destination is reached through the same [publishing-target contract](design-publishing-targets.md), so the release process stays one process regardless of how many destinations a repository has. Adding a destination supplies a contract and a publish step; it MUST NOT change the release process. -- **All-or-nothing across targets.** Where a repository publishes one artifact to more than one destination, a version MUST NOT end up present on some destinations and absent from others. Partial publication is a failure, reported as one, and resumed by completing the remaining destinations with the same immutable artifact and version. -- **Recovery distinguishes retries from changed output.** Retrying validation or publication of unchanged bytes MUST reuse their artifact and version. A correction that changes the bytes MUST create a new versioned artifact; an existing version is never overwritten or reused. -- **Standard GitHub primitives only.** Pull requests, labels, comments, and, where implemented, workflow dispatch — no external tooling beyond `gh` and GitHub Actions. - -### Release evidence - -- **Incremental consumer contract.** Every release MUST describe its consumer-facing delta against an identified release/source baseline, including applicability, exact actions, and verification, or explicit no-action evidence. Breaking behavior MUST be documented independently of its semantic-version classification. An applicable integration template MUST be identified by repository and verified compatible immutable commit, with producer-source compatibility evidence and linked template work or a justified no-change result. -- **Resolved coordinates.** The release process MUST record the actual target version, tag, immutable source and artifact identity, effective release decision and its source, version-computation base, and consumer-change baseline. The version base and change baseline MUST be distinguished when they differ. A first release MUST identify its initial versioning baseline and lack of a prior release. Authors MUST NOT assign a final version before resolution. -- **Traceable publication.** Generated identity and provenance MAY surround the authored note as a distinct envelope; they MUST NOT replace or rewrite it. The release MUST retain the note's source identity and snapshot provenance so its relationship to the published code is inspectable. Every destination carrying release notes, including downstream propagation, MUST receive the complete record. -- **Correct release scope.** A bundled release's integration PR MUST cover every bundled consumer delta. An optional ad hoc release MUST provide equivalent evidence without implying a nonexistent PR. A prerelease MUST preserve the note appropriate to its immutable published source, not a later final-PR description that describes different code. -- **Metadata-only correction.** A correction to published notes MUST retain an audit of the original and corrected content, reason, supporting source evidence, actor, and time. It MUST NOT alter release artifacts, tags, source identities, or the behavior attributed to a version. Unverifiable historical facts MUST be registered as gaps, not guessed. - -### Consumer update policies - -A consumer chooses how much version movement it accepts. Selecting a policy is a **consumer-side** concern — the release capability's obligation is to publish versions that make every policy expressible: - -| Policy | Accepts | Suits | -| --- | --- | --- | -| **Latest** | any newer version, including major | consumers that track the current release and have tests to catch breakage | -| **Lock major boundary** | newer minor and patch within one major | the default for a library dependency under SemVer | -| **Lock minor boundary** | newer patch only | consumers that accept fixes but no new surface | -| **Lock specific version** | nothing; movement is an explicit change | consumers under change control | -| **Lock immutable fingerprint** | nothing; the reference is a digest or SHA | consumers that require the exact bytes to be provable | - -Because versions are semantic, immutable, and published once, a consumer can adopt any of these without the producer knowing which one it chose. - -## Success criteria - -- Merging an eligible PR with an explicit or configured bump produces a GitHub Release, a git tag, and (where one exists) a published artifact, with no manual step. -- For a publishing PR, an explicit owned bump label overrides the configured default; without that label, a valid `DefaultBump` supplies the level and is recorded as its source. -- A PR with no explicit level, configured default, or valid no-release decision fails the required decision check and cannot merge. Invalid defaults and conflicting owned labels also block merge rather than selecting a fallback. -- Removing the only decision source or changing its inputs re-evaluates the PR check; a prior result does not validate different inputs. -- An open pull request carrying `release:pre-release` and an explicit or configured bump publishes a prerelease without promoting it to latest. -- The artifact that consumers download is byte-identical to the artifact that passed validation. -- A documentation-only merge carrying `release:skip` produces no new version but still runs its CI checks. -- Two release runs for the same ref never overlap; the second waits for the first to finish rather than racing it. -- Only the single production branch ever publishes a stable release. -- A version that reaches one publishing target reaches all of them, or the release is reported as failed. -- Every release is linkable and records its immutable artifact reference. -- The published authored title and body match the release-bound snapshot in full, including adoption, consumer/template evidence, and maintainer details. -- A consumer can identify the actual target, version base, change baseline, and any applicable compatible template without relying on a moving branch, alias, or today's documentation. -- A bundled or ad hoc note covers its complete change range, and a prerelease note never gains instructions for code absent from that prerelease. -- A published-note correction is traceable to released-source evidence while all artifact and source identities remain unchanged. +Requirements use [BCP 14](https://www.rfc-editor.org/info/bcp14) keywords. +Identifiers are append-only and MUST NOT be renumbered or reused. + +### FR1 — Every release has an approved decision and fixed source {#fr1} + +The release decision MUST be major, minor, patch, or skip. For a change with an +associated pull request, one explicit owned decision MUST override the +repository's configured fallback. Missing, invalid, or conflicting decisions +MUST fail closed. A skip decision MUST suppress immediate publication without +removing the change from a later release range. Commit-message syntax MUST NOT +select the decision. + +The ordinary stable release path MUST be an approved pull request merged to the +stable line. A direct push with no associated review MUST validate but MUST NOT +inherit the repository fallback or publish a stable release. + +A manual stable release MUST name a fixed source revision on the stable line, an +explicit aggregate increment, complete release-note context, and approval that +covers the entire included change range. Invoking the release mechanism is not +approval. A retry MUST identify an existing release intent and reuse its frozen +decision. + +#### FR1 scenarios + +```gherkin +Scenario: An explicit decision overrides the fallback + Given the repository fallback is patch + And an approved pull request carries the owned minor decision + When release resolution runs + Then the resolved decision is minor + And the fallback is recorded as not used + +Scenario: A direct push does not inherit the fallback + Given the repository fallback is patch + And a direct push has no associated pull request + When release resolution runs + Then validation runs + And no release is published +``` + +### FR2 — The increment states the complete compatibility impact {#fr2} + +Versions MUST follow [Semantic Versioning 2.0.0](https://semver.org/). For a +stable public contract at `1.0.0` or later, breaking change requires major, +backward-compatible capability requires minor, and backward-compatible repair +requires patch. Before `1.0.0`, breaking or additive change requires minor and a +compatible repair requires patch; reaching `1.0.0` remains a deliberate major +decision. + +The effective increment MUST cover every unreleased change included from the +previous completed stable source through the fixed release source. Known pending +increments establish a minimum aggregate increment: major takes precedence over +minor, which takes precedence over patch. Skipped and direct changes remain in +the range and require an explicit reviewed aggregate decision when their +compatibility impact is not already approved. + +#### FR2 scenarios + +```gherkin +Scenario: Accumulated decisions establish a minimum increment + Given the unreleased range contains approved patch and major changes + When an aggregate release decision is resolved + Then the effective increment is at least major + And a patch decision is rejected +``` + +### FR3 — One stable authority produces one discoverable current version {#fr3} + +Exactly one source line MUST be authorized to produce stable releases. +Current-version discovery MUST select the greatest eligible completed stable +version by SemVer precedence, independently of tag listing order or optional +aliases. + +Incomplete, failed, retired, and explicitly withdrawn releases MUST NOT be +current. If no eligible completed stable release remains, discovery MUST report +that no current stable version exists. A lookup error MUST be surfaced and MUST +NOT be interpreted as withdrawal or absence. + +#### FR3 scenarios + +```gherkin +Scenario: Partial publication is not current + Given version 2.4.0 has reached only one of two required destinations + And version 2.3.2 is the greatest eligible completed stable release + When current-version discovery runs + Then it returns version 2.3.2 + And version 2.4.0 remains incomplete +``` + +### FR4 — Prereleases and release candidates identify the next stable candidate {#fr4} + +A published prerelease MUST use the core version produced by applying its +resolved aggregate increment to the stable baseline and permanent reservations. +It MUST sort before the normal version with the same core and MUST NOT consume +that stable version. Successive versions in one prerelease series MUST increase +by SemVer precedence. + +An ordinary prerelease MUST identify its source series. A dedicated release +candidate MUST use the constant `rc` identifier and an unpadded increasing +counter, such as `2.5.0-rc.1`. A single request MUST NOT select both ordinary +prerelease and release-candidate modes. + +A prerelease MUST be testable but MUST NOT become the current stable release or +advance stable aliases. Cleanup MAY remove an exact prerelease and its record, +but MUST NOT reuse its identifier for different content. + +#### FR4 scenarios + +```gherkin +Scenario: A release candidate leaves its stable version available + Given the stable baseline is 2.4.1 + And the approved aggregate decision is minor + When release candidates are published + Then they use 2.5.0-rc.1, 2.5.0-rc.2, and later counters + And the stable version 2.5.0 remains available +``` + +### FR5 — A durable record is authoritative for release lifecycle state {#fr5} + +Before build, the capability MUST persist a release intent containing its +identity, fixed source and included range, approval evidence, aggregate decision, +resolved version, notes, required destinations, and current stage. After build it +MUST also retain the artifact fingerprint and verification and publication +progress. + +The lifecycle MUST distinguish pending or resolved work, successful build, +successful verification, partial publication, completion, failure, explicit +retirement, and withdrawal of a completed release. Workflow runs, tags, and +destination listings are observations of that lifecycle; none alone is the +authoritative state. + +Resolve MAY allocate a version before build. The reservation becomes permanent +when a build succeeds or any release coordinate becomes externally visible. A +permanent reservation survives failure, retirement, withdrawal, and deletion at +a destination. A resolved request with no successful build and no external +visibility MAY be explicitly superseded while coalescing pending work. + +#### FR5 scenarios + +```gherkin +Scenario: External visibility permanently reserves a version + Given a release exposes version 3.1.0 at one destination + And publication then fails elsewhere + When a later release is resolved + Then version 3.1.0 remains reserved for the original source and artifact + And the later release cannot reuse it +``` + +### FR6 — The verified artifact is the artifact that is published {#fr6} + +The version MUST be fixed before the artifact is built. Build MUST create the +artifact once, after which the artifact MUST remain unchanged. Verification and +every publishing destination MUST consume those same bytes. + +The successful artifact, its fingerprint, and verification evidence MUST remain +available for the entire unresolved lifetime of its release intent. A failed +build that produced no complete artifact MAY run again. A correction that changes +bytes MUST resolve a new version and produce a new artifact. + +#### FR6 scenarios + +```gherkin +Scenario: Build, verification, and publication share one artifact + Given a release is resolved to version 1.8.0 + When build, verification, and publication succeed + Then the published fingerprint equals the verified build fingerprint + And no later stage rebuilt or modified the artifact +``` + +### FR7 — Recovery resumes the recorded phase without changing identity {#fr7} + +A retry MUST load the original release intent and resume only unfinished or +failed work. It MUST reuse the frozen source, decision, version, notes, +destinations, successful artifact fingerprint, verification evidence, and +completed publications. It MUST NOT re-resolve from edited labels, changed +configuration, a later branch tip, or a later pull-request description. + +If the retained artifact is missing, recovery MUST stop with an explicit error. +Restoring bytes that match the recorded fingerprint MAY permit resumption; +rebuilding under the same permanently reserved version MUST NOT. + +An unrecoverable built or externally visible intent MAY be explicitly retired. +Retirement MUST preserve its failure record and permanent reservation and MUST +NOT mark it complete. A corrective release requires separate approval and a new +version. + +#### FR7 scenarios + +```gherkin +Scenario: A partial publication resumes at the missing destination + Given a verified artifact reached one of two required destinations + When the release is retried + Then the completed publication is verified and reused + And the same artifact is published only to the unfinished destination + +Scenario: Missing retained bytes stop recovery + Given a release has a successful recorded build + And no retained or published copy matches its fingerprint + When recovery runs + Then recovery fails with a missing-artifact error + And no rebuild occurs under the reserved version +``` + +### FR8 — Every required destination completes before the release completes {#fr8} + +One release MAY publish to multiple destinations, but MUST use the same canonical +identity and notes at each. The release MUST remain incomplete until every +required artifact and release record is published successfully. Completed +destination work MUST be recorded and reused idempotently. + +Completion announcements MUST be separate from artifact publication. Delivery +progress MUST be retained per release and destination, and an unacknowledged +delivery MAY be retried with at-least-once semantics. Announcement failure MUST +NOT create another version, rebuild, republish the artifact, or revoke an +otherwise complete release. + +#### FR8 scenarios + +```gherkin +Scenario: Announcement retry does not create another release + Given version 4.2.0 is complete + And one announcement destination did not acknowledge delivery + When announcement delivery is retried + Then the same release identity and message context are reused + And no new version, build, or artifact publication occurs +``` + +### FR9 — Every destination satisfies the publishing-target contract {#fr9} + +Every publishing destination MUST document: + +- its native version syntax and canonical SemVer mapping; +- prerelease representation, validation, and ordering; +- immutability and the strongest consumer reference; +- withdrawal, unpublish, or yank behavior; +- supported moving-alias families; +- native version-constraint support; and +- the location of its durable release record. + +Distinct canonical releases MUST NOT map to one native coordinate. A mapping +collision or disagreement between the canonical prerelease status and a native +prerelease flag MUST fail rather than overwrite or misrepresent a release. + +#### FR9 scenarios + +```gherkin +Scenario: A native coordinate collision blocks publication + Given two canonical releases map to one destination version + When the second mapping is validated + Then publication fails before content is overwritten + And the original coordinate remains unchanged +``` + +### FR10 — The release record covers the complete included change {#fr10} + +Release notes MUST account for every change from the previous completed stable +source through the fixed release source, including skipped and direct changes. +They MUST preserve contributor-authored title, description, adoption guidance, +consumer-change evidence, and maintainer evidence under +[PR Format](../../Ways-of-Working/PR-Format.md), or equivalent reviewed aggregate +context for a manual release. + +The resolved version, version baseline, change baseline, fixed source, artifact +fingerprint, effective decision and source, destination coordinates, and note +snapshot provenance MUST be retained as a publication envelope without replacing +or rewriting the authored note. + +#### FR10 scenarios + +```gherkin +Scenario: A later release includes a skipped change + Given change A was merged with a skip decision + And approved change B triggers the next release + When release notes are frozen + Then the notes and aggregate decision cover A and B exactly once +``` + +### FR11 — Eligibility evaluates the complete unreleased artifact scope {#fr11} + +Only changes that affect the delivered artifact or a supported consumer contract +MUST produce a release. Eligibility MUST evaluate the complete unreleased range, +including direct and transitive build inputs, rather than only the latest change. +Validation MUST still run when publication is skipped. + +Without configured scope, every changed path MUST be treated conservatively as +release-affecting. A repository MAY narrow the scope explicitly. File categories +such as documentation or workflow definitions MUST NOT be universally excluded, +because they can be the product or an artifact input. + +#### FR11 scenarios + +```gherkin +Scenario: An out-of-scope change validates without publishing + Given the repository declares its artifact-affecting scope + And the full unreleased range contains no matching change + When release processing runs + Then validation runs + And no version or release is published +``` + +### FR12 — Release requests are serialized and accounted for in source order {#fr12} + +Release work on the same release line MUST NOT run concurrently and MUST queue +rather than cancel in-flight publication. Requests MUST be reconciled in source +history order, independently of worker start order. Duplicate event delivery +MUST return the same intent or outcome. + +A missed event, replaced worker, or bounded execution queue MUST NOT silently +discard a release request. Pending work MUST remain recoverable from durable +state or source history before later source revisions advance the line. + +After intervening changes, a built or externally visible intent MUST first be +resumed or explicitly retired. Remaining unbuilt and unexposed requests MAY be +backfilled individually or coalesced under a reviewed aggregate decision. Every +original request MUST record the release that accounts for it. + +#### FR12 scenarios + +```gherkin +Scenario: Later work cannot overtake an unresolved intent + Given changes A and B reached the stable line in that order + And A has a built incomplete release + When B is processed + Then A is resumed or explicitly retired first + And B cannot replace A's reserved version or artifact +``` + +### FR13 — Moving aliases are closed, optional, and withdrawal-aware {#fr13} + +The available moving-alias families MUST be exactly `latest`, major, and minor. +Each family MUST be enabled independently and MUST default to disabled. Unknown +families or unsupported target combinations MUST fail configuration validation. + +An enabled alias MUST resolve to the greatest eligible completed stable version +matching its family. A new older release MUST NOT move an alias backward. +Withdrawal MUST reselect the greatest remaining eligible match, which may be an +older version. If no eligible match remains, the alias MUST be removed or +disabled rather than left on an ineligible release. Prereleases MUST NOT move a +stable alias. + +#### FR13 scenarios + +```gherkin +Scenario: Withdrawal reselects an alias + Given the major alias points to completed stable version 3.4.0 + And completed stable version 3.3.2 remains eligible + When version 3.4.0 is explicitly withdrawn + Then the major alias is reconciled to 3.3.2 + And the withdrawn version remains reserved +``` + +### FR14 — Consumer update policy is explicit and trust-aware {#fr14} + +A consumer MAY select exactly one of these policies: + +| Policy | Accepted movement | +| --- | --- | +| `latest` | The newest eligible stable release, including a new major. | +| `lock-major-boundary` | Minor and patch releases within one selected major. | +| `lock-minor-boundary` | Patch releases within one selected major and minor. | +| `lock-specific-version` | No automatic movement from one exact version. | +| `lock-immutable-fingerprint` | No automatic movement from one exact content identity. | + +When no policy is selected, the consumer MUST use the most immutable reference +available. A boundary policy MUST be available only when the producer and target +publish the corresponding major or minor alias. Where the consumer syntax +accepts no version range, the producer alias carries that bound; a range-like +string MUST NOT be treated as a resolver expression. + +Mutable aliases MAY be used only within the consumer's permitted trust boundary. +An external producer, including one owned by another team in the same company, +MUST be pinned to an immutable fingerprint or exact immutable version. `latest` +MAY discover a release without an alias, then resolve it to the reference allowed +by the trust boundary. + +#### FR14 scenarios + +```gherkin +Scenario: An unsupported boundary policy fails explicitly + Given a consumer requests lock-minor-boundary + And the producer does not publish the minor alias family + When the policy is resolved + Then the request fails as unsupported + And it is not widened to latest or a major boundary + +Scenario: An external producer remains immutable + Given a consumer selects latest for a producer outside its trust boundary + When the current release is discovered + Then the durable consumer reference is its immutable fingerprint + And no moving producer alias is retained +``` + +### FR15 — Release-decision validation blocks an invalid merge {#fr15} + +Every pull request targeting a release line MUST receive a named release-decision +check required by branch policy. The check MUST re-evaluate when source, owned +release markers, or release settings change. A missing, conflicting, or invalid +decision MUST fail; a valid skip MUST pass with an explicit no-release result. +A failed, pending, absent, or stale required result MUST block manual and +automated merge. + +#### FR15 scenarios + +```gherkin +Scenario: Decision input changes invalidate the prior result + Given a pull request has a successful release-decision check + When its only owned decision is removed + Then the check is rerun + And merge remains blocked until the new inputs resolve validly +``` + +### FR16 — Withdrawal preserves history and requires explicit authorization {#fr16} + +A completed release MAY be withdrawn only through an explicit +maintainer-authorized operation. Withdrawal MUST be recorded separately from the +original completion and MUST preserve source history, approval evidence, +artifact identity, destination outcomes, announcements, and version reservation. +Ordinary deprecation or a temporary lookup failure MUST NOT imply withdrawal. + +Replaying a withdrawn release MUST report its withdrawn outcome. It MUST NOT +recreate removed records, resend the completion announcement, or make the +release current again. Correcting a bad release MUST roll forward with a newly +approved version. + +#### FR16 scenarios + +```gherkin +Scenario: Withdrawal does not erase completion + Given version 5.1.0 completed and was announced + When an authorized maintainer withdraws it + Then its completion and announcement history remain recorded + And replay reports the withdrawal without recreating or re-announcing it +``` + +### FR17 — Published-note corrections are auditable metadata changes {#fr17} + +A correction to published explanatory metadata MUST retain the original and +corrected content, reason, source evidence, actor, and time. It MUST NOT change +the artifact, exact version reference, fixed source, release decision, or +behavior attributed to that version. Unverifiable historical facts MUST be +recorded as evidence gaps rather than guessed. + +#### FR17 scenarios + +```gherkin +Scenario: A note correction cannot change release identity + Given a completed release has incorrect explanatory text + When an authorized correction is applied + Then the original and corrected text and evidence are retained + And the version, source, and artifact fingerprint remain unchanged +``` + +## Non-functional requirements + +### NFR1 — Published versions are immutable {#nfr1} + +One hundred percent of published stable versions MUST remain bound permanently +to their original source and artifact. A version identifier MUST NOT be reused +after failure, retirement, withdrawal, unpublish, or repository recreation. +Destination-native immutability controls MUST be enabled where available. + +#### NFR1 scenarios + +```gherkin +Scenario: A withdrawn version cannot be reused + Given stable version 2.0.0 was published and later withdrawn + When different content requests version 2.0.0 + Then publication is rejected + And the original reservation remains authoritative +``` + +### NFR2 — Recovery retains all unresolved release evidence {#nfr2} + +One hundred percent of unresolved built release intents MUST retain the artifact +fingerprint, frozen inputs, and phase progress required for safe resumption until +they complete or are explicitly retired. + +#### NFR2 scenarios + +```gherkin +Scenario: An unresolved release remains recoverable + Given a verified release is incomplete + When its worker and workflow run no longer exist + Then its durable record still identifies the artifact and remaining work +``` + +### NFR3 — Release behavior is shared and GitHub-native {#nfr3} + +One hundred percent of governed repositories MUST inherit one shared release +behavior, with zero repository-local copies of the resolution or lifecycle +algorithm. Contributors and maintainers MUST be able to drive decisions, manual +requests, retries, retirement, withdrawal, and evidence through GitHub pull +requests, owned labels, comments, checks, and workflow dispatch without a +separate local release tool. + +#### NFR3 scenarios + +```gherkin +Scenario: Equivalent repositories resolve equivalent requests consistently + Given two repositories inherit the same release capability and policy + When equivalent approved release requests are processed + Then they apply the same lifecycle, version, and recovery rules +``` + +### NFR4 — New destinations do not change the lifecycle contract {#nfr4} + +One hundred percent of new artifact types and destinations MUST be added through +the publishing-target contract without changing the requirements or release +lifecycle. + +#### NFR4 scenarios + +```gherkin +Scenario: A new destination uses the existing lifecycle + Given a destination satisfies the publishing-target contract + When it is added to a release + Then Resolve, Build, Verify, Publish, completion, and recovery retain their existing meaning +``` + +## Acceptance criteria + +```gherkin +Scenario: AC1 A reviewed merge completes one durable stable release + # Verifies: FR1, FR2, FR3, FR5, FR6, FR8, FR10 + Given the current stable release is 1.4.2 + And an approved in-scope pull request resolves to minor + When every required destination succeeds + Then exactly one completed release 1.5.0 is recorded + And its notes, fixed source, artifact fingerprint, and destination coordinates are durable + And current-version discovery returns 1.5.0 + +Scenario: AC2 An interrupted release resumes without identity drift + # Verifies: FR5, FR6, FR7, FR8, FR12 + Given a verified artifact is published to only one required destination + And later commits reach the stable line + When the original release is retried + Then it completes the missing destination with the recorded artifact + And its source, version, notes, and completed publication remain unchanged + +Scenario: AC3 Withdrawal changes eligibility without changing history + # Verifies: FR3, FR5, FR13, FR16, NFR1 + Given two completed stable releases and aliases pointing to the newer one + When the newer release is explicitly withdrawn + Then discovery and aliases select the older eligible release + And the withdrawn release's completion history and version reservation remain +``` ## Where this connects -- [Design](design.md) — how these requirements are delivered. -- [Publishing Targets](design-publishing-targets.md) — the contract each destination documents. -- [Documentation Model](../../Ways-of-Working/Documentation-Model.md) — why this spec holds only the why and the what. -- [Automation Labels](../../Ways-of-Working/Automation-Labels.md) — why release labels are owned by the `release:` namespace. -- [PR Format](../../Ways-of-Working/PR-Format.md) — the change-type labels that drive the bump. -- [Dependency Updates](../dependency-updates/spec.md) — update PRs are artifact-affecting and release through this capability. +- [Design](design.md) — how the intended lifecycle and current implementation + coverage realize these requirements. +- [Publishing Targets](design-publishing-targets.md) — the destination contract + and native behavior. +- [Automation Labels](../../Ways-of-Working/Automation-Labels.md) — the owned + release instruction vocabulary. +- [PR Format](../../Ways-of-Working/PR-Format.md) — the authored release note and + consumer evidence. +- [Dependencies](../../Coding-Standards/Dependencies.md) — dependency pinning and + update trade-offs. diff --git a/src/docs/Coding-Standards/GitHub-Actions.md b/src/docs/Coding-Standards/GitHub-Actions.md index dc8c22e..c3411f5 100644 --- a/src/docs/Coding-Standards/GitHub-Actions.md +++ b/src/docs/Coding-Standards/GitHub-Actions.md @@ -38,6 +38,12 @@ exception is a floating major tag on automation whose release path MSX controls: release creates the next major tag; it never repoints the existing major tag across the compatibility boundary. +A local clone that consumes an allowed owned major tag must explicitly accept +producer-controlled tag movement. Follow +[Accept moved release tags](../Capabilities/release-management/accept-moved-release-tags.md) +to repair a stale tag and configure trusted fetches. This does not relax the +immutable-SHA requirement for external actions. + ```yaml # External — immutable SHA; comment carries the readable version - name: Check out the repository diff --git a/src/docs/Ways-of-Working/Automation-Labels.md b/src/docs/Ways-of-Working/Automation-Labels.md index 83b792a..46a0880 100644 --- a/src/docs/Ways-of-Working/Automation-Labels.md +++ b/src/docs/Ways-of-Working/Automation-Labels.md @@ -54,6 +54,8 @@ namespaced, including the release set: | `release:minor` | Publish a minor release. | | `release:major` | Publish a major release. | | `release:pre-release` | Publish a prerelease from the open pull request. | +| `release:rc` | Publish the next release candidate for the resolved stable version. | +| `release:announce` | Deliver configured announcements after the release completes. | | `release:skip` | Validate the change without publishing a release. | These labels are read by @@ -62,9 +64,20 @@ One owned bump label records an explicit level and overrides the optional repository `DefaultBump`; for publishing decisions without a bump label, a valid configured default supplies it. `release:skip` records a no-release decision. `release:pre-release` is a mode used with a resolved explicit or configured bump, -never with `release:skip`. Release Management owns the +never with `release:skip`. `release:rc` is the dedicated release-candidate mode: +it uses the constant `rc` identifier with an increasing numeric counter and +conflicts with both `release:pre-release` and `release:skip`. +`release:announce` can accompany a stable or prerelease publication; it selects +configured post-completion delivery and does not authorize a version, build, or +publication by itself. + +Release Management owns the [resolver and required pre-merge validation](../Capabilities/release-management/design.md#version-computation); -an absent label and absent default are not an implicit patch decision. +an absent label and absent default are not an implicit patch decision. The +[implementation crosswalk](../Capabilities/release-management/design.md#intended-and-implemented-behavior) +identifies which labels the current shared baseline executes. An owned label +reserved by this contract but unsupported by the invoked workflow MUST fail +validation rather than be ignored or approximated. Reserving bare words would be weaker because it depends on a documented prohibition rather than making ownership visible in the label itself. diff --git a/src/zensical.toml b/src/zensical.toml index 6e19106..e48c103 100644 --- a/src/zensical.toml +++ b/src/zensical.toml @@ -124,6 +124,7 @@ nav = [ {"Spec" = "Capabilities/release-management/spec.md"}, {"Design" = "Capabilities/release-management/design.md"}, {"Publishing Targets" = "Capabilities/release-management/design-publishing-targets.md"}, + {"Accept Moved Release Tags" = "Capabilities/release-management/accept-moved-release-tags.md"}, ]}, {"Repository Governance" = [ "Capabilities/repository-governance/index.md",