Skip to main content
RunBook Academy

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

Intermediate⏱ ~18 mingit

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

Not yet marked complete on this device.

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 &lt;tag&gt; 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 &lt;tag&gt; to pull a single tag explicitly.

Production discipline

  1. Treat the tag push as an explicit step. git push is not a release step; git push origin $RELEASE_TAG is. A CI pipeline that runs git push and exits zero has not necessarily pushed the tag.
  2. Prefer git push origin &lt;tag&gt; over --tags for release pipelines. The release script knows which tag it intends to push; --tags is broader than that and pushes every local tag, including scratch tags.
  3. Use --follow-tags for a trunk-push release flow. When the release tag is on the default branch and the push is git push origin main, the --follow-tags flag adds the tag to the same push without a separate step.
  4. Use git fetch --tags for the first clone of a release-bearing repository. Subsequent fetches can be plain git 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 push does by default.
  • Git, CI/CD & GitOps — Part XIX-06 (Release workflows) — the git push --follow-tags pattern 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

  1. 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?

  2. Q2. `git push origin --follow-tags main` pushes every tag the local clone has, including lightweight personal bookmarks.

  3. Q3. What does `git fetch --tags origin` do that a plain `git fetch origin` does not?

  4. 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.