skip to content

Tags and Releases

Lightweight versus annotated tags, tagging a specific commit, remembering that tags are not pushed by default, and using semantic versioning to make a tag mean something. Tags are how you anchor a release you may need to reproduce months later.

on this pageshow

questions

4

Why is a version-control tag treated as a permanent name for one commit, while a branch keeps moving?

level: juniorimportance: must knowfreq 68%

answer

  1. Two names, only one is stable
  2. Ask what happens after the next commit
  3. One pointer advances, the other stays put
  4. Consumers rebuild from the published name

basics

~20 s

A tag names one exact commit in history and is meant never to move again; a branch is a pointer that advances to each new commit on its line. Re-pointing a released tag silently changes what consumers rebuild.

solid answer

~40 s

Both are readable names over the same history, and they differ in whether the name is *expected* to change. A **branch** is a moving pointer: every commit recorded on that line advances it, so the name always means *the newest work here*. A **tag** is fixed to one commit and, once published, is never re-pointed — it means *this exact content, forever*. That stability is what makes a tag usable as a release anchor: a rebuild, a rollback or an audit resolves the same name to the same source months later. Moving a published tag breaks pinned dependencies, caches and bug reports **silently**, because the name still resolves — it just resolves somewhere else. If a released name has to change, publish a new version instead of repointing the old one.

code

pseudocode · 10 lines
pseudocode
history (oldest -> newest):  c1 -- c2 -- c3 -- c4
                                         ^      ^
                                   tag "2.3.0"  branch pointer

after two more commits are recorded on the same line:

history (oldest -> newest):  c1 -- c2 -- c3 -- c4 -- c5 -- c6
                                         ^                  ^
                                   tag "2.3.0"        branch pointer
                                    (unmoved)           (advanced)

go deeper

for a junior

Be ready to say in one sentence what each name is for: a branch follows the newest work, a tag fixes one point in history. Interviewers ask this to check you have actually cut a release rather than only read about one.

for a middle

Explain the mechanics — what advances a branch pointer, what a tag resolves to, and what happens downstream when a published name is moved. Expect a follow-up asking how you would correct a release that was tagged from the wrong commit.

for a senior

Show production judgement: how you would detect that a published name has moved, what you tell consumers who cached the old content, and why publishing a new version beats repointing even when a deadline is pressing.

for a principal

Own the contract angle. Immutability of released names is a convention your organisation either enforces or quietly erodes, so be ready to say how you make it hold across many teams and what you do when a team has already broken it.

