← ResourcesDEVOPS Β· VERSIONING

Versioning and tagging strategy for releases

Semantic versioning, Git tags and automation so every version is identifiable and deployable without confusion.

STRALYA15 min readJuly 2026

Why structured versioning is essential in a release pipeline

As a cloud infrastructure reaches a certain level of maturity, manual deployments give way to automated pipelines. Without a clear versioning convention, however, the team quickly loses control: which version exactly is running in production? Why does the staging deployment not match the one in dev? Which artifact should be promoted to the next environment?

A structured release versioning scheme answers these questions by establishing conventions that the entire pipeline follows. Every build produces a predictable number or tag. Every environment knows what to deploy and from which artifact. Logs, alerts, and rollbacks become traceable because everything rests on consistent identifiers. For a scale-up or a mid-market company running on AWS with architectural technical debt, this structure is not a luxury: it is the foundation that lets platform engineers regain control of a system that has drifted, and accelerate deployments without multiplying incidents.

Semantic versioning: the major.minor.patch convention explained

Semantic versioning (SemVer) remains the most widely adopted convention for release versioning, particularly for distributed applications and microservices. The principle is simple but powerful: MAJOR.MINOR.PATCH (for example, 2.5.3).

The MAJOR version increases when an incompatible change affects the API or the contract exposed to clients. If you update your database in a way that breaks backward compatibility, or if you remove an endpoint, that is a major version change. The MINOR version increases when you add a new feature that is compatible with what already existed. A new optional parameter, a new endpoint, an internal optimization: that is a MINOR bump. The PATCH version increases for a bug fix that changes nothing about the interface or the overall functionality.

In practice, within a structured CI/CD pipeline, you automate these increments. A commit whose message contains the keyword [BREAKING] triggers a major bump. [FEAT] triggers a minor bump. [FIX] triggers a patch. Tools such as conventional commits, along with scripts in your pipeline, perform this detection and generate the version number automatically. This means no developer has to manually edit a version file: everything rests on the commit message convention, which you enforce with a linter.

Semantic versioning also gives consumers an implicit clarity. A user who sees your version 2.5.0 knows they can update to 2.5.3 without fear. If they need to move from 1.x to 2.0, they know they must review their code, configuration, or integrations. On AWS, which often involves dependencies on lambdas, layers, and different runtimes, this semantics saves time during deployment.

Git tags and the relationship between branches and versions in a release

Semantic versioning defines what to number, but Git tagging defines where and how. A Git tag is simply a fixed label that points to a specific commit in the history. It is the fundamental artifact that ties a logical version to an exact state of the source code.

