Tags & Releases
Git tags and hosted releases give a chosen commit a version name while branch work continues. A tag is a Git ref; an annotated tag adds a tag object. A hosted release lives outside Git and is associated with a tag. This lesson's scoped registry also snapshots the tag's peeled commit when publication occurs.
Picture a warehouse. main is the locator on the cart still being loaded, so each new commit rolls it forward. A version tag is a paper label deliberately attached to one crate. Routine commits leave that label alone, although -f can peel it off and stick the same name on another crate. The lesson's release record writes down the crate ID used for this shipment.
Branches advance automatically; tags don't
A branch ref and a tag ref are both names whose files contain object IDs. The useful difference is narrower than "branches move, tags are immutable." When HEAD is attached to a branch, git commit advances that branch automatically. It does not advance any tag that happened to point at the old branch tip. A tag moves only when a command explicitly rewrites its ref.
A lightweight tag stores an object ID directly in refs/tags/<name>. When that target is a commit, peeling the tag reaches the commit immediately.
An annotated tag inserts a Git object between the ref and the commit. In this lesson, Git serializes a tag object with an object line, type commit, the tag name, a tagger identity, and a message. The ref stores the tag object's ID. Peeling v1.0.0^{} follows the ref to the tag object, then follows its object field to the commit. The object is content-addressed; the ref that names it is still writable.
A hosted release is not another Git object. Provider databases own release titles, assets, draft flags, and similar metadata. They do not all preserve provenance the way this lesson's registry does. GitHub's release API associates a release with tag_name; target_commitish matters when the tag must be created, not as a universal publication-time commit snapshot for an existing tag. GitLab's release API also centers the release on tag_name and uses ref when it needs to create the tag. Treat the stored peeled commit in this animation as a deliberate scoped model, not a promise made by every host.
Checking out a tag uses the same peel. git switch --detach v1.0.0 resolves the tag to a commit and writes that commit ID directly into HEAD. HEAD no longer names refs/heads/main. A commit made from there advances detached HEAD and leaves both the branch and tag refs alone.
Tags and releases step by step
The default trace runs five operations against one local repository. It uses allow-empty commits over a fixed empty tree so object content does not distract from the refs.
- The repository opens with symbolic
HEAD -> main, butmainis unborn. There are no commits, tags, or hosted releases. git commit --allow-empty -m "release-candidate"resolves the parent as none, writes root commit C1, then advancesrefs/heads/mainfrom unborn to C1. The object exists before the ref moves.git tag -a v1.0.0 -m "First stable"resolves HEAD to C1. Git writes an annotated tag object whoseobjectfield names C1, then writesrefs/tags/v1.0.0to the tag-object ID. Peeling the tag reaches C1.Publish hosted release from v1.0.0peels the tag through its annotated object to C1. The lesson's registry stores titleVizLearn 1.0.0, assetvizlearn-1.0.0.zip, and C1 as its source snapshot. No Git object or ref changes.git commit --allow-empty -m "next-work"writes C2 and advances onlyrefs/heads/main. The trace now pauses on the proof:main -> C2, while the tag and scoped release source remain at C1.git switch --detach v1.0.0peels the tag back to C1, then changes HEAD from symbolicmainto direct commit C1. The final state ismain -> C2,v1.0.0 -> annotated tag object -> C1,scoped release source -> C1, andHEAD -> C1 (detached).
The warehouse picture now earns its keep. The active cart rolled to C2. The version label stayed on the C1 crate. The shipment record still names C1. Inspecting that crate takes you off the moving cart, which is exactly what detached HEAD means.
A release name is not provenance
Suppose a release retained only the string v1.0.0. Someone later runs git tag -f -a v1.0.0 ... at C2. Today's lookup resolves the friendly name to C2, although the published ZIP may have been built from C1. The label looks official; the evidence disagrees.
The force-move scenario keeps both facts visible because this lesson's registry records a source snapshot: the release card says C1 while the rewritten tag peels to C2. That mismatch is a warning, not an automatic repair. Git permits the explicit ref rewrite, and a hosting provider's own data model decides what historical evidence remains available.
Audit the release, not the label
Start by comparing the live branch tip, the tag's peeled commit, and the tag object itself:
git rev-parse refs/heads/main
git rev-parse 'v1.0.0^{}'
git cat-file -t v1.0.0
git cat-file -p v1.0.0
For an annotated tag, git cat-file -t reports tag, and git cat-file -p shows the object it names. For a lightweight tag, the type is the target object's type. Compare the peeled commit with the build SHA recorded by CI. Then hash the artifact itself, for example with shasum -a 256 vizlearn-1.0.0.zip, and publish that digest beside the release. A tag name answers "what do we call this?" A commit ID and artifact digest answer "what did we build and ship?"
Edge cases
- Unborn HEAD: creating a tag before the first commit fails because there is no object for the ref to name.
- Duplicate tag name: creating an existing tag without
-fstops before a new annotated tag object or ref write occurs. - Missing tag: publication and detached checkout both fail cleanly; neither invents a ghost ref or release record.
- Duplicate hosted release: this scoped registry permits one release per tag name and keeps the first record unchanged.
- Detached commit: committing after a tag checkout advances HEAD directly.
main, the tag ref, and the scoped release source do not move. - Forced tag update:
-fcan retarget a tag. If a release was already published, compare its recorded source, the current peeled tag, and the artifact digest.
Common mistakes
- Saying "tags are immutable." Annotated tag objects are content-addressed; tag refs can be rewritten. The everyday guarantee is only that commits do not advance tags automatically.
- Assuming every hosting platform snapshots the peeled commit when a release is published. Check the provider API and your CI records instead.
- Treating a hosted release as a Git object. Commits, trees, blobs, and annotated tag objects live in Git; release pages and downloadable assets do not.
- Assuming checkout attaches HEAD to a tag ref. Git peels the tag to a commit and detaches HEAD.
- Publishing from current
mainwhen the release action names a tag. Peel the selected tag; the branch tip may already be newer. - Force-moving a published version name without comparing commit IDs and artifact digests. One friendly label can now describe two different builds.
A note on simplification
This deterministic model has one branch, allow-empty commits over one fixed tree, and annotated tags that point directly to commits. It omits signed tags, nested tag objects, tag transport, packed refs, ref locking, reflogs, hooks, CI execution, checksums and SBOM storage, authentication, draft and prerelease states, download counts, and provider-specific release behavior. Object IDs use Git's SHA-1 object format; repositories configured for SHA-256 use a different hash algorithm. The release registry's peeled-commit snapshot exists to expose provenance drift, not to imitate GitHub, GitLab, or any other host exactly.
Keep exploring
A few lessons that share the same technique or lead into the next idea.
Found a mistake or something unclear? Leave feedback.