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
gstands 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 describeonly 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-dirtysuffix. 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:
- 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.
- 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.
- Fetch Full History: Systems like GitHub Actions often perform a shallow clone (fetching only the last commit). You must configure
fetch-depth: 0to 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!

