Tags and Releases

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

A tag names a point in history so it can be found again without remembering a commit hash. This page covers what a tag is and how it differs from a branch, the command line, GitHub releases built on top of tags, and tagging from IntelliJ IDEA and VS Code. It reflects Git 2.55.x, GitHub CLI 2.101.x, IntelliJ IDEA 2026.2, the current stable release of VS Code with the GitHub Pull Requests and Issues extension, and github.com as of September 2026, with a default branch named main.

Concept

A branch is a movable pointer: each new commit advances it. A tag is the opposite — a fixed pointer to one specific commit that never moves on its own. Once v1.4.0 is created it always means the same commit, which is exactly the property a release number needs: two people who check out v1.4.0 months apart get identical code.

Git has two kinds of tags:

  • Lightweight — just a name pointing at a commit, stored the same way a branch ref is. No author, date or message.

  • Annotated — a full object in the Git database: tagger name and email, date, a message, and optionally a GPG signature. git show on an annotated tag prints that metadata before the commit it points to.

Annotated tags are the ones to use for anything that leaves a local clone — a release, a hotfix point, anything another person or a CI pipeline will refer to. Lightweight tags are fine for a private, throwaway bookmark.

Releases are conventionally numbered with semantic versioning — vMAJOR.MINOR.PATCH, for example v1.4.0 — where a major bump signals a breaking change, a minor bump adds functionality without breaking existing callers, and a patch bump is a backward-compatible fix. The leading v is a common convention, not a Git requirement.

%%{init: {'gitGraph': {'mainBranchName': 'main'}}}%% gitGraph commit id: "feat: parser" commit id: "fix: null check" tag: "v1.3.1" commit id: "feat: exporter" branch feature/reports checkout feature/reports commit id: "reports: draft" checkout main merge feature/reports commit id: "chore: changelog" tag: "v1.4.0"

Tags sit on main at the commits that were actually released; the commits in between are ordinary history. A tag does not have to be the tip of a branch — v1.3.1 above marks an older commit, tagged after main had already moved on, which is how a patch release of a previous line gets its own fixed marker.

Command line

List and inspect tags:

git tag                    # list every tag, alphabetically
git tag -l "v1.*"          # list tags matching a shell pattern
git show v1.4.0            # tag metadata (annotated) or the commit (lightweight), then the diff

Create a lightweight tag at HEAD:

git tag v1.4.0-rc1

Create an annotated tag at HEAD, the form to use for a real release:

git tag -a v1.4.0 -m "Release 1.4.0: exporter, changelog"

Tag an older commit instead of HEAD — useful for cutting a patch release on a line that has already moved on, as v1.3.1 does in the diagram above:

git log --oneline -5                 # find the commit to tag
git tag -a v1.3.1 <sha> -m "Release 1.3.1: null-check fix"

Delete a tag locally:

git tag -d v1.4.0-rc1

git push never sends tags by default — creating a tag is a purely local action, and it stays local until it is pushed explicitly. Push one tag, or every tag that exists locally and is missing on the remote:

git push origin v1.4.0        # push one tag
git push --tags                # push every local tag to its default remote

Deleting a tag locally does not remove it from the remote either; that needs its own push:

git push origin --delete v1.4.0

For a tag whose authenticity matters — a release artifact consumers build trust in — sign it with git tag -s instead of -a; it produces the same annotated object plus a GPG signature that git tag -v or git verify-tag can check later. Signing needs a GPG (or, with recent Git, SSH) key already configured; that setup is out of scope here.

On GitHub.com

GitHub releases are built on top of tags: a release adds a title, notes and downloadable assets to a tag, and can exist before or after the tag itself does.

  1. Open the repository on github.com and go to Releases, then click Draft a new release.

  2. Under Choose a tag, either pick an existing tag (such as one just pushed from the command line) or type a new version and click Create new tag: … on publish — GitHub then creates the tag when the release is published.

  3. When creating a new tag, use the Target dropdown to pick the branch or commit it should point to; this is ignored when an existing tag is selected, since that tag already has a commit.

  4. Give the release a title, then either write notes by hand or click Generate release notes to have GitHub build them from merged pull requests and contributors since the previous release, grouped by label.

  5. Drag files onto the assets box to attach build artifacts (binaries, checksums, an SBOM) to the release.

  6. Check Set as a pre-release for a release that is not production-ready (a release candidate, a beta); leave it unchecked for a normal release.

  7. Check or leave Set as the latest release: left alone, GitHub assigns the "latest" label by semantic-version order, but it can be pinned or unpinned by hand — useful when publishing a patch for an older line after a newer one already shipped.

  8. Click Publish release (or Save draft to keep working on it privately first).

The GitHub CLI does the same thing from a terminal, creating the tag if it does not exist yet:

gh release create v1.4.0 --generate-notes
gh release create v1.4.0 --target main --generate-notes ./dist/app-1.4.0.jar
gh release create v1.5.0-rc1 --prerelease --generate-notes

--generate-notes fills in the same auto-generated notes as the Generate release notes button; --target picks the branch or commit for a tag that does not exist yet; trailing file paths are uploaded as assets; --prerelease and --draft mirror the checkboxes above.

Protecting tags

A repository can restrict who is allowed to create, update or delete tags matching a pattern (for example v*) with a tag ruleset, the same mechanism GitHub uses for branch protection — see Creating rulesets for repositories in your organization. Rulesets replace the older, simpler tag protection rules.

Tags triggering GitHub Actions

A workflow can trigger specifically on tag pushes (on: push: tags: ["v*.."]), which is how a repository turns git push origin v1.4.0 into a build that publishes a versioned image or package. See Docker CI/CD with GitHub Actions for what happens next — computing image tags and labels from that same Git tag and pushing the resulting image.

In IntelliJ IDEA

  1. Open the Git tool window (Alt+9) and switch to the Log tab.

  2. Right-click the commit to tag and choose New Tag….

  3. Enter the tag name and click OK — this creates a lightweight tag. For an annotated tag, use Git | New Tag… from the main menu instead, pick the commit, and fill in the Message field.

  4. To push tags together with commits, open the Push dialog (Ctrl+Shift+K) and enable the Push Tags option before pushing — IntelliJ IDEA does not push tags as part of an ordinary push otherwise.

Existing tags are also listed and manageable from the Branches popup in the Git widget, alongside local and remote branches.

In VS Code

  1. Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P) and run Git: Create Tag; it prompts for the tag name and an optional message, and creates the tag at the current HEAD — an annotated tag (git tag -a) when a message is entered, a lightweight tag when it is left empty.

  2. Run Git: Push Tags to push every local tag to the remote — VS Code, like the command line, does not push tags automatically with an ordinary sync or push.

  3. Run Git: Delete Tag to remove a tag locally (it does not touch the remote copy); run Git: Delete Remote Tag to remove it from the remote as well.

The same commands are reachable from the Source Control view: open the view’s …​ menu (on the view title or the repository row) and look under Tags.

Common mistakes and tips

  • A tag pushed and then re-tagged silently confuses anyone who already fetched it — once a tag has been pushed, do not move it. If a tag was created against the wrong commit, delete it everywhere (git tag -d, then git push origin --delete) and recreate it, and say so if anyone else may already have it.

  • A lightweight tag created by mistake where an annotated one was wanted can simply be deleted and recreated with -a; nothing points at a tag the way commits point at each other, so there is no history to rewrite.

  • Checking out a tag puts the working tree in a detached HEAD, the same state Checkout, Switch and Restore describes for checking out any commit directly — branch from it (git switch -c fix/1.3.1 v1.3.1) before committing any new work.

  • git tag -l "v1.*" and similar filters are useful once a repository has accumulated dozens of release tags; plain git tag still lists everything.