Release Management — Design#
The behaviour in the spec 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) receivesdev. Mergingdev → maincomputes 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 isprerelease. The single stable branch (typicallymain) 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.
# .github/release.config.yml
release-branches:
- branch: main
release-type: stable
- branch: dev
release-type: prerelease
The pipeline#
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.
flowchart LR
resolve["Resolve<br/>version decided"] --> build["Build<br/>artifact created once"]
build --> test["Test<br/>same artifact validated"]
test --> publish["Publish<br/>same artifact released"]
| 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:
# .github/release.config.yml
DefaultBump: patch
| 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:
- Validate
DefaultBumpwhen 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. - Honor a valid
release:skipas the explicit no-release decision and stop bump resolution. - 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. - 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 |
|---|---|---|
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.0orv1.0.0). Pre-1.0.0breaking changes arerelease:minorper SemVer §4;release:majoris 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. 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, 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.
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-releasewith a resolved explicit or configured bump publishesv<base>-<identifier>.<counter>:baseis the next version from that bump,identifieris the normalized branch name, andcounterauto-increments per push. - Artifact-specific conventions replace the SemVer suffix where they exist
(
-alpha.Nfor npm,.devNfor 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, 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.
release-paths:
- ".github/workflows/reusable.yml" # public caller contract
- ".github/actions/**" # this workflow's local implementation
- "src/**"
Release notes#
The GitHub Release name is the resolved version. Its body preserves the release-bound PR title and complete description, using PR Format 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#
- 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.
- 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.
- 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.
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 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:
- 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.
- 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.
- 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.
- 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; editing notes never bypasses it.
Release output#
- A git tag
vX.Y.Zon the release-branch commit — always. - The published artifact where one lives outside git — a container image
(
<image>:<version>and@<digest>), a package in its registry. For Action, workflow, and module artifacts the tag itself is the artifact. - 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. 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 |
|---|---|---|
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.
vMAJORadvances 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
vMAJORtag. External automation and anything requiring byte-for-byte reproducibility pins to the immutable version, digest, or SHA (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:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
Serialisation is provided once by the reusable workflow so every repository inherits it; the mechanism is the GitHub Actions standard. 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.
Configuration surface#
| 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 |
Where this connects#
- Spec — the requirements this design delivers.
- Publishing Targets — the contract each destination documents.
- Downstream Release Propagation — consumes the release note and immutable reference.
- GitHub Actions — how the workflow itself is authored (SHA pins, least privilege, concurrency).
- Security — why consumers pin to immutable references.