Skip to main content

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

RefRole
Feature branch or developDay-to-day development; subject to Development CI
masterIntegrated, code-clean history; may contain untagged commits and tagged release points
release/X.Y.ZDedicated release qualification line; always used for version bumps and release stabilization
Tag vX.Y.ZCanonical 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.

ProfileTypical triggerPurposeGating
Development CIPush or MR on a feature branch, develop, or release/X.Y.ZBuild, test, and quality gates required before integration or taggingProduces code_clean
Release CIPush of tag vX.Y.ZPackaging, verification, publishing, promotion, deploymentProduces released
Master CI (optional, light)Push to masterReporting, Pages, portal sync, audit, notificationsNon-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:

  1. Work happens on a feature branch or on develop (equivalent for CI).
  2. Development CI runs on that ref until green.
  3. If the branch is behind master, rebase or update onto master and rerun Development CI as needed.
  4. When Development CI is green, the commit is code_clean on that ref.
  5. The commit may be fast-forward merged to master without 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-merge master
  • Single-developer: feature branch or develop → Development CI → code_clean → ff-merge master

develop is optional. Nothing requires landing on develop before master.

Path 2 — Tagged release commits

Release validation on a dedicated release branch:

  1. Create release/X.Y.Z from the current tip of master (which must already be code_clean for the intended release base).
  2. 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.py where applicable.
  3. Run Development CI on release/X.Y.Z after each potentially application-affecting change. Release-branch fixes may invalidate earlier validation and must be treated as potentially application-affecting.
  4. When Development CI is green on the release branch, the release commit is code_clean.
  5. Create and push annotated tag vX.Y.Z on that commit. The commit becomes a release_candidate.
  6. Run Release CI on the tag until green. The commit becomes released.
  7. 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:

StateMeaning
unvalidatedNo successful Development CI result exists for the commit on the validating ref
code_cleanDevelopment CI has succeeded for the commit on the validating ref
release_candidateTag vX.Y.Z exists on the commit and Release CI is pending or in progress
releasedRelease 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

  1. An untagged commit may reach master only if it is at least code_clean on the validating ref used for promotion.
  2. A tagged commit may reach master only if it is released.
  3. A tagged commit on master carries a stronger guarantee than an untagged commit on master.
  4. Successful Development CI does not imply successful Release CI.
  5. Failure of Release CI means the release path is dirty, even when application code is clean.
  6. Changes on release/X.Y.Z may affect both release behavior and application behavior; they may invalidate prior Development CI results.
  7. If Release CI fails, master must not advance to that tagged commit.
  8. Release-path repairs happen on release/X.Y.Z, not by forcing a broken tagged commit onto master.
  9. Successful Master CI does not change commit state.
  10. A moved or recreated tag voids any prior release_candidate or released status 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 master only after the commit is released.

Tag moves (for example git tag -f and protected-tag force-push where policy allows):

  • void prior release_candidate / released status for the old tag name;
  • require fresh Development CI on release/X.Y.Z when the tagged commit changes;
  • require fresh Release CI on the new tag pointer;
  • forbid ff-merge to master until the new tag is released.

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:

StateTypical validating refExample jobs (a2c-vscode-extension)Example jobs (a2c-workflow)
code_cleanfeature branch, develop, release/X.Y.Zlint, typecheck, test:unit, package:fetch-cli, package:vsix, package:vsix-smokepre-commit, workflow-docs:contract, test:pytest, branch build:package
release_candidatetag vX.Y.Ztag pipeline startedtag pipeline started
releasedtag vX.Y.Zrelease:verify, release:promoterelease: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:

  1. Treat Development CI, Release CI, and optional Master CI as separate domains.
  2. Never infer that a commit is releasable solely because Development CI is green.
  3. Use release/X.Y.Z as the only release qualification branch.
  4. Treat develop like any feature branch for CI purposes.
  5. Allow ff-merge of untagged commits to master only after code_clean on the source ref.
  6. Allow ff-merge of tagged commits to master only after released.
  7. If Release CI fails, stop promotion and confine repairs to release/X.Y.Z.
  8. Treat changes on release/X.Y.Z as potentially affecting application behavior.
  9. Do not tag until Development CI on release/X.Y.Z is green and required release-candidate artifacts exist.
  10. Do not treat Master CI success as code_clean or released.
  11. 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.Z as canonical release markers
  • Root CHANGELOG.md with [Unreleased] curation during development and script-finalized release sections
  • scripts/bump_release.py for maintainer-confirmed version bumps
  • scripts/verify_release_version.py on 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.Z isolates release stabilization without overloading develop
  • master carries 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:

  1. Adopt release/X.Y.Z release branches forked from master
  2. Update .gitlab-ci.yml so Development CI runs on feature branches, develop, and release/*; Release CI runs on tags; Master CI is optional and light
  3. Update release scripts and maintainer docs to gate tagging on release/X.Y.Z Development CI and master promotion on released
  4. Document project job mappings in docs/release-pipeline.md

References

Supersedes

  • ADR-0004 — branch promotion order, implicit CI semantics, and treatment of develop / master as integration gates

Superseded by

  • (none)