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
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
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:
MAJORincrements on incompatible changes. A consumer upgrading from1.x.yto2.0.0should expect breaking changes.MINORincrements on backward-compatible new functionality. A consumer upgrading from1.2.xto1.3.0should expect new features and no breakage.PATCHincrements on backward-compatible bug fixes. A consumer upgrading from1.2.3to1.2.4should 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:
mainreceives new feature work and new minor releases (v1.2.0,v1.3.0,v2.0.0).release-1.1receives only bug-fix backports and patch releases (v1.1.1,v1.1.2,v1.1.3).release-1.0may exist for legacy consumers who cannot upgrade to1.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 <commit> 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:
- Tag. A signed annotated tag is created at the release commit.
- Resolve to commit. The CI pipeline resolves the
tag to its commit OID (
git rev-parse v1.1.0). - Resolve to artefact. The CI pipeline builds the artefact at the commit and records the content digest.
- 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. - 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
- 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.
- Verify the push after the tag push.
git ls-remoteaftergit pushcatches the green-CI-but-tag-missing failure mode. - 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.
- Pin production deployments by digest, not by tag name. The tag name is the human-facing trigger; the digest is the durable deployment identity.
- 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
Q1. What is the canonical trigger for a release in the tag-on-merge pattern?
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...`).
Q3. What are the three SemVer components, and what does each one promise the consumer?
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.