Setting up automatic version tagging (SemVer) in CI/CD

Manual tags get confused, releases fail, and the team spends hours reconciling versions. We set up automatic SemVer tagging turnkey: from analyzing the current branching scheme to a process for the team. We reduce release preparation time and eliminate a class of errors caused by the human factor.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1365
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

Why manual tagging breaks releases: SemVer in practice

Manual version tagging looks safe as long as releases are infrequent. A person assigns the version, and major, minor, and patch are chosen by the loudness of the changes rather than by their composition. SemVer turns into a verbal agreement — and it is the first thing to break during an urgent release.

The most common release failure is a mismatch between the tag and the content. The build was made with a hotfix, the tag was placed on the previous revision, and deployment from that tag brings up different code.

A month later there is no way to restore the state: it is unclear which revision is actually serving users and what was in it.

Next come lost fixes. A patch was merged into the release branch, but the version number was not changed; in the next build it does not make it into the changeset, and the defect returns.

The opposite also happens: a fix went into production without being reflected in the version, and no one knows where it is already present and where it is not.

The list is rounded out by inconsistent versions between services.

The major was bumped in one module, the dependency expects the previous line, the version string is edited by hand in several places — the desynchronization surfaces at deployment, not at build time, when it is most expensive to investigate.

The cost of these failures is not the release itself, but the hours spent figuring out which code is running and how to roll back. We will look at how automatic version tagging based on the composition of changes closes the gap between the tag, the build, and the running code.

What automatic versioning delivers: a measurable effect in the team

Manual versioning rests on one engineer's memory: the number is chosen before release, the tag is placed manually, and the mismatch surfaces later for the consumer.

Automation moves this rule into the build — the number is derived from the change history according to SemVer, and the tag is placed before the artifact is published. The effect breaks down into four measurable areas.

Release preparation time shrinks from hours to minutes

The decision about major/minor/patch is made by the commit parsing rule, not by a person.

The engineer only needs to confirm the release: the number is calculated, the change list is assembled, the branch is tagged — the release stops being a separate task that requires advance preparation.

The share of erroneous tags drops almost to zero

Duplicates, a tag on the wrong branch, a forgotten increment — typical causes of manual errors that are discovered only after publication. The rule checks the history and rejects a conflicting number at the build stage: the incorrect version simply does not go further.

Tools like Semantic Release do this declaratively — the configuration is stored next to the code and reviewed like code.

Rollback and build reproducibility

The tag unambiguously points to the commit from which the artifact was produced, so returning to a previous release is choosing a known version rather than searching for “that particular build.” Reproducibility no longer depends on who ran the build and when.

Change traceability

A continuous chain is built from the version to the task in the tracker: comparing two tags shows exactly the changes that went into the release. Incident investigation starts with comparing versions, not with questioning the team.

SemVer: comparing manual, hybrid, and automatic tagging

A version in SemVer consists of major, minor, and patch plus a pre-release label: major is bumped for breaking changes, minor for backward-compatible functionality, patch for fixes.

Exactly how these numbers are assigned is decided before configuring the pipeline: the chosen mode determines whether the build is reproducible from the tag and how much time is spent investigating mismatches between the artifact and the version.

Tagging mode When it fits Limitations
Manual Infrequent releases and a public API where a major change is an agreement with consumers A person forgets to bump the number, tags are duplicated, the correspondence between build and tag is lost; pre-release tags live separately from the main branch
Hybrid versioning scheme Regular releases where major and minor remain a deliberate product decision A policy is needed: who bumps major and minor and when, otherwise auto-increment of the version for patch masks a breaking change
Automatic Continuous delivery, many short releases and repositories An error in the commit message produces an incorrect major; branch protection, pre-release tag rules, and a clear rollback are mandatory

For most products, the workable scheme is hybrid: major and minor are fixed by the release owner, while patch and the pre-release tag are placed automatically according to branch rules.

Then the tag always points to exactly the artifact that went into delivery, and pre-release builds do not enter the stable channel.

How we implement automatic tagging: stages of engineering work

Automatic tagging is not installing a plugin, but an agreement about how the team names releases.

While the rule lives in the release engineer's head, labels diverge: one marks a build with a number, another with a date, a third forgets to bump the minor version for new functionality.

A versioning audit shows where the history lost coherence — and only after that do we touch the pipeline on an individual basis.

  1. Versioning audit. We pull up the history of tags and releases and compare the labels with the contents of the builds. We look for releases that cannot be rolled back: it is unclear what was in the artifact and from which branch it was built.
  2. Versioning scheme. We agree on what we consider a breaking change, what is new functionality, and what is a fix. The semantic versioning scheme must be readable unambiguously both by a human and by the pipeline.
  3. Change message rules. We define the format by which the pipeline itself determines the type of change. Otherwise the tagging step will have to be confirmed manually.
  4. Pipeline setup. We add a step that reads the history, calculates the next number, and places an annotated tag on a specific commit. The label is issued once and is not reused.
  5. Checking tags against release history. We run the rule over past releases and see which labels would have resulted. This catches duplicate tags, skipped minor versions, and breaks in the version chain.
  6. Handing over the policy. We provide a document with the rules, protection of labels from overwriting, and brief team training. After that the release engineer places a tag with one command, not from memory.

Implementation artifacts: configurations, pipeline schemes, and policies

Everything that appears during the work stays in your repository next to the code, not in chat and not in someone's head.

The rules for numbering and releasing versions become part of the project: they are visible in the change history, can be adjusted through normal review, and do not depend on whether anyone on the team remembers the previous agreements.

  • Pipeline configuration — a description of the build and check stages, the step for automatically calculating the number and publishing the artifact with a SemVer tag.
  • Branching and tagging scheme — which branch produces a fix, which produces new functionality, and which produces a breaking change, and which number is assigned in each case.
  • History parsing rules — which commit messages are considered significant, how the change list between tags is assembled, and what to do with changes that revert previous ones.
  • Permission matrix — who is allowed to place a tag, who approves merges into the main branch, and who can change the numbering rules themselves.
  • Release policy — the procedure from merge to publication, the release window, the point of no return, and the sequence of actions when rolling back to a previous version.
  • Branch protection and mandatory checks — the conditions without which a tag is not placed: passed tests, review, no open comments.
  • Process documentation — instructions for the team: how to release an urgent fix, how to place a preliminary tag, and how to act when versions diverge between branches.

Case study: how we reduced the share of erroneous tags and release preparation time

What it was like before the changes

The team maintained eight services in a shared repository, and the release engineer assigned the version manually: reviewed the set of changes, decided whether it was a major or a patch, and placed the tag.

For several dozen releases per quarter, there were 4-6 erroneous tags — most often a patch that pulled back an incompatible contract change, and consumers hit a failure when updating.

Investigating each case took hours: the tag had to be revoked, the artifact rebuilt, and teams notified.

What was changed

Commit messages were brought to an agreement from which the type of change is unambiguously derived, and the history parsing rule lives directly in the pipeline and calculates the next number automatically.

A separate verification step compares the resulting version with the actual content of the changes and stops the pipeline if the patch contains incompatible changes — the tag is physically not created.

A service account places the tag after checks pass, and the changelog and artifact are assembled from the same set of commits, so no mismatch remains between them.

All of this lives in GitLab CI alongside the existing build stages and did not require changing the developers' usual workflow.

Result in numbers

Metric Before After
Share of erroneous tags 11-13% of releases zero over two quarters; disputed cases are caught by the check before the tag is created
Time from accepting a change to the tag 40-60 minutes waiting for the release engineer 3-4 minutes of an automatic step, independent of people's workload
Manual operations per release 12-14 two: release confirmation and consumer notification

Rollback also became predictable: the previous tag remains valid, and the history shows exactly which change formed it.

SemVer in a monorepo: how to separate service versions?

A single shared version counter for a monorepo breaks the meaning of SemVer: a change in one service bumps the major for twenty others, and consumers see a “breaking change” where nothing changed.

Therefore the number is calculated not per repository but per scope — the path of a specific service.

The tag gets a prefix with the service name, and the number is calculated from the commit history only inside its directory: if there have been no changes in that scope since the last tag, no release is made at all.

This way independent versioning relies on the rules of Semantic Versioning and does not require manual number selection.

The flip side is a shared release cycle: while services are versioned independently, the compatibility of their contracts is not recorded anywhere. It is covered by a separate layer — compatibility checks and a matrix of supported version pairs “consumer — provider.”

The choice is determined not by repository size but by coupling: if services can be deployed and rolled back separately, independent tags work; if contracts change synchronously, a shared version is more honest and cheaper to maintain.

Contested points of automatic versioning: common questions

Moving versioning into the build pipeline almost always runs into the same doubts. They concern not SemVer itself, but how the rule fits a specific repository with its label history and branches. Below are four cases that come up most often.

The repository already has tags that do not match SemVer. Should they be rewritten?

No — the label history is not rewritten, otherwise links to past releases break. We do an inventory, mark the last correct tag as the starting point, and after that versions are issued automatically. Old labels remain readable and do not interfere with numbering.

Parallel branches — how to avoid a version conflict?

The number is assigned relative to the last common ancestor of the branch, not a global counter. The maintenance branch runs its own patch line, the development branch its own minor line. The labels of the two lines do not intersect, so no conflict arises on merge.

What about rebuilding the same commit?

The version is deterministic: one commit — one version, no matter how many times the build is run. The label is placed only after checks pass. If a rebuild produced a new number, artifacts from the same commit would differ in content, and rollback would become ambiguous.

Can manual confirmation be kept for critical releases?

Yes, and this does not break the automation. Version calculation and publication are separate: the pipeline prepares the number and label, but publishes after the responsible person confirms. This removes the risk of a release going out due to an accidental merge.

Let's discuss your versioning scheme and come back with a plan

For automatic tagging to work not as a one-off script but as a stable rule, we need inputs about your repository: where your release boundary is, how labels are placed now, and what is considered a breaking change for consumers.

Without this, the numbering scheme falls apart on the first urgent fix. Below is what to send so that we can discuss the task concretely rather than in general terms.

  • Branching scheme and release point: which branch the product is built from, whether there are maintenance branches for previous lines, and how urgent fixes are formalized.
  • Current state of labels: how many have accumulated, whether manually placed ones remain, and whether they conflict with automatic ones.
  • Numbering rules: which digit increases for a fix, which for new functionality, and what you consider a breaking change.
  • Repository structure: one repository for all components or separate ones, whether each module needs its own numbering or a single one for the product.
  • Data source for the label: how the type of change reaches the pipeline — from commit messages, from merge requests, or via an explicit flag at launch.
  • Artifacts and changelog: where builds are published, whether a list of changes needs to be generated, and in what form it goes out.
  • Access and protection: who can place labels, whether bypassing protected branches is needed, and how to embed the check into existing rules.

At the first meeting we clarify the release scheme and agree on the next step. In response, you receive a staged implementation plan with rollback points and an estimate of the work.