ADR-0009: Release semantics and commit promotion to master
- Status: Accepted
- Date: 2026-06-16
- Deciders: Michel Gillet
Context
A2C-governed software repositories contain both application code and release-system code: packaging, tagging, deployment, publishing scripts, and CI configuration. Because the release system lives in the same repository, a commit may change the application, the release process, or both.
Although application and release concerns should be as decoupled as practical, they are not fully orthogonal. Versioning logic, packaging metadata, generated files, build configuration, dependency declarations, install layout, and release scripts can affect application behavior directly or indirectly. A fix made to unblock a release may accidentally break the application, even when the change appears confined to release logic.
ADR-0004 standardized bump scripts, changelog finalization, and tag verification, but its branch promotion sequence proved ambiguous for automation. AI-driven release steps treated “green CI” too broadly and did not distinguish normal development validation from release validation. Informal terms such as “ready”, “safe to merge”, and “green pipeline” are insufficient for machine agents, which require explicit states, transitions, and invariants.
Development may use short-lived feature branches or a long-lived develop branch. develop is not a special CI integration line. From CI semantics, develop is handled exactly like a feature branch. The only difference is reduced cognitive load for a single developer working alone. Multi-developer work uses feature branches; single-developer work may use feature branches or develop interchangeably.
This ADR supersedes the promotion and validation semantics of ADR-0004. It retains ADR-0004’s tooling expectations where still applicable: Semantic Versioning, Keep a Changelog, scripts/bump_release.py, scripts/verify_release_version.py, and mandatory check-changelog discipline (ADR-0002, ADR-0003).
Decision
A2C-governed software repositories must adopt the release semantics, state model, branch roles, and CI profiles defined here.
Branch roles
| Ref | Role |
|---|---|
Feature branch or develop | Day-to-day development; subject to Development CI |
master | Integrated, code-clean history; may contain untagged commits and tagged release points |
release/X.Y.Z | Dedicated release qualification line; always used for version bumps and release stabilization |
Tag vX.Y.Z | Canonical release marker; subject to Release CI |
Release branches are always named release/X.Y.Z. Release work must not use develop as the release line.
CI profiles
Three CI profiles exist. They are not interchangeable.
| Profile | Typical trigger | Purpose | Gating |
|---|---|---|---|
| Development CI | Push or MR on a feature branch, develop, or release/X.Y.Z | Build, test, and quality gates required before integration or tagging | Produces code_clean |
| Release CI | Push of tag vX.Y.Z | Packaging, verification, publishing, promotion, deployment | Produces released |
| Master CI (optional, light) | Push to master | Reporting, Pages, portal sync, audit, notifications | Non-gating — must not imply code_clean or released |
Successful Master CI does not upgrade commit state. It must never be used as a substitute for Development CI or Release CI.
Path 1 — Untagged development commits
Normal development validation:
- Work happens on a feature branch or on
develop(equivalent for CI). - Development CI runs on that ref until green.
- If the branch is behind
master, rebase or update ontomasterand rerun Development CI as needed. - When Development CI is green, the commit is
code_cleanon that ref. - The commit may be fast-forward merged to
masterwithout a tag.
A commit on master through this path is integrated and code-clean. It is not release-clean and is not automatically a release.
Integration patterns:
- Multi-developer: feature branch → Development CI →
code_clean→ ff-mergemaster - Single-developer: feature branch or
develop→ Development CI →code_clean→ ff-mergemaster
develop is optional. Nothing requires landing on develop before master.
Path 2 — Tagged release commits
Release validation on a dedicated release branch:
- Create
release/X.Y.Zfrom the current tip ofmaster(which must already becode_cleanfor the intended release base). - Apply release-specific commits on the release branch (version bump, changelog finalization, packaging metadata, bundled dependency pins, release-script fixes, and other stabilization work) using
scripts/bump_release.pywhere applicable. - Run Development CI on
release/X.Y.Zafter each potentially application-affecting change. Release-branch fixes may invalidate earlier validation and must be treated as potentially application-affecting. - When Development CI is green on the release branch, the release commit is
code_clean. - Create and push annotated tag
vX.Y.Zon that commit. The commit becomes arelease_candidate. - Run Release CI on the tag until green. The commit becomes
released. - Fast-forward merge the tagged release commit to
master.
A tagged commit on master is integrated, code-clean, and release-clean.
Pre-tag and post-tag release validation: Some release-candidate work (for example VSIX build and install smoke tests) runs during Development CI on release/X.Y.Z. Artifact promotion, version verification against tags, and publish/deploy steps run during Release CI on the tag. Agents must not push a tag until Development CI on the release branch has produced artifacts required by Release CI.
State model
Each commit is reasoned about with explicit states:
| State | Meaning |
|---|---|
unvalidated | No successful Development CI result exists for the commit on the validating ref |
code_clean | Development CI has succeeded for the commit on the validating ref |
release_candidate | Tag vX.Y.Z exists on the commit and Release CI is pending or in progress |
released | Release CI has succeeded for the exact commit, tag, and artifact set |
code_clean is ref-scoped: the same commit may be code_clean on a feature branch but not yet integrated on master.
The distinction between code_clean and released is fundamental. A commit may be acceptable for integration while still being unacceptable as a release point.
Invariants
- An untagged commit may reach
masteronly if it is at leastcode_cleanon the validating ref used for promotion. - A tagged commit may reach
masteronly if it isreleased. - A tagged commit on
mastercarries a stronger guarantee than an untagged commit onmaster. - Successful Development CI does not imply successful Release CI.
- Failure of Release CI means the release path is dirty, even when application code is clean.
- Changes on
release/X.Y.Zmay affect both release behavior and application behavior; they may invalidate prior Development CI results. - If Release CI fails,
mastermust not advance to that tagged commit. - Release-path repairs happen on
release/X.Y.Z, not by forcing a broken tagged commit ontomaster. - Successful Master CI does not change commit state.
- A moved or recreated tag voids any prior
release_candidateorreleasedstatus for the old tag pointer until validation is rerun.
Consequences for master
master is linear history, but commits on master do not all carry the same semantics:
- Untagged commit on
master: integrated and code_clean - Tagged commit on
master: integrated, code_clean, and released
master therefore acts as:
- a history of integrated development commits, and
- a history of fully validated release points
The stronger guarantee applies only to tagged commits.
Release failure and tag moves
When Release CI fails for release/X.Y.Z or tag vX.Y.Z:
- treat the failure as a release-path failure;
- keep repair activity on
release/X.Y.Z; - rerun Development CI after fixes that may affect application behavior;
- rerun Release CI after a new or moved tag;
- advance
masteronly after the commit isreleased.
Tag moves (for example git tag -f and protected-tag force-push where policy allows):
- void prior
release_candidate/releasedstatus for the old tag name; - require fresh Development CI on
release/X.Y.Zwhen the tagged commit changes; - require fresh Release CI on the new tag pointer;
- forbid ff-merge to
masteruntil the new tag isreleased.
Tag moves are maintainer-only operations and must follow repository protected-tag policy.
Reference sequences
Sequence A — Normal development (untagged integration to master):
Sequence B — Release (release/X.Y.Z to tagged master):
CI mapping (project-specific appendix)
Each project documents concrete jobs in docs/release-pipeline.md. Abstract states map to jobs as follows:
| State | Typical validating ref | Example jobs (a2c-vscode-extension) | Example jobs (a2c-workflow) |
|---|---|---|---|
code_clean | feature branch, develop, release/X.Y.Z | lint, typecheck, test:unit, package:fetch-cli, package:vsix, package:vsix-smoke | pre-commit, workflow-docs:contract, test:pytest, branch build:package |
release_candidate | tag vX.Y.Z | tag pipeline started | tag pipeline started |
released | tag vX.Y.Z | release:verify, release:promote | release:verify, package:build, binary package jobs, publish jobs |
Projects may add optional Master CI jobs on master that do not appear in this table because they are non-gating.
AI automation rules
Agents operating release or integration workflows must use explicit state and branch semantics:
- Treat Development CI, Release CI, and optional Master CI as separate domains.
- Never infer that a commit is releasable solely because Development CI is green.
- Use
release/X.Y.Zas the only release qualification branch. - Treat
developlike any feature branch for CI purposes. - Allow ff-merge of untagged commits to
masteronly aftercode_cleanon the source ref. - Allow ff-merge of tagged commits to
masteronly afterreleased. - If Release CI fails, stop promotion and confine repairs to
release/X.Y.Z. - Treat changes on
release/X.Y.Zas potentially affecting application behavior. - Do not tag until Development CI on
release/X.Y.Zis green and required release-candidate artifacts exist. - Do not treat Master CI success as
code_cleanorreleased. - When moving a tag, invalidate prior release state and rerun required validation.
Retained from ADR-0004
The following remain required unless a later ADR supersedes them:
- Annotated Git tags
vX.Y.Zas canonical release markers - Root
CHANGELOG.mdwith[Unreleased]curation during development and script-finalized release sections scripts/bump_release.pyfor maintainer-confirmed version bumpsscripts/verify_release_version.pyon tag pipelines- Script-driven releases rather than ad-hoc UI-only tagging
Consequences
Positive
- Explicit finite-state semantics for humans and AI agents
- Clear separation between integration (
code_clean) and release (released) - Dedicated
release/X.Y.Zisolates release stabilization without overloadingdevelop mastercarries dual, intentional meaning for untagged vs tagged commits- Release failures do not corrupt the meaning of tagged commits on
master
Negative
- Additional branch discipline (
release/X.Y.Z) and maintainer training - Existing CI, scripts, and docs that encoded ADR-0004 promotion order require migration
- Per-project job mapping must be kept current in
docs/release-pipeline.md - Tag-move policy must be enforced on protected tags
Migration
A2C-governed software repositories should:
- Adopt
release/X.Y.Zrelease branches forked frommaster - Update
.gitlab-ci.ymlso Development CI runs on feature branches,develop, andrelease/*; Release CI runs on tags; Master CI is optional and light - Update release scripts and maintainer docs to gate tagging on
release/X.Y.ZDevelopment CI andmasterpromotion onreleased - Document project job mappings in
docs/release-pipeline.md
References
- ADR-0010: TestPyPI and PyPI publishing gated by release state — specializes
releasedfora2c-workflowPython registry publication - ADR-0002: Standardized changelog and release documentation
- ADR-0003: Standardized A2C pre-commit hooks
- ADR-0004: Standardized CI bump and release flow (superseded by this ADR)
- docs/workflow/ci-release-flow.md
- docs/release-pipeline.md
- rules/13-ci-release-flow.md
Supersedes
- ADR-0004 — branch promotion order, implicit CI semantics, and treatment of
develop/masteras integration gates
Superseded by
- (none)