Stop Manual Versioning! Automate Your Version Strings with git describe

Git tutorial - IT technology blog
Git tutorial - IT technology blog

The Versioning Story: A Nightmare Every Release Season

Early in my career, I worked on a project where every release sparked panic in the team: “Hey everyone, what version is this build?”. Back then, our versioning process was completely manual: open the config file, manually update 1.0.1 to 1.0.2, and then start the build.

Mistakes were inevitable. Once, a colleague forgot to update the file but still tagged it in Git as v1.1.0. The result? The running application claimed one thing, while the Git tag said another. When a customer reported a bug on version v1.1.0, I spent hours debugging the code from that tag, only to realize the actual build was running code from… two weeks prior. Lesson learned: Detaching Git tags from version info in the code is a catastrophic mistake.

Why Manual Tagging Is Bound to Fail

The biggest issue with manual version strings is the desynchronization between the source code and reality. In a modern workflow, source code constantly moves through different states:

  • Tagged commits (Official Release).
  • Commits between two tags (In development, unstable).
  • Uncommitted local changes (Dirty state).

Using static numbers like 1.2.0 in a package.json file is risky. You won’t know exactly where that build stands in Git history without manual cross-referencing. Especially in DevOps environments, where builds happen automatically on Jenkins or GitHub Actions, we need a mechanism for the system to “read” the Git history and name versions logically.

From Manual to Automated: Finding a Practical Solution

Many teams choose to use the Commit Hash (e.g., 7a3b1c2) as the version. This is extremely accurate since every commit is unique. However, hashes aren’t human-friendly. Looking at a random string, you can’t tell if it’s new or old, or how far it is from the last release.

This is where git describe becomes your “secret weapon.” Instead of a lifeless number, this command combines the most recent tag, the number of additional commits, and the current commit’s hash. The result is a string that is both readable and perfectly accurate.

git describe Pro-Tip: Turning Commit History into a Version String

Give this command a try in your repository:

bash
git describe

If you are exactly on a tagged commit (e.g., v1.0.0), Git returns exactly v1.0.0. Things get interesting when you add new commits after that tag.

Decoding the git describe Structure

Suppose you get the result: v1.0.0-5-g7a3b1c2. Let’s break down what it means:

  • v1.0.0: The name of the most recent tag Git finds when tracing back through history.
  • 5: The number of new commits created since tag v1.0.0.
  • g7a3b1c2: The abbreviated hash of the current commit (the g stands for Git).

Looking at this, I know immediately: “This build is based on v1.0.0, with 5 additional changes, and the commit hash is 7a3b1c2.” Completely transparent!

Most Practical Options

For maximum effectiveness, I usually combine the following parameters:

bash
git describe --tags --always --dirty

A quick explanation:

  • --tags: By default, git describe only looks for “annotated tags.” This option enables scanning for “lightweight tags” as well.
  • --always: If the repository has no tags, the command returns the commit hash instead of an error. This prevents build scripts from crashing.
  • --dirty: My favorite option. If you have uncommitted changes, it appends a -dirty suffix. This acts as a safeguard, preventing you from deploying “untracked” code to staging or production.

Applying to Real-World CI/CD Workflows

In a previous Python project, I integrated this command into a Makefile for full automation. Instead of manual versioning, the script automatically pulled info from Git and injected it into the build. This saved the team 15-20 minutes of verification per release.

For example, creating an automated _version.py file is simple:

bash
# Get the version string from git
VERSION=$(git describe --tags --always --dirty)

# Write to the project's version file
echo "__version__ = '$VERSION'" > src/myapp/_version.py

When building Docker images, I use this same string for tagging:

bash
IMAGE_TAG=$(git describe --tags --always)
docker build -t myapp:$IMAGE_TAG .

Since implementing this, version mismatches have vanished. When a bug occurs, I just check the application version and git checkout that exact hash to reproduce the error immediately.

Tips from the Trenches

While powerful, to keep git describe working smoothly, remember to:

  1. Be Consistent with Tagging: Follow Semantic Versioning standards (v1.0.0, v1.1.0…). Without tags, this command just returns a hash, losing its primary value.
  2. Be Careful with Merges: When merging an old branch into the main branch, Git might pick a tag from the old branch if it has a more recent commit. Double-check your commit graph if the version looks off.
  3. Fetch Full History: Systems like GitHub Actions often perform a shallow clone (fetching only the last commit). You must configure fetch-depth: 0 to retrieve all tags; otherwise, the command will fail or behave incorrectly.

Implementing git describe takes only 15 minutes to set up but provides immense value. If your team is still manual versioning, give this a try today. Your DevOps engineers will definitely thank you!

Share: