Tagging Git releases for semantic versioning is less about Git and more about building a disciplined process for how your software evolves.
Let’s say you’re working on a web application. You’ve got a main branch where all your production-ready code lives. When you’re ready to ship a new version, you’ll typically create a tag. For semantic versioning, this tag needs to follow a specific pattern: MAJOR.MINOR.PATCH.
# Example: Creating a tag for a patch release
git tag v1.0.1
git push origin v1.0.1
This v1.0.1 tag signifies:
- 1 (MAJOR): A breaking change has been introduced. Consumers of your API or library will likely need to modify their code to adapt.
- 0 (MINOR): A new feature has been added in a backward-compatible manner. Existing consumers can upgrade without breaking their applications.
- 1 (PATCH): A backward-compatible bug fix has been implemented.
Consider a real-world scenario. Your team has been developing a REST API.
Initial State:
You’ve released version v1.2.0. This means you’ve made backward-compatible feature additions since v1.0.0, and v1.0.0 itself contained backward-compatible feature additions since some hypothetical v0.x.y (where 0.y.z typically indicates initial development, and breaking changes are expected).
Scenario 1: Bug Fix
A critical bug is found in the GET /users/{id} endpoint. It’s returning incorrect data for users with special characters in their names. This is a bug, and the fix doesn’t introduce new functionality or break existing usage.
- Action: You fix the bug on your
mainbranch, then create a patch release tag. - Command:
# Assuming your current HEAD is the fix git tag v1.2.1 git push origin v1.2.1 - What happens: Consumers of your API can now update to
v1.2.1and get the bug fix without any code changes on their end. ThePATCHnumber increments.
Scenario 2: New Feature
You’re adding a new optional query parameter include_metadata=true to the GET /users/{id} endpoint. This parameter, when present, returns additional user metadata. If it’s omitted, the response remains identical to v1.2.1.
- Action: You implement the feature on your
mainbranch and create a minor release tag. - Command:
# Assuming your current HEAD is the new feature git tag v1.3.0 git push origin v1.3.0 - What happens: Consumers can upgrade to
v1.3.0. Their existing calls toGET /users/{id}will continue to work as before. They can optionally start using?include_metadata=trueto get the new functionality. TheMINORnumber increments, andPATCHresets to0.
Scenario 3: Breaking Change
You decide to deprecate the GET /users/{id} endpoint and replace it with GET /api/v2/users/{id}. The old endpoint will be removed in a future release. This is a breaking change because any client relying on the old endpoint will stop working.
- Action: You implement the new endpoint and mark the old one for removal, then create a major release tag.
- Command:
# Assuming your current HEAD is the breaking change git tag v2.0.0 git push origin v2.0.0 - What happens: Consumers are now alerted that they must update their integrations to use
/api/v2/users/{id}before they can safely upgrade tov2.0.0. TheMAJORnumber increments, and bothMINORandPATCHreset to0.
The core problem semantic versioning solves is communicating the impact of a new release to your users. Without it, a user might update from v1.2.0 to v1.3.0 expecting only bug fixes and suddenly find their application broken because a hidden breaking change was introduced.
The "system" here isn’t Git itself, but the convention built around Git tags. Tools like npm, Yarn, Composer, and even GitHub’s release features leverage these tags to manage dependencies and understand compatibility. For instance, if you declare a dependency on a package as ^1.2.1, it means "allow updates to any version from v1.2.1 up to, but not including, v2.0.0." This is only possible because the MAJOR.MINOR.PATCH tags clearly signal the nature of changes.
Here’s a crucial detail: the version number itself is just a string in Git. The meaning comes from the human process you follow to assign those strings. If you tag v2.0.0 but only made a bug fix, you’ve broken the semantic contract. The system relies on your team’s discipline to adhere to the rules.
The most common pitfall is not understanding what constitutes a "breaking change." If you change the expected output format of an API, add a required field, or rename a method, and clients must update their code to accommodate it, that’s a breaking change, and it warrants a major version bump.
If you’ve meticulously followed semantic versioning and your git log shows a clear progression of v1.0.0, v1.0.1, v1.1.0, v1.1.1, v2.0.0, and you’re now preparing to ship a backward-compatible feature, you’ll likely create a tag like v2.1.0.