Git, CI/CD & GitOpsXIX · Tags and ReleasesTags and Releases
Tag pushing and fetching — why git push does not push tags by default, and what --tags and --follow-tags do
What you'll learn
- State why a plain git push does not push tags and what changes when tags are explicit
- Distinguish git push --tags (all tags) from git push --follow-tags (annotated tags reachable from pushed commits)
- Explain what git fetch --tags pulls versus what a plain git fetch pulls
- Choose the correct push flag for a release tag in a CI script
- Identify the failure mode when a downstream consumer expects a tag that was never pushed
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
git push does not push tags. The rule is surprising
because it contradicts the intuition that a tag is “just a
ref like a branch”. A branch is pushed when its name is
matched by the push refspec; a tag is not, because tags are
deliberately excluded from the default refspec to prevent
accidental pushes of personal bookmarks. The flag set is
small (--tags, --follow-tags, and the explicit
<tag> argument), but each flag has different semantics,
and choosing the wrong one is a routine production
incident.
Why git push does not push tags
The default push refspec in Git is
refs/heads/*:refs/heads/*: branch refs only. Tags live
under refs/tags/, which is outside that refspec. A git push with no arguments pushes the local branches that have
upstream tracking configured, and nothing else.
# Create a tag locally
git tag -a v1.0.0 -m "Release v1.0.0" "$COMMIT_OID"
# Push branches — the tag is NOT pushed
git push
# Everything up-to-date
# (the tag v1.0.0 is local only)
# Confirm the remote tag is missing
git ls-remote --tags origin | grep v1.0.0
# (no output)
This is the production foot-gun: a CI job runs git push,
the exit code is zero, the job is marked green, and the
release tag is still local-only on the runner. The next
deploy job — or the next downstream consumer that pulls the
image — finds no v1.0.0 on the remote and either fails
with reference not found or falls back to the wrong tag.
flowchart LR
A["git push\nrefspec: refs/heads/*"] --> B["branches pushed"]
C["tags under refs/tags/\noutside default refspec"] --> D["NOT pushed"]
git push origin <tag> — one tag, explicit
The simplest correct invocation names the tag explicitly. It pushes exactly that tag and nothing else:
# Push a single tag to origin
git push origin v1.0.0
# To git@github.com:example/repo.git
# * [new tag] v1.0.0 -> v1.0.0
This is the right form for a release script: the tag name is a variable, the push is explicit, and there is no ambiguity about what is being propagated. The downside is that the script must remember to push; forgetting is the failure mode.
git push origin —tags — all tags
The --tags flag pushes every tag the local clone has that
the remote does not. It pushes lightweight and annotated
tags alike, signed or unsigned:
# Push all local tags to origin
git push origin --tags
# To git@github.com:example/repo.git
# * [new tag] v0.9.0 -> v0.9.0
# * [new tag] v1.0.0 -> v1.0.0
# * [new tag] v1.1.0 -> v1.1.0
--tags is the right form for the very first push of a
newly cloned repository (backfilling all tags), but it is
the wrong form for a normal release pipeline. The reason
is that --tags includes personal bookmarks, scratch tags,
and any lightweight tag that happens to exist on the
runner. Production discipline is to push only the release
tag, not the tag namespace as a whole.
git push origin —follow-tags — annotated tags reachable from pushed commits
--follow-tags is the middle ground. It pushes every
annotated tag that is reachable from a commit being pushed
by the same git push. Lightweight tags are excluded:
# Push the branch and the annotated tags reachable from it
git push origin --follow-tags main
# To git@github.com:example/repo.git
# * [new branch] main -> main
# * [new tag] v1.0.0 -> v1.0.0
The “reachable from” predicate is the key. If main
advances from v1.0.0-5-g8a3f9d2 to v1.0.0, the push of
main carries v1.0.0 along with it because v1.0.0 is
an ancestor of the new main tip. A personal lightweight
tag on a side branch is not reachable from main, so it
is not pushed.
flowchart LR
P["git push origin --follow-tags main"] --> B["push main ref"]
P --> A["push annotated tags\nreachable from new main"]
A --> X["v1.0.0 (annotated, ancestor of new main)"]
A --> Y["v1.0.0-light (lightweight, side branch) — NOT pushed"]
--follow-tags is the right form when the release tag is
on the trunk and the push is a regular git push origin main. The flag adds the tag to the push without requiring
a separate git push origin <tag> step.
What git fetch pulls
The default git fetch pulls every ref the remote
advertises for the branches it knows about. The relevant
question for tags is: does the default git fetch include
tags?
The answer is yes for the tags reachable from fetched
refs, and no for tags that point outside the fetched
history. The --tags flag changes the answer to yes for
every tag the remote has:
# Plain fetch — pulls refs the local refs/heads/* tracks
git fetch origin
# + refs/heads/main: 1 new commit
# (does not pull v1.0.0 if main is not its ancestor)
# Explicit fetch with tags — pulls every tag
git fetch --tags origin
# + refs/heads/main: 1 new commit
# + refs/tags/v1.0.0: 1 new tag
# (pulls v1.0.0 even if no local branch points at it)
The failure mode is the same as for push: a consumer that
expects a tag to be present after git fetch and finds it
absent because no local branch tracks the commit the tag
points at. The fix is git fetch --tags for the first
clone of a release-bearing repository, or git fetch origin <tag> to pull a single tag explicitly.
Production discipline
- Treat the tag push as an explicit step.
git pushis not a release step;git push origin $RELEASE_TAGis. A CI pipeline that runsgit pushand exits zero has not necessarily pushed the tag. - Prefer
git push origin <tag>over--tagsfor release pipelines. The release script knows which tag it intends to push;--tagsis broader than that and pushes every local tag, including scratch tags. - Use
--follow-tagsfor a trunk-push release flow. When the release tag is on the default branch and the push isgit push origin main, the--follow-tagsflag adds the tag to the same push without a separate step. - Use
git fetch --tagsfor the first clone of a release-bearing repository. Subsequent fetches can be plaingit fetch; the tags are already present.
Cross-course references
- Git, CI/CD & GitOps — Part XIX-02 (Creating and listing tags) — the local creation step that precedes the push.
- Git, CI/CD & GitOps — Part VIII (Branching) — the
branch refspec that defines what
git pushdoes by default. - Git, CI/CD & GitOps — Part XIX-06 (Release workflows)
— the
git push --follow-tagspattern inside a tag-on-merge release pipeline. - Docker for Production Sysadmins — Part VII (Tagging) — the parallel foot-gun: an image built in CI and never pushed to a registry is a tag local-only to the runner.
Quiz
Knowledge check · 4 questions
Q1. A CI pipeline creates `v1.0.0` with `git tag -a v1.0.0 -m "Release"`, then runs `git push` and exits zero. What is the state of the remote tag?
Q2. `git push origin --follow-tags main` pushes every tag the local clone has, including lightweight personal bookmarks.
Q3. What does `git fetch --tags origin` do that a plain `git fetch origin` does not?
Q4. Diagnose a release pipeline whose deploy step fails because the tag is missing on the remote, and recommend the corrected push step.
A release pipeline creates `v2.1.0` with `git tag -a v2.1.0 -m "Release v2.1.0" $COMMIT_OID`, then runs `git push`. The exit code is zero and the pipeline marks the release job green. The downstream deploy job runs `git fetch && git checkout v2.1.0` and fails with `error: pathspec 'v2.1.0' did not match any file(s) known to git`. The deploy engineer asks why the tag is missing despite the release being green.
Passing score: 75%. Answers are checked in this browser.