Skip to content

0010. Semantic Versioning and Tag-Driven Releases

Status

Accepted

Context

The image has only ever existed as a locally built dev-container:latest. CHANGELOG.md declares Semantic Versioning and Keep a Changelog, but every change has accumulated under [Unreleased]: no git tag, no released version, no published artifact. Consuming projects (see ADR 0006) reference dev-container:latest and rebuild by hand, so "which image am I running?" has no stable answer, and there is no way to roll back to a known-good image.

SemVer is defined for APIs. For a tool-aggregate container image the "API" has to be defined explicitly, otherwise every version bump becomes a judgment call with no shared rule.

Decision

Adopt Semantic Versioning with annotated git tags vX.Y.Z, starting at v1.0.0. The version describes the container contract, defined as:

  • the set of tools on PATH and their major behaviors;
  • the entrypoint interface (entrypoint.sh setup|start), mount points, named volumes, environment variables, and ports documented in README.md;
  • the sudo allowlist and the egress/firewall model.

Bump rules:

  • MAJOR -- the contract breaks: a tool is removed or renamed, a mount path / volume / env var / port changes meaning, the entrypoint interface changes, or a base change requires consumer action.
  • MINOR -- additive: new tools, new services, new features. Also major version bumps of bundled tools (e.g. Terraform 1.x -> 2.x): visible to users, but the container contract itself is unchanged.
  • PATCH -- fixes and routine Renovate pin bumps of bundled tools.

Release mechanics (the checklist lives in AGENTS.md "Versioning and Releases"):

  • Releases are manual and changelog-gated: the maintainer moves [Unreleased] to a dated ## [X.Y.Z] section, lands it via PR, then pushes the tag. CI refuses a stable tag without a matching changelog section and publishes the section as the GitHub Release notes.
  • Docker tags on docker.io/felipecoelho90/dev-container: X.Y.Z is immutable; X.Y, X, and latest move with releases. Prereleases (vX.Y.Z-rc.N) push only their exact tag and a GitHub prerelease.
  • Fix forward, never retag: a bad release is followed by a new patch release, an existing version tag is never overwritten.
  • Consuming projects pin the major (felipecoelho90/dev-container:1) for stability with automatic minor/patch updates; pin X.Y.Z for full reproducibility.

Consequences

Easier:

  • Consumers get stable, rollback-able references instead of a moving latest; "which image?" is answerable.
  • The changelog gate forces release notes to exist before a release does.
  • Renovate's stream of tool bumps maps cleanly to patch releases.

Harder:

  • The maintainer must judge bumps against the contract definition and curate the changelog before tagging -- releases are deliberate, not automatic.
  • latest now means "latest release", not "whatever was built last"; local development keeps using the locally built dev-container:latest, which is a different thing by design.