## Two names over the same history A version-control repository records work as a chain of commits, and each commit carries an identifier derived from its own content. That identifier is permanent but unreadable — nobody quotes a forty-character string in a release note. Names exist so people and build systems can refer to a point in history without it, and the repository keeps a small table mapping each readable name to a commit identifier. Two kinds of name live in that table. The difference between them is not what they store — both resolve to one commit — but **what is expected to happen to them over time**. - A **branch name** is expected to move. It marks the tip of a line of work, and every commit recorded on that line advances it. Its meaning is *the latest work on this line*, and that meaning is only useful because it changes. - A **tag** is expected to stand still. It marks one commit and, once published, is treated as a permanent name for that exact content. Its meaning is *this content, forever*. ## Why a branch has to move Work is continuous. If the name of a line of development never advanced, every new commit would need a new name and nothing could be said about "where we are". Moving is the branch's entire function: someone asking for the shared line wants whatever landed most recently, not a snapshot from three weeks ago. The consequence is that a branch name is a **poor release identity**. "What was on the shared line?" has no answer without also saying *when*, and the moment when it matters — an incident, an audit, a customer stuck on an old version — is exactly the moment nobody wrote the *when* down. ## Why a released tag must not move A release is a promise about content. Once a version number has been published, everything downstream treats it as a stable coordinate: - **Consumers pin to it.** A dependency declaration naming a version expects identical content on every resolution. - **Caches and mirrors store it.** A copy fetched last month is assumed to still be correct. - **Defect reports cite it.** "Reproduced on 2.3.0" only means something if 2.3.0 is one thing. - **Rollbacks and audits resolve it.** Recovering the exact source behind what is running depends on the name still pointing where it did. Re-pointing a published tag breaks all four **silently**. Nothing errors, because the name still resolves; it simply resolves to different source than it did yesterday. Two engineers can stand at the same version number, see different code, and lose a day to it. Machines that already cached the old content never notice at all, so the population splits: some hold the old release under that name, some hold the new one, and the version number stops identifying anything. ## The comparison in one table | | Branch name | Release tag | |---|---|---| | Expected to change | Yes, with every new commit | No, never after publication | | Answers the question | Where is this line now? | What exactly was released? | | Safe to pin a build to | No — the meaning drifts | Yes — that is its purpose | | Effect of moving it | Normal operation | Silent divergence downstream | | Typical lifetime | Until the line is finished | As long as the release is referenced | ## What to do instead of moving a tag 1. **Tagged the wrong commit and nothing has consumed it?** Correcting it immediately is survivable, but only while you are genuinely certain nothing has fetched it. That certainty evaporates the moment the name leaves your machine. 2. **Already published?** Publish a new version. The superseded release stays as a record of what happened, and the new number carries the fix. Withdrawing the bad version is a separate, announced act — not a quiet edit. 3. **Want a name that always means "newest release"?** Keep it, but treat it as deliberately mutable: document it as unsafe to build from, and never let it appear in a release manifest or a pinned dependency. ## The judgement behind the rule "Tags are immutable" is not a technical restriction — most systems will happily let you move one, and some will do it without a warning. It is a **social contract**, and its value is exactly proportional to how reliably everyone keeps it. A team that moves a published tag once has usually not caused a visible incident; what it has done is remove the ability of everyone downstream to trust *any* version number it publishes. That loss is far more expensive than the mistake being covered up, and it takes months to surface, because the failure mode is two people quietly disagreeing about what a version contains.

  • Someone force-moves an already published release tag onto a newer commit. What is the practical damage?
    One version number now means two different things depending on who fetched it and when. Anyone who cached the old content keeps it and sees no warning; anyone resolving fresh gets the new content. Pinned builds stop being reproducible, defect reports citing that version become ambiguous, and the divergence surfaces days later as two people disagreeing about code that is supposedly identical. The repair is to publish a new version and announce the mistake, not to move the name again.
  • A release was tagged from the wrong commit and consumers already have it. What now?
    Leave the bad name where it is and publish a corrected version on top. If the bad release is harmful, withdraw it explicitly and say so — an announced withdrawal is something consumers can act on, while a silently repointed name is something they cannot even detect. Keeping the mistake visible costs one version number; hiding it costs the credibility of every number you publish.
  • Is a name that follows the newest release ever legitimate?
    Yes, as a convenience for humans — a pointer meaning "the current stable release" is genuinely useful for documentation links and demos. It must be documented as deliberately mutable, and it must never be the identity recorded in a build manifest, a pinned dependency or an incident report. The rule is not "names never move"; it is that a name used as a release coordinate never moves.

A branch is the shelf label "current issue" on a magazine rack — it always points at whatever is newest. A tag is the issue number printed on one copy: it identifies that copy forever, even after ten more have been published.

saying these in an interview costs you the question

  • Says a tag and a branch are the same thing under different names
  • Thinks a tag advances as new commits land on the line
  • Force-moves a published release tag to fix a mistake
  • Assumes consumers will notice a changed name because it is only a pointer
  • Believes deleting and recreating a published release name is harmless
open as a page

In semantic versioning, what does each of the three number positions promise a consumer?

level: middleimportance: must knowfreq 74%

basics

~20 s

Semantic versioning reads a version as major.minor.patch: a major bump warns of an incompatible change, a minor bump adds capability without breaking callers, and a patch bump is a backwards-compatible fix. The number is a compatibility promise, not marketing.

open as a page

How do you make a shipped release traceable to the exact commit it was built from?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Build every release from a tagged commit in a pipeline, never from a working copy with uncommitted edits, and stamp the version and commit identifier into the built artefact so a running instance can report where it came from.

open as a page

Why can a release tag stored as its own object be signed, while a tag that is only a name pointing into history cannot?

level: middleimportance: nice to knowfreq 31%

basics

~20 s

A signature must cover a fixed sequence of bytes. The object form stores real content (target, name, author, timestamp, message), so there is something to sign; a bare table entry has no content of its own.

open as a page