Create your own
Lesson illustration

Using Git Tags and Semantic Versioning for Releases

Good to see you again. In the previous lesson, you connected your local repository to GitHub and practiced the distinction between fetch, pull, and push. Your service-check repository now has a shared history, which gives us a reliable place to mark a version that is ready to ship.

This lesson introduces Git tags: durable labels attached to particular commits. You will combine them with Semantic Versioning to make a release identifier meaningful, such as v0.1.0 or v1.2.3. By the end, you will select a releasable commit, create and verify an annotated tag, publish it to GitHub, and create a concise GitHub release record.


A release is a specific, reproducible source state

A commit already identifies an exact state of a repository, but a hash such as a41d8f3 is not a useful release name for humans or automation. A Git tag gives that commit a meaningful, memorable label:

v0.1.0

Unlike a branch name, which normally moves forward as you commit, a release tag is intended to remain fixed. If production is running v0.1.0, the tag should always identify exactly the source revision that was tested and approved for that release.

This traceability matters in operations. Suppose a health-check script begins returning false failures after deployment. A release tag lets the team answer concrete questions:

  • Which source revision was deployed?
  • What changed since the previous known-good release?
  • Can we rebuild or inspect the exact version running in production?
  • Which version should be used as the rollback target?

A release is therefore more than “the newest commit on main.” It is a deliberately chosen, validated commit plus an identifier and a record of what changed.

Before creating a tag, use a basic release gate:

  1. The intended change is committed and present on the intended branch.
  2. The working tree is clean.
  3. Required checks or manual tests have passed.
  4. You know the changes since the previous release.
  5. You have chosen the correct version number.
  6. You are ready for that source state to become immutable from a release-management perspective.

Git technically permits tags to be deleted or moved, but moving a tag after it has been published as a release undermines traceability. Treat published release tags as permanent. If a release contains a defect, create a corrected new version rather than retagging the old one.


Semantic Versioning gives the tag meaning

A release number should communicate more than sequence. Semantic Versioning, commonly abbreviated SemVer, uses the form:

For example, the semantic version in the Git tag v1.4.2 is 1.4.2; the leading v is a common Git-tag naming convention, not formally part of the semantic version itself.

The practical rule is concise:

Change since the previous releaseVersion changeExample
Backward-compatible bug fixIncrease PATCH1.4.2 to 1.4.3
New backward-compatible functionalityIncrease MINOR, reset patch1.4.2 to 1.5.0
Breaking change to a supported public interfaceIncrease MAJOR, reset minor and patch1.4.2 to 2.0.0

The key term is public interface. For a command-line operations tool, that might include its documented command name, options, output format, exit codes, environment variables, or configuration-file format. For an HTTP service, it includes the published endpoints, request fields, response formats, and behavior that clients rely on.

For example, imagine service-check eventually offers a command with a documented interface:

./service-check --url https://example.internal --timeout 5

These releases would normally be classified as follows:

  • Fixing a defect where --timeout was ignored: patch, perhaps 1.0.0 to 1.0.1.
  • Adding an optional --retries flag while preserving existing behavior: minor, perhaps 1.0.1 to 1.1.0.
  • Replacing --url with a mandatory positional argument and removing --url: major, perhaps 1.1.0 to 2.0.0.

A version number is useful only when the team applies those meanings consistently. A database patch that is backward compatible may be routine from a DBA perspective; in application delivery, the corresponding question is whether consumers can upgrade without changing their integration or operational procedure.

Semantic Versioning 2.0.0 | Semantic Versioning

Read the SemVer specification’s summary and its most practical rules. The goal is not to memorize the full formal specification, but to make version selection a defensible release decision rather than a guess.

Start with the “Summary” and “Introduction” sections. Read the three increment rules, then continue through the explanation of why a clear public API matters. In “Semantic Versioning Specification (SemVer)”, focus on the rules covering normal version structure, released-version immutability, the meaning of major version zero, and the reset of lower-order numbers after a minor or major increment. Pay special attention to released-version immutability. Finally, in the “FAQ”, read the entries “How should I deal with revisions in the 0.y.z initial development phase?”, “How do I know when to release 1.0.0?”, and “Is ‘v1.2.3’ a semantic version?” Use these to distinguish the semantic version from the Git tag name you will create.

