Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 042 / 100

Release tags and versioning: what developers should know

Mark releases clearly.

Expona Admin7 min read

Release tags and versioning: what developers should know

Marking releases clearly in your repository is one of those practices that separates production-grade projects from ad-hoc ones. When you're shipping code to customers or integrating with other teams, nobody wants to guess which commit corresponds to version 2.1.0 or wonder if a bugfix landed in the version they're running. Release tags and versioning give you a clear, auditable way to identify every release, automate deployments, and communicate what changed to both your team and your users.

Why tagging matters in professional workflows

Early in your career, you might ship by checking out a branch and deploying whatever's there. Once you're coordinating releases across teams, managing multiple environments, or running a service that other developers depend on, that approach breaks down fast. Tags create an immutable reference point in your Git history. They let CI/CD pipelines know exactly what to deploy. They let you roll back to known good states. They let customers and teammates see the complete history of what's been released.

Without consistent tagging, you end up with conversations like "which commit was 1.5.2?" or "did that fix ship in production?" Tags answer these questions automatically.

Annotated tags — git tag -a v1.2.0 -m "Release 1.2.0"

Git offers two types of tags: lightweight and annotated. A lightweight tag is just a pointer to a commit. An annotated tag is a full object in the Git database with metadata: a tagger name, timestamp, and message. For any release you care about tracking, use annotated tags.

Here's how to create one:

bash
git tag -a v1.2.0 -m "Release 1.2.0: performance improvements and bugfixes"

That command creates a tag pointing to the current commit with your message attached. If you want to tag a past commit, add its SHA:

bash
git tag -a v1.2.0 -m "Release 1.2.0" abc1234

To verify what you've created, view the tag with details:

bash
git show v1.2.0

You'll see the tag metadata, the committer info, the message, and the full commit details. This is valuable when you need to audit who tagged a release and when.

The naming convention v1.2.0 is standard. Some teams use just 1.2.0 without the v prefix, but the v makes it obvious at a glance that you're looking at a version identifier, not a commit hash or branch name. Consistency across your team is what matters most.

Semantic versioning — MAJOR.MINOR.PATCH communicates impact

Semantic versioning (semver) is a versioning scheme where each release number carries specific meaning. The format is MAJOR.MINOR.PATCH, and each segment tells the story of what changed.

  • MAJOR: Breaking changes. Any change that requires consumers of your API or library to update their code. Incrementing MAJOR signals "you need to adapt to this release."
  • MINOR: New features added in a backward-compatible way. Existing code keeps working, but new capabilities are available. Users can upgrade safely without code changes.
  • PATCH: Bugfixes and non-breaking improvements. No new features, no breaking changes. These are safe updates everyone should take.

In practice, semver saves your team constant explanations. If you release v2.0.0, every engineer knows there's something they need to review. If you release v1.5.2, they know it's routine maintenance. If you're integrating a third-party service and it jumps from 1.2.0 to 2.0.0, you immediately know to budget time for testing before upgrading.

Consider a real scenario: your payment processing library goes from v1.3.2 to v1.3.3 (patch). The team reviewing the changelog knows they can merge it into production without deep testing. Now imagine it goes to v1.4.0 (minor). There's new functionality to consider, but existing integrations remain compatible. Now imagine v2.0.0 (major). Every service using this library needs careful review before upgrading. That clarity is the whole point.

Semver also integrates seamlessly with dependency management. Package managers (npm, NuGet, cargo, etc.) understand these rules. You can declare that your service works with any patch version of a dependency (1.3.x) or any minor version (1.x), knowing you won't get broken by a major update you didn't opt into.

Push tags — git push origin v1.2.0

By default, git push doesn't send tags to your remote. Tags live only locally until you push them explicitly. This is intentional; it prevents accidental public release of tags you didn't mean to share.

After creating a tag locally, push it:

bash
git push origin v1.2.0

If you've created multiple tags and want to push them all at once:

bash
git push origin --tags

Be careful with --tags in large repositories. If someone created a lightweight tag for internal work, it goes public too. A safer approach for teams is to only push annotated tags, and to establish a convention: "only annotated tags mark releases."

Once tags are pushed, they're visible to your entire team and to any automation reading your repository. CI/CD pipelines watch for new tags and trigger deployments. Webhooks fire. Slack notifications go out. The tag becomes the official release marker.

If you need to delete a tag you pushed by mistake, use:

bash
git push origin --delete v1.2.0

And locally:

bash
git tag -d v1.2.0

GitHub Releases — release notes generated from merged PRs

GitHub Releases take tags one step further. A release is a GitHub concept built on top of a tag. It lets you attach release notes, prebuilt binaries, and other artifacts to that tagged commit. More importantly, it gives you a browsable, human-friendly way to see your release history.

To create a release on GitHub, navigate to your repository and go to the Releases section. Click "Create a new release," select or create a tag, write release notes, and publish. You can do this entirely in the GitHub UI, or use the CLI:

bash
gh release create v1.2.0 --title "Version 1.2.0" --notes "Bugfix for payment retry logic and performance improvements to query engine"

But here's where GitHub Releases shine: automatic release notes generation. Instead of manually writing "Fixed bug X, added feature Y," GitHub can generate release notes from your merged PRs. Go to the release creation page and click "Generate release notes." GitHub scans merged PRs since the last release, groups them by feature and bugfix, and creates a structured changelog automatically.

This works best when your team uses PR titles and labels consistently. A PR labeled "bug" with title "Fix null reference in payment handler" becomes a bullet point in release notes. A PR labeled "enhancement" with title "Add retry backoff to API client" becomes another. Customers and internal teams can read a proper changelog without developers manually compiling it.

Consider attaching build artifacts too. If your project builds a Docker image, a .NET NuGet package, or a binary, you can upload it as part of the release. Downstream teams have one authoritative place to find release artifacts, and the release is cryptographically linked to that specific commit.

A common workflow: your CI/CD pipeline detects a new tag, builds and tests the code, then uses the GitHub API to create a release with the build artifacts attached. By the time your team sees the release in GitHub, everything is built, tested, and ready to deploy.

Common mistakes

Tagging the wrong commit. You build and tag v1.2.0, then someone commits a hotfix on main. Now your tag points to an older state than main. This creates confusion about what "the release" actually contains. Always verify you're on the correct branch and commit before tagging. Use git log --oneline -5 to see recent commits and confirm you're at the right place.

Inconsistent versioning. One release is tagged v1.2.0, the next is 1.2.1, the next is release-1.3. Inconsistency makes automation harder and version comparisons error-prone. Pick a scheme and stick with it. Semantic versioning with a v prefix is standard; use it unless you have a specific reason not to.

Forgetting to push tags. You tag locally and deploy, but teammates can't see the tag. Weeks later, someone wonders what version is in production and can't find the tag in GitHub. Always push tags immediately after creating them. Make it part of your release checklist.

Using lightweight tags for releases. Lightweight tags have no metadata. When you come back six months later and see a tag, you have no idea who created it or why. Annotated tags solve this with timestamps and messages. Make annotated tags the standard for anything customer-facing.

Skipping release notes. A version number tells you what changed, but not why or what users should do. Release notes bridge that gap. Even a brief note explaining the main fix or feature helps. Use GitHub Releases or a CHANGELOG.md file to document what each version includes.

Key takeaways

  • Use annotated tags for all releases. They carry metadata and are the professional standard.
  • Follow semantic versioning (MAJOR.MINOR.PATCH) so version numbers communicate the scale of change.
  • Always push tags to your remote repository after creating them. Tags have no value if they're only local.
  • Use GitHub Releases to attach release notes, binaries, and other artifacts to your tags for a complete release history.
  • Generate release notes automatically from your merged PRs and labels when possible. It saves time and keeps notes consistent.
  • Establish a naming convention and stick to it across your team. Consistency makes automation and collaboration easier.
  • Link your CI/CD pipeline to tags so deployments trigger automatically when you tag a release.
---
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles