Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXIX · Tags and ReleasesTags and Releases

Release workflows — tag-on-merge, release branches, semantic versioning, release artefacts, and the deploy-after-tag pattern

Intermediate⏱ ~22 mingit

What you'll learn

  • Describe the tag-on-merge release workflow and why it is the canonical pattern
  • Apply semantic versioning rules to infrastructure releases and explain pre-release suffixes
  • Identify when a release branch is needed and how backports interact with the main release line
  • Define a release artefact as the unit of deployment and explain its relationship to the tag
  • Explain the deploy-after-tag pattern and the chain of trust from tag to production

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A release workflow is the pipeline from a commit to a production deployment. Every step has a tag in it: the release is a signed annotated tag, the artefact is built from the tag, the deployment pins to the tag, and the audit log records the tag. The five elements of the workflow are: tag-on-merge as the trigger, semantic versioning as the naming scheme, release branches for backports, the release artefact as the unit of deployment, and the deploy-after- tag pattern that pins production to the tag OID. Get any one wrong and the chain of trust breaks; get all five right and the release workflow is the audit’s best friend.

Tag-on-merge to the default branch

The canonical trigger for a release is a merge to the default branch (main). The CI pipeline listens for the merge, determines the next version, creates a signed annotated tag, and pushes the tag in the same job:

flowchart LR
    A["PR merged to main"] --> B["CI: determine next version\n(semver)"]
    B --> C["CI: git tag -s v$NEXT_VERSION -m Release $COMMIT_OID"]
    C --> D["CI: git push origin $NEXT_VERSION"]
    D --> E["CI: build artefact at tag"]
    E --> F["CI: publish release"]

The pattern’s appeal is that the tag is created in a known state (the merge commit on main) by a known actor (the CI runner with a known signing key). The tag is a consequence of the merge; the merge is the human-decided event, and the tag is the machine-recorded artefact.

# CI step: create the tag at the merge commit
NEXT_VERSION=$(./scripts/next-version.sh)
COMMIT_OID="$GITHUB_SHA"
git tag -u "$RELEASE_KEY_FINGERPRINT" \
  -m "Release $NEXT_VERSION — $(cat release-notes.md)" \
  "v$NEXT_VERSION" "$COMMIT_OID"
git push origin "v$NEXT_VERSION"

# CI step: verify the tag is on the remote
git ls-remote --tags origin "v$NEXT_VERSION" | grep -q "$COMMIT_OID" \
  || (echo "tag push failed" && exit 1)

The verification step (git ls-remote) catches the failure mode from XIX-03: a green CI job that did not actually push the tag. The pattern is “push, then verify the push”.

Semantic versioning

The naming scheme for releases is semantic versioning (SemVer): MAJOR.MINOR.PATCH, with optional pre-release suffixes:

  • MAJOR increments on incompatible changes. A consumer upgrading from 1.x.y to 2.0.0 should expect breaking changes.
  • MINOR increments on backward-compatible new functionality. A consumer upgrading from 1.2.x to 1.3.0 should expect new features and no breakage.
  • PATCH increments on backward-compatible bug fixes. A consumer upgrading from 1.2.3 to 1.2.4 should expect bug fixes and no new features.

Pre-release suffixes (-rc1, -beta.2, -alpha.1) indicate a release candidate or pre-production build. The suffix sorts before the corresponding release (1.3.0-rc1 < 1.3.0), which is the right ordering for a CI system that needs to distinguish “this version exists in a pre-release form” from “this version is the final release”.

# Production releases
v1.0.0
v1.1.0
v2.0.0

# Pre-releases, sorted before the corresponding release
v1.1.0-rc1
v1.1.0-rc2
v1.1.0-beta.1

For infrastructure repositories, the SemVer discipline is the same as for application code: MAJOR for a breaking change to the public contract (a Terraform module’s input variables, an Ansible role’s interface, a Kubernetes operator’s CRD), MINOR for a new feature, PATCH for a bug fix. The breaking-change threshold is what makes SemVer a contract, not a convention: a consumer can trust that upgrading PATCH is safe and upgrading MAJOR requires reading the release notes.

Release branches for backports

Not every release is from the head of main. A release branch (release-1.1, release-2.0) is a long-lived branch that tracks a specific major or minor version and receives only bug-fix backports from main:

flowchart LR
    M["main"] --> R["release-1.1"]
    M --> B["backport PR\ncherry-pick from main"]
    B --> R
    R --> T["v1.1.4 tag\non release-1.1"]

The discipline:

  • main receives new feature work and new minor releases (v1.2.0, v1.3.0, v2.0.0).
  • release-1.1 receives only bug-fix backports and patch releases (v1.1.1, v1.1.2, v1.1.3).
  • release-1.0 may exist for legacy consumers who cannot upgrade to 1.1; it receives only security backports.

The release branch is the source of truth for the 1.1.x line. A consumer pinned to v1.1.x should expect the release-1.1 branch to be the canonical location for the next 1.1.x release; the tag is created on the release branch, not on main.

The backport operation is git cherry-pick &lt;commit&gt; from main to the release branch. The cherry-picked commit inherits the original commit hash; the release branch’s history shows the cherry-pick as a separate commit with a new hash. The discipline is to cherry-pick commits, not patch series; the granularity of the backport should match the granularity of the original change.

The release artefact as the unit of deployment

A release artefact is the binary or document produced by the build at the tag. For an infrastructure repository, the artefact might be:

  • A container image, tagged with the release version (registry.example.com/infra:v1.1.0).
  • A Terraform module tarball, uploaded to the Terraform module registry under the version.
  • A Helm chart tarball, uploaded to the chart repository under the version.
  • An Ansible role tarball, published to Ansible Galaxy under the version.
# CI step: build the artefact at the tag
git checkout "v$NEXT_VERSION"
docker build -t "registry.example.com/infra:v$NEXT_VERSION" .
docker push "registry.example.com/infra:v$NEXT_VERSION"

# Record the artefact's hash as the deployment identity
ARTEFACT_DIGEST=$(docker inspect --format '{index .RepoDigests 0}' \
  "registry.example.com/infra:v$NEXT_VERSION")
echo "$ARTEFACT_DIGEST" > artefact-digest.txt

The artefact’s content-addressed digest (sha256:abc123...) is the durable identity. The tag name is the human-friendly name; the digest is the machine-checkable identity. The release record stores both: the tag name for human readers, the digest for machine verification.

The deploy-after-tag pattern

The deploy-after-tag pattern pins production to the tag’s content digest, not to the tag name. The flow:

flowchart LR
    T["v1.1.0 signed tag"] --> D["resolve to commit OID"]
    D --> A["resolve to artefact digest"]
    A --> P["deploy production\npin: digest"]
    P --> M["monitor: tag unchanged,\ndigest unchanged"]

The steps:

  1. Tag. A signed annotated tag is created at the release commit.
  2. Resolve to commit. The CI pipeline resolves the tag to its commit OID (git rev-parse v1.1.0).
  3. Resolve to artefact. The CI pipeline builds the artefact at the commit and records the content digest.
  4. Deploy. The production deployment uses the digest, not the tag name. A Kubernetes deployment references image: registry.example.com/infra@sha256:abc123...; a Terraform module sources the version by digest; a Helm install pins to the digest.
  5. Monitor. The monitoring system alerts on any change to the tag OID (which would indicate a tag rewrite) or any change to the deployed digest (which would indicate an unexpected redeploy).

The pattern’s strength is that the audit trail has two independent identities: the tag (the release identity, human-facing) and the digest (the deployment identity, machine-facing). A mismatch between the two is detectable and is a security event.

Production discipline

  1. Tag-on-merge is the canonical release trigger. The tag is created in CI from the merge commit; the merge is the human-decided event, the tag is the machine-recorded artefact.
  2. Verify the push after the tag push. git ls-remote after git push catches the green-CI-but-tag-missing failure mode.
  3. Use SemVer for every release, with explicit pre-release suffixes for non-final builds. The naming scheme is a contract with downstream consumers; the contract is only as strong as the discipline.
  4. Pin production deployments by digest, not by tag name. The tag name is the human-facing trigger; the digest is the durable deployment identity.
  5. Monitor the deployed digest for unexpected changes. A change to the digest without a corresponding version bump is a security event.

Cross-course references

  • Git, CI/CD & GitOps — Part XIX-05 (Tag protection and releases) — the platform-layer protection rules that make the tag-on-merge workflow safe.
  • Git, CI/CD & GitOps — Part XIX-04 (Signed tags) — the cryptographic half of the chain of trust.
  • Git, CI/CD & GitOps — Part XX (CI/CD Pipelines) — the pipeline definitions that implement the tag-on-merge trigger and the deploy-after-tag pattern.
  • Docker for Production Sysadmins — Part VII (Tagging) — the parallel between Git tags and OCI image tags; the pin-by-digest discipline applies to both.
  • Terraform for Production Sysadmins — Part IX (State) — the relationship between Terraform module versions and Git release tags.

Quiz

Knowledge check · 4 questions

  1. Q1. What is the canonical trigger for a release in the tag-on-merge pattern?

  2. Q2. A deployment that pins by image tag (e.g. `image: registry.example.com/infra:v1.1.0`) is as durable as a deployment that pins by digest (e.g. `image: registry.example.com/infra@sha256:abc...`).

  3. Q3. What are the three SemVer components, and what does each one promise the consumer?

  4. Q4. Diagnose a deployment pipeline that pins by tag name and silently deploys a rewritten tag, and recommend the corrected pin-by-digest pattern.

    A team's Kubernetes deployment references `image: registry.example.com/infra:v1.1.0` (tag-pinned, not digest-pinned). The release `v1.1.0` is a signed tag on commit `8a3f9d2`. A later engineer force-pushes the tag to a different commit `4d2c8e0a` (a security fix that should have been `v1.1.1`). The next kubelet pull resolves v1.1.0 to the new commit's image and the deployment silently runs the new bytes. The audit asks: which image was running in production at 14:00 yesterday?

Passing score: 75%. Answers are checked in this browser.