Initial development and version zero

For a new project that does not yet provide a stable public interface, SemVer reserves the 0.y.z range for initial development. Its API may change without the compatibility promise associated with 1.0.0.

For this course project, use:

v0.1.0

as the first release tag. It communicates: “This is an identifiable early release, but not yet a stable compatibility contract.”

When the project matures, has documented behavior, and others may reasonably depend on it, v1.0.0 marks the first stable public contract. This does not mean the project must be large or feature-complete. It means you are prepared to manage compatibility deliberately.

Pre-releases: a brief practical note

SemVer also supports pre-release identifiers after a hyphen:

v1.0.0-alpha.1
v1.0.0-beta.2
v1.0.0-rc.1

A pre-release has lower precedence than its final version: 1.0.0-rc.1 comes before 1.0.0. In practice, rc commonly means release candidate: a version believed to be ready but awaiting final validation.

You do not need pre-release tags for today’s lab. The important habit is to reserve a normal release tag such as v1.0.0 for a source state you are willing to identify as the final release.


Tags: lightweight pointers versus release records

A Git tag names a particular commit. Git offers two primary forms.

Tag typeCreationContainsSuitable use
Lightweightgit tag v0.1.0Only a named reference to a commitTemporary landmarks or personal convenience
Annotatedgit tag -a v0.1.0 -m "..."Tagger identity, date, message, and a reference to the tagged commitShared releases

For release work, use annotated tags. They preserve release-specific metadata separate from the underlying commit message. Git can also cryptographically sign tags, but that is a later security enhancement; an annotated tag is not automatically a signed tag.

Tagging

Read the relevant part of the official Pro Git book to establish the distinction between lightweight and annotated tags, then see how tags are attached to historical commits and shared with a remote.

In “Creating Tags”, read the comparison of tag types. Then read the “Annotated Tags” and “Lightweight Tags” subsections and compare the output of git show for each type. Next, read “Tagging Later” to see how a tag can identify an earlier validated commit rather than the current HEAD. Finish with “Sharing Tags”, especially the tag publishing options. Notice that normal git push does not automatically publish a newly created tag.

Use these commands to inspect tags:

git tag
git tag -l "v0.*"
git show v0.1.0

git tag lists tag names. Use git show <tag> to verify what a release label actually identifies. For an annotated tag, the output begins with tag metadata and the annotation message, then shows the commit itself.

You can tag the current commit simply by omitting a commit reference:

git tag -a v0.1.0 -m "Release v0.1.0: initial service-check baseline"

You can also tag an earlier commit explicitly:

git tag -a v0.1.0 -m "Release v0.1.0: initial service-check baseline" a41d8f3

The second form is valuable if a later commit is unfinished, but an earlier commit was fully tested and is the true release candidate.


Lab: publish the first releasable version

This lab builds directly on the shared GitHub repository created in the previous lesson. Work in your primary clone, not service-check-collaborator.

1. Inspect the state you are considering for release

Start with a clean, synchronized main branch:

cd ~/devops-labs/service-check
git switch main
git status
git pull --ff-only

A clean working tree is essential: uncommitted files cannot be part of a reproducible tagged commit.

Inspect the recent history:

git log --oneline --decorate -n 10
git log --oneline --all --graph -n 15

If this repository has no existing release tags, the current tested main commit is the candidate for v0.1.0. If it already has tags, inspect what changed since the most recent version:

git describe --tags --always
git log --oneline v0.1.0..HEAD
git diff --stat v0.1.0..HEAD

Replace v0.1.0 with the actual prior release tag when appropriate. These commands are useful operational evidence: they show the exact set of commits and the high-level file impact between a known release and the candidate.

For this first baseline release, confirm the following before proceeding:

  • main is synchronized with origin/main.
  • The files represent a state you are willing to identify and rebuild later.
  • You have checked the project in the way currently available. This might be a manual run of the service check or a project-specific validation command.
  • git status is clean.

2. Create an annotated release tag

Create the tag on the current HEAD commit:

git tag -a v0.1.0 -m "Release v0.1.0: initial service-check baseline"

The message should be short but meaningful. The tag name establishes the version; the annotation records why this commit matters.

Confirm both the tag type and its target:

git tag -l "v0.*"
git show v0.1.0
git rev-parse v0.1.0^{}
git rev-parse HEAD

For an annotated tag, v0.1.0^{} dereferences the tag and prints the commit it ultimately identifies. At this moment, it should match HEAD.

You can also view the tagged point in context:

git log --oneline --decorate -n 8

Look for tag: v0.1.0 beside the intended commit.

3. Publish the tag explicitly

Tags are separate Git references. Pushing the branch does not necessarily publish a newly created tag, so push this release tag deliberately:

git push origin v0.1.0

Verify the remote now contains it:

git ls-remote --tags origin

In the collaborator clone, retrieve the published tag and inspect it:

cd ~/devops-labs/service-check-collaborator
git fetch --tags origin
git tag -l "v0.*"
git show v0.1.0

This confirms that a teammate can retrieve the same release marker and see the same source state.

For a routine release, prefer pushing the intended tag by name rather than using git push --tags. The latter publishes every unpublished local tag, including any experimental or accidental ones.


GitHub Releases add a human-facing release record

A Git tag identifies source history. A GitHub Release is a GitHub record associated with a tag that adds a title, release notes, and optionally downloadable build artifacts. It is useful for communication, but the tag remains the essential Git-level identity of the release.

GitHub’s release-creation form shows a selected release tag (`v3.4.5`), its target branch (`main`), a release title and description fields, and the option to generate release notes. A GitHub Release documents a tag; it does not replace the tagged commit as the release identity.

After pushing v0.1.0, open your repository on GitHub:

  1. Select Releases in the repository’s right-side area or through the repository navigation.

  2. Choose Draft a new release.

  3. In Choose a tag, select the existing v0.1.0 tag. Do not create a differently named tag in the form.

  4. Give it the title Service Check v0.1.0.

  5. Use Generate release notes if available, then review the result rather than publishing it blindly.

  6. Add a short, honest description such as:

    Initial tagged baseline for the service-check project.
    
    Includes:
    - Current repository structure and operations notes
    - A reproducible source revision for future changes
    
  7. Publish the release.

Generated notes are a starting point, not a substitute for judgment. In a real production release, notes should state the user-visible changes, known limitations, and any upgrade or rollback considerations. A breaking release should explain migration steps prominently.


Safe release habits

A few habits prevent common tag-related mistakes.

Do not “fix” a released tag by moving it

If you discover a defect in v0.1.0, make the corrective commit, validate it, and create v0.1.1 if the change is backward compatible:

git tag -a v0.1.1 -m "Release v0.1.1: correct health-check behavior"
git push origin v0.1.1

Do not delete and recreate v0.1.0 on a different commit after it has been shared. Someone may already have deployed, downloaded, or built from the original tag.

Tag the exact release candidate, not merely the latest commit

If HEAD has advanced beyond the tested source revision, first find the release commit:

git log --oneline --decorate -n 15

Then tag its abbreviated hash:

git tag -a v0.1.0 -m "Release v0.1.0: validated baseline" a41d8f3

Always check the result with git show v0.1.0.

Treat tag checkout as inspection, not ordinary development

You can inspect the exact files in a release:

git switch --detach v0.1.0

This places Git in a detached HEAD state. It is suitable for reproducing an issue or examining an old version, but not for normal development. Return to your primary branch afterward:

git switch main

If you need to create a bug fix based on an old release, create a branch from the tag instead:

git switch -c fix/v0.1.0-issue v0.1.0

That preserves a normal branch reference for your new commits.


Key takeaways

A Git tag is a stable label for a particular commit; an annotated tag adds author, timestamp, and a release message, making it the appropriate default for shared releases. A GitHub Release is the accompanying human-facing release record, built around a tag.

Semantic Versioning makes release tags informative:

  • PATCH for backward-compatible bug fixes.
  • MINOR for backward-compatible new functionality.
  • MAJOR for breaking public-interface changes.
  • 0.y.z for initial development before a stable public contract.

You created and published v0.1.0 as an annotated tag, verified that it points to the intended commit, and documented it as a GitHub Release. From now on, the release workflow is: validate a specific commit, choose the version based on the change, create an annotated tag, push that tag explicitly, and document the release.

Next, you will move from direct shared-branch collaboration to a lightweight branching workflow using GitHub pull requests.

Can't find a good explanation? Sign up and we'll make it for you

Sign up