In a well-structured pipeline, each semantic version corresponds to exactly one Git tag, named according to a convention: usually v1.2.3 or release-1.2.3 (the v prefix is conventional). This tag is created on the production branch or on a dedicated release branch (often main or release/*) at the moment the version is validated and ready to be built into an artifact.

Tagging brings several benefits. First, it is immutable: if you need to investigate a production problem with v2.3.1, you check out exactly that tag and get the source code as it was then, bit for bit. Second, it is automatable: your pipeline can trigger a build as soon as a tag is created or pushed. Third, it is readable in interfaces: GitHub, GitLab, and Bitbucket display tags as visual milestones in your release history.

A common practice is to combine branches and tags: you keep a main or develop branch, and a release/1.2.x branch for each major.minor version. When you decide to release from release/1.2.x, you create a v1.2.3 tag on the latest commit of that branch. If an urgent production fix is needed (hotfix), you work from the previous tag, validate the fix, then immediately create the v1.2.4 tag. This structure guarantees that every environment can ask: which tag am I on exactly? And the answer is unambiguous.

Tagging artifacts and metadata for traceability across multiple environments

Git tagging establishes the source code version, but in a multi-environment pipeline that deploys to AWS, you also create artifacts: Docker images, packages, binary files. These artifacts need labels too, and this is where release versioning takes on its full operational dimension.

Imagine a stage in your pipeline that builds a Docker image from the code corresponding to Git tag v1.2.3. This image must be tagged and stored in a registry (ECR on AWS, for example). The most readable convention is to tag the image with the same number: my-service:1.2.3 or my-service:v1.2.3. Some teams also add a my-service:latest alias, but this makes deployment ambiguous: which commit exactly is inside "latest"? It is better to avoid latest in production and stay explicit.

When you promote the artifact to a new environment (validation in staging, then deployment to production), you keep the same tag. This means everyone knows that production is running my-service:1.2.3, that it corresponds to the code of the commit pointed to by Git tag v1.2.3, and that this artifact has most likely passed all required validations (unit tests, integration tests, security scan) at build time.

A more advanced level of traceability consists of enriching the artifact metadata. Many teams use Docker labels or Kubernetes annotations to store metadata: the source commit hash, the pipeline build number, the date, the author. When you inspect a pod in production (or an ECS container on AWS), you can immediately trace where the artifact came from without digging through pipeline logs. This speeds up incident investigations.

Automating version incrementing and tag creation in the pipeline

Managing versions manually is instructive but it does not scale. A pipeline stage automates this incrementing. The typical steps are as follows.

First, the pipeline scans the commits since the last tagged version. A tool such as gitversion, semantic-release, or even custom scripts using git log examines the commit messages to determine which type of bump is needed. If all commits contain [FIX], it is a PATCH bump. If there is at least one [FEAT], it is a MINOR bump. If there is a [BREAKING], it is a MAJOR bump.

Next, the pipeline computes the next version number (for example: currently at v1.2.3, if it is a PATCH, the next will be v1.2.4). It creates the Git tag pointing to the current commit, then pushes that tag to the remote repository (GitHub, GitLab, etc.).

This tag creation can trigger a series of events: immediately after the tag push, a webhook can restart the pipeline to build the artifact and tag it with this version number. Some pipelines use a step that GitOps practitioners call a "release commit": writing the version number into a configuration file, making a commit with that number, then tagging that commit. Other systems compute the version each time, without storing it anywhere, deriving it solely from the Git tag.

The key is consistency: whatever the method, every stage of the pipeline must use the same versioning logic. If the build stage injects version v1.2.4 into the artifact, the deploy stage must read exactly the same version from the tag or from the configuration file, without recomputing it independently.

On AWS, this often means: a CodePipeline stage that uses CodeBuild to run a versioning script, which writes the version into an artifact file or into the environment variables passed to the following stages (ECR, ECS, etc.).

Validating and promoting artifacts between environments with release versioning

Once your artifacts are correctly tagged according to release versioning, promotion between environments becomes predictable and audited. The question "which artifact should be promoted from staging to production?" has a clear answer: the one carrying the tag my-service:v1.2.4.

In a structured multi-environment pipeline, each environment (dev, staging, prod) uses an explicit declaration of the version to deploy. This can be an environment.yaml file per environment, or a GitOps system where the repo contains a Kubernetes manifest for each environment specifying which image to deploy. When it is time to promote a version from staging to production, the team or the pipeline updates that manifest with the new image tag, then commits and pushes the change. The GitOps pipeline detects this push and automatically redeploys.

The advantage is that every promotion is a traceable commit in Git. An auditor can follow exactly when v1.2.4 transitioned from staging to production, who approved the change, and revert to the previous commit if a rollback is needed. If an incident occurs, you do not wonder "which code is running?": the answer is in the image tag and in the Git history.

This approach integrates naturally with cross-environment validations. In staging, you can run a suite of integration, load, or security tests. If all tests pass, a human approval or an automated criterion approves the promotion. The pipeline then pushes the artifact image tag to production, and the GitOps system or the deployment service (ECS, Kubernetes on EKS, etc.) redeploys with this precise versioned tag.

Managing pre-releases and development versions in release versioning

Strict semantic versioning covers stable versions (1.0.0, 1.1.0, etc.), but in a real pipeline you also need to number development versions, release candidates, and unvalidated builds. This is where pre-release suffixes come into play.

The SemVer format includes an optional suffix after the PATCH: for example, 1.2.3-alpha.1, 1.2.3-beta.2, or 1.2.3-rc.1 (release candidate). Each suffix indicates the maturation status: alpha means a very unstable exploratory version, beta means functionally complete but unvalidated, rc means ready to release if the validation tests pass. The .1, .2, and so on let you create several successive alpha or beta versions without changing the major.minor.patch number.

In a CI/CD pipeline, this translates as follows: every build from a development branch (main, develop, or a feature branch) generates a tag such as v1.2.3-alpha.5. This tag creates an artifact my-service:1.2.3-alpha.5. This version can be deployed to a dev or test environment, but not to production. When all tests pass and the team is ready for a release candidate, the pipeline creates v1.2.3-rc.1. After a final round of tests, it becomes v1.2.3 (a stable version, with no suffix). At that point, the version is ready to be pushed to production.

Managing pre-releases requires some discipline: your pipeline must be able to distinguish an "alpha" tag from an "rc" tag or a stable version, and apply different rules (for example, alpha artifacts can be quickly deleted from the registry to save space, while stable versions are archived). Many teams use separate repositories or distinct tags for candidates: a "snapshots" registry for pre-releases, a "releases" registry for stable versions.

Audit and traceability: tracing the origin of a production version

All this structured versioning work leads to one key goal: auditability. When a performance degradation or a critical bug appears in production, you must be able to answer immediately: which version is running? Which source code? Who approved this deployment? When?

Thanks to the versioning and tagging conventions described above, these answers are a command away. You query your deployment system (ECS, Kubernetes, CloudFormation on AWS, etc.) to find out which image is running: my-service:v1.2.4. Then you go into your Git repository and look for the tag v1.2.4. You reach the commit that tag points to and see exactly which files changed, who wrote the code, and which commit messages explain why. You consult the promotion history in your GitOps manifest or your deployment history: who approved this promotion from staging to production? When?

This complete audit is possible only if every layer (source code, artifacts, deployments) uses the same version numbers or clear cross-references. If v1.2.4 of the code generates a Docker image tagged differently, this traceability is lost. That is why release versioning conventions are not minor details: they are fundamental building blocks of a mature cloud infrastructure that lets teams operate with confidence.

For scale-ups and mid-market companies on AWS, where the internal team is often small, this traceability speeds up incident diagnostics. Instead of spending hours digging through logs or trying to reconstruct "what changed?", you have a clear chain from commit to commit, from build to build, from deployment to deployment. A partner like Stralya can help you put this structure in place within your existing pipeline, automating the versioning steps and ensuring the conventions are respected at every build and deployment stage.

Concrete example: versioning a multi-service application on AWS

To make this tangible, imagine a startup that runs three microservices (api-service, worker-service, frontend) on ECS (Elastic Container Service). Each lives in its own Git repository.

Each service follows semantic versioning. A developer commits a fix for a memory leak in worker-service, with the message "[FIX] memory leak in the archiving task". The pipeline detects this [FIX] and increments the version from v2.3.5 to v2.3.6. A Git tag v2.3.6 is created. A webhook triggers a CodeBuild stage that builds the Docker image and pushes it to ECR with the tag worker-service:v2.3.6.

In parallel, another developer adds a new endpoint to api-service, with the message "[FEAT] batch migration endpoint". The pipeline increments api-service from v3.1.2 to v3.2.0. The resulting image is api-service:v3.2.0.

Now the three services are not on the same version, and that is normal: each moves at its own pace. A docker-compose.yml file or a set of Kubernetes manifests (or a CloudFormation template) defines which versions to deploy in dev, staging, and prod. In staging, the config might specify api-service:v3.2.0 (new), worker-service:v2.3.5 (old), frontend:v1.4.3 (old). This config is a Git commit, so every change is auditable and traceable.

When QA has validated that api-service:v3.2.0 and worker-service:v2.3.6 work well together in staging, the config is updated for production, and the pipeline applies it. In production, you can then check exactly which image of which service is running, and trace back to the source commit and the deployment approval, all automatically.

AWS TEARDOWN Β· FREE

Get the AWS Teardown: where your bill really goes.

The guide listing the 12 cost areas that leak the most at scale-ups, and how to plug them. Free, by email, no obligation.