Skip to main content
RunBook Academy

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

Tag protection and releases — repository settings, release notes, and immutability via signing

Intermediate⏱ ~19 mingitgpg

What you'll learn

  • Configure repository settings to prevent tag deletion and to require signed tags
  • Distinguish the GitHub Releases feature from a Git tag and explain what the release workflow adds
  • Describe the release-notes workflow that turns a signed tag into a published release
  • Identify the chain-of-trust properties that tag protection plus signing produces
  • Recognise the failure mode when a force-pushed tag bypasses the protection rules

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.

Tag protection is the operational half of the immutability contract. The on-disk contract is “tags do not move unless git tag -f is typed”; the repository-side contract is “the platform refuses to move or delete a tag without the right permission”. Without protection, the on-disk contract is enforced only by convention; with protection, it is enforced by the platform. For an infrastructure repository, the absence of tag protection is a deployment risk: an engineer with push access can rewrite a release tag, and the next consumer that pins to the tag name silently resolves to the wrong commit.

Repository settings for tag protection

Both GitHub and GitLab expose settings to restrict tag operations. The settings fall into three categories:

  1. Prevent deletion. A protected tag cannot be deleted without an explicit bypass. The bypass typically requires a maintainer role or a release-engineering team membership; a regular contributor with push access cannot delete the tag.
  2. Restrict creation. A protected tag can only be created by users or teams in an allowlist. The allowlist can be “release-engineers”, “the CI bot”, or “maintainers”. A regular contributor cannot create a v* tag.
  3. Require signing. When the setting is enabled, the platform refuses to accept a tag push unless the tag is signed by a verified key. An unsigned git push origin v1.0.0 is rejected at the platform layer before it reaches the repository.
flowchart LR
    P["tag protection rules"] --> D["prevent deletion"]
    P --> C["restrict creation\nto allowlist"]
    P --> S["require signing\nat platform layer"]

The combination of the three is what makes a tag a release identity. A tag that cannot be deleted, can only be created by the release team, and must be signed is a tag whose identity cannot be silently rewritten by a well-meaning engineer or a compromised account.

GitHub Releases versus Git tags

A Git tag is a ref in the repository. A GitHub Release is a publishing artefact built on top of a tag: it has a title, release notes (a markdown body), attached binaries, and a download URL. The release is not a Git concept; it is a GitHub concept, and the underlying tag is what makes it work.

# Create the tag (this is the Git operation)
git tag -s v1.0.0 -m "Release v1.0.0" "$COMMIT_OID"
git push origin v1.0.0

# Publish the release (this is the GitHub operation)
gh release create v1.0.0 \
  --title "Release v1.0.0" \
  --notes-file release-notes.md \
  ./dist/artifact-v1.0.0.tar.gz

The two operations are independent: the tag can exist without a release (a tag-only marker), and a release can exist without a tag (a release created from a branch tip, though this is discouraged). The production discipline is to publish releases only from signed tags, never from branch tips, because a release from a branch tip has the moving-ref problem that releases are supposed to avoid.

The release workflow adds three things a plain tag does not:

  • Release notes. A markdown body that documents the changes, the contributors, the breaking changes, and the upgrade instructions. The body is editable in the GitHub UI and is the canonical record downstream consumers read.
  • Attached binaries. Tarballs, container images, signatures, checksums. The release is the place to publish build artefacts; the tag alone has no upload capability.
  • A stable URL. A release has a permanent URL (/owner/repo/releases/tag/v1.0.0) that does not change. Downstream consumers can pin to the URL or to the tag name and get the same artefact.

The release-notes workflow

The canonical release workflow turns a signed tag into a published release in three steps:

# Step 1: Tag the commit (the Git operation)
git tag -s v1.0.0 -m "Release v1.0.0" "$COMMIT_OID"
git push origin v1.0.0

# Step 2: Generate the release notes from the changelog
RELEASE_NOTES=$(cat CHANGELOG.md | awk '/^## /{flag=1; next} /^## /{flag=0} flag')

# Step 3: Publish the release (the GitHub operation)
gh release create v1.0.0 \
  --title "Release v1.0.0" \
  --notes "$RELEASE_NOTES"

The release notes are the user-facing artefact; the tag message is the durable machine-facing artefact. The two should agree on the version and the date, but they serve different audiences: the tag message is read by git cat-file -p six months later by an audit script; the release notes are read by a downstream engineer deciding whether to upgrade.

Chain of trust from commit to release

Tag protection plus signing produces a chain of trust from the commit to the published release:

flowchart LR
    C["commit\nauthor: Engineer A"] --> T["tag -s\nsigner: Release Bot\nkey: <fingerprint>"]
    T --> R["release published\nnotes from CHANGELOG.md\nattached: artifact-v1.0.0.tar.gz"]
    R --> D["downstream consumer\nverifies tag signature\npins to tag name"]

The chain is four links:

  1. Commit author. The commit’s author field attests who wrote the code. This is the change attribution.
  2. Tag signer. The signed tag’s gpgsig block attests who tagged the commit as released. This is the release attribution. A different person from the commit author is expected: the release engineer signs the tag, the developer wrote the commit.
  3. Release notes. The release notes attest what the release contains. The notes are editable, but the first notes — the ones published at release time — are timestamped in the GitHub API and are the historical record.
  4. Attached artefacts. The artefacts attest what the consumer should run. The artefact’s hash (sha256) is recorded in the release; the consumer can verify the downloaded artefact against the recorded hash.

A break in any link is detectable: an unsigned tag fails git verify-tag; an unauthorised tag fails the allowlist; a deleted tag fails the protection rule; a force-pushed tag fails the protection rule and leaves a trace in the platform’s audit log.

Production discipline

  1. Protect the release tag namespace. A repository setting of “tag pattern v* is protected, requires maintainer role to delete” is the minimum. The pattern should match the team’s release convention.
  2. Require signed tags for protected tags. The platform-layer signing requirement is a stronger guarantee than the local git tag -s invocation — the platform refuses to accept an unsigned tag at all.
  3. Publish releases only from signed tags. A release from a branch tip has the moving-ref problem; a release from a signed tag has the immutability guarantee. The release workflow should accept a tag name as input, not a branch name.
  4. Treat the release notes as the durable record. The release notes are the canonical changelog for downstream consumers. They should be generated from a committed changelog file (not typed into the GitHub UI) so the source of truth is in the repository.

Cross-course references

  • Git, CI/CD & GitOps — Part XIX-04 (Signed tags) — the cryptographic half of the chain of trust; this lesson covers the platform half.
  • Git, CI/CD & GitOps — Part XIX-06 (Release workflows) — the tag-on-merge release pipeline that produces the signed tag in the first place.
  • GitHub for Production Sysadmins — Part IV (BranchProtection) — the branch-protection rules that apply the same discipline to branches; the tag rules are the tag-namespace counterpart.
  • Ansible for Production Sysadmins — Part XXXVIII (Review) — the role of signed release tags in the review workflow.

Quiz

Knowledge check · 4 questions

  1. Q1. What three categories of protection do GitHub and GitLab expose for tags?

  2. Q2. A GitHub Release can be published from a branch tip without an underlying Git tag.

  3. Q3. What is the difference between a Git tag and a GitHub Release, and what does the release workflow add?

  4. Q4. Diagnose a release whose tag was force-pushed after publication, and recommend the corrected platform configuration.

    A team published `v4.2.0` from a signed tag, attached the binaries, and announced the release. Two weeks later, a different engineer ran `git tag -f v4.2.0 $NEW_COMMIT && git push --force origin v4.2.0` to 'fix' what they thought was a typo in the tag. The force-push succeeded because the repository had no tag protection rules. The next downstream consumer that pulled v4.2.0 received the new commit and broke production.

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