Release Tags, Versioning and a Traceable Deployment History
AI generated
CI/CD
.yml
GitLab · CI/CD · Magento · Deployment History
Release Tags & Versioning
building a traceable deployment history

Teams that never tag their deployments and never maintain version numbers lose track of what ran on which server and when. Release tags make every Magento deployment unique, traceable and instantly rollback capable in an emergency.

12 min read Git Tags · Semantic Versioning · CHANGELOG · GitLab Releases GitLab CI/CD · Magento 2

1. Why release tags are essential

In many Magento projects, deployments run directly from a branch, usually main or master. That sounds simple, but it carries a fundamental drawback: no deployment can be uniquely identified. If a bug shows up after the last release, there is no way to tell without tags which commit is actually running on the server. A deployment history without tags is a story without chapter headings, you can see what happened, but not what was considered a completed release, or when.

A release tag is an immutable pointer to a commit. Unlike a branch, it cannot be accidentally overwritten. Every tag represents a clearly defined state of the code that was built, tested and deployed. In GitLab, a pipeline can be configured to trigger exclusively on tags, branches get checked but are never deployed straight to production. That keeps review and deployment strictly separated.

Magento projects add one more twist: the deployment infrastructure keeps multiple release directories around. Which directory belongs to which state follows directly from the tag. releases/v1.4.2 is unambiguous, while releases/20260509-143012 requires digging through logs to find out what is actually behind that timestamp. Both approaches can be combined, a timestamp as the directory name and the tag as metadata inside the release.

2. Semantic versioning for Magento releases

The most proven scheme for release tags is Semantic Versioning (SemVer): vMAJOR.MINOR.PATCH. MAJOR gets bumped for incompatible database migrations or breaking changes. MINOR marks new, backward compatible features. PATCH flags bug fixes and minor corrections. This scheme is especially valuable for Magento projects because the version number instantly communicates what level of review effort is expected before deployment.

In practice, we recommend using v as a prefix and protecting tags in GitLab as Protected Tags. That prevents developers from accidentally, or intentionally, creating tags with the same name or deleting existing ones. On top of that, the .gitlab-ci.yml can restrict the deploy job to react only to tags matching the pattern /^v\d+\.\d+\.\d+$/, meaning only cleanly formatted SemVer tags, not test runs or hotfix tags in some other format.

# .gitlab-ci.yml, tag-driven deploy pipeline for Magento
stages:
  - build
  - test
  - package
  - deploy
  - verify
  - rollback

variables:
  GIT_STRATEGY: fetch
  COMPOSER_CACHE_DIR: .cache/composer
  NPM_CONFIG_CACHE: .cache/npm
  # Release ID derived from the git tag (e.g. v1.4.2)
  RELEASE_VERSION: $CI_COMMIT_TAG

# Only run the full deploy pipeline on semver tags
workflow:
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: always
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: always
    - when: never

3. Protected tags and branch governance in GitLab

The technical foundation for a clean deployment history is a clear governance configuration in the GitLab project. Branches get protected first: main and release/* accept no direct pushes, only merge requests. Tags are then configured as Protected Tags: only maintainers may create tags matching the pattern v*. That guarantees no developer can accidentally trigger a production deploy.

This separation creates a well defined approval process: code gets merged into main via merge request, then, after tests and reviews, a maintainer tags it with a SemVer tag. Only that tag triggers the production pipeline. Every step is traceable, every decision logged. The git log with tags effectively becomes the deployment record of the past weeks.

4. Triggering the pipeline only on tags

In .gitlab-ci.yml, the rules directive controls when a job runs. For a production deploy, the criterion is simple: only when a tag is set. On branches, only the build and test job runs, never the deploy job. This separation makes deployments explicit and prevents a push to main from unintentionally landing in production.

build:magento:
  stage: build
  image: php:8.4-cli
  script:
    - composer install --no-dev --prefer-dist --no-interaction
    - npm ci --prefix app/design/frontend/Mironsoft/default/web/tailwind
    - npm run build --prefix app/design/frontend/Mironsoft/default/web/tailwind
    - php bin/magento setup:di:compile
  artifacts:
    paths:
      - vendor/
      - generated/
      - pub/static/
    expire_in: 1 day
  # Build runs on branches and tags alike
  rules:
    - if: $CI_COMMIT_TAG
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

deploy:production:
  stage: deploy
  environment:
    name: production
    url: https://shop.example.com
  script:
    - ./scripts/deploy.sh
  # Deploy ONLY on semver tags, never on branches
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: manual
      allow_failure: false

5. Deriving the release ID from the tag

The deploy script receives the tag name via the environment variable $CI_COMMIT_TAG and uses it as part of the release directory name on the target server. That produces directories such as releases/v1.4.2, which instantly show which release lives there. Alternatively, a timestamp based path can be used with the tag stored as a symlink or metadata file, both approaches can be combined.

It is important that the tag name never needs to be manipulated or normalized before being used as a directory name. A cleanly defined tag format like v1.4.2 is perfectly suited as a path component. The deployment script checks whether the target directory already exists, and if so, aborts to avoid duplicate releases. This mechanism turns tags into a unique, immutable reference at the server level too.

# scripts/deploy.sh, deploy a tagged Magento release to the server
#!/usr/bin/env bash
set -euo pipefail

# Use the git tag as the release identifier
readonly RELEASE_VERSION="${CI_COMMIT_TAG:?CI_COMMIT_TAG is not set}"
readonly APP_PATH="${DEPLOY_PATH:?DEPLOY_PATH is not set}"
readonly RELEASE_PATH="${APP_PATH}/releases/${RELEASE_VERSION}"

# Abort if this release already exists on the server
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "test ! -d '${RELEASE_PATH}'" \
  || { echo "[ERROR] Release ${RELEASE_VERSION} already deployed"; exit 1; }

# Transfer the build artifact to the new release directory
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "mkdir -p '${RELEASE_PATH}'"
rsync -az --delete ./ "${DEPLOY_USER}@${DEPLOY_HOST}:${RELEASE_PATH}/"

# Link shared directories and switch the current symlink atomically
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash -s <<SSH
set -euo pipefail
ln -sfn "${APP_PATH}/shared/app/etc/env.php" "${RELEASE_PATH}/app/etc/env.php"
ln -sfn "${APP_PATH}/shared/pub/media" "${RELEASE_PATH}/pub/media"
ln -sfn "${RELEASE_PATH}" "${APP_PATH}/current"
echo "[OK] Switched current to ${RELEASE_VERSION}"
SSH

6. Creating a GitLab release object

GitLab has, for a while now, offered the concept of Releases as a dedicated object within a project. A release is more than a tag, it can contain release notes, links to artifacts, and a changelog. This information appears in the GitLab interface under "Deployments > Releases", forming an automatically maintained deployment history visible to the whole team.

Creating a GitLab release object happens inside the pipeline job using the release-cli tool, which GitLab ships as a Docker image. The job reads the changelog for the current version from a file, or generates it from the git commit messages since the last tag. The result is a complete, structured deployment history with no manual effort, every time a new SemVer tag gets pushed.

create:release:
  stage: package
  image: registry.gitlab.com/gitlab-org/release-cli:latest
  script:
    - echo "Creating GitLab Release for ${CI_COMMIT_TAG}"
  release:
    name: "Release ${CI_COMMIT_TAG}"
    tag_name: "${CI_COMMIT_TAG}"
    description: |
      ## Magento Release ${CI_COMMIT_TAG}

      Deployed: $(date -u +"%Y-%m-%d %H:%M UTC")
      Commit: ${CI_COMMIT_SHA}
      Pipeline: ${CI_PIPELINE_URL}
    assets:
      links:
        - name: "Deployment Log"
          url: "${CI_PIPELINE_URL}"
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/

7. Maintaining a CHANGELOG and deployment log

A deployment history does not come from tags alone, it also requires documented content per release. The minimum is a CHANGELOG.md file in the repository, maintained in the "Keep a Changelog" format. Every version section lists the key changes: new features, bug fixes, database migrations and breaking changes. This file gets linked from the GitLab release object and is therefore reachable directly from the GitLab interface.

In addition to the CHANGELOG, a server side deployment log is worth adding: a RELEASES.log file in the shared shared directory that every deployment script automatically appends a line to, with timestamp, version, commit SHA and the name of the triggering user. That way, even without access to GitLab, anyone can trace directly on the server what got deployed and when. This simple measure prevents the helpless "when was the last deployment and what was in it?" moment during an incident.

8. Versioning compared: chaotic vs. structured

The difference between a project with and without consistent release tags shows up most clearly during an incident. Without tags, you have to search the git log for the last commit running on the server, reconstruct the deployment time from logs, and manually decide which state to roll back to. With tags, the same task takes seconds: git tag --sort=-creatordate | head -5 shows the most recent releases, and a rollback simply means switching to an already existing release directory.

Aspect Without tags (chaotic) With SemVer tags (structured) Benefit
Release identification Commit SHA or timestamp v1.4.2, unambiguous, readable Instantly traceable
Triggering a production deploy Every push to main Only on protected tags Explicit approval
Determining the rollback target Searching logs, guessing releases/v1.4.1 already exists Instant rollback
Deployment history Nonexistent GitLab Releases + CHANGELOG Complete, navigable
Communication within the team "The last deployment" "v1.4.2 from May 9" Clear reference

9. Summary

Release tags are not a bureaucratic obligation, they are the foundation of any traceable deployment history. A SemVer tag is the only mechanism that irrevocably marks a commit as an approved release. In GitLab, this whole process can be fully automated: tags trigger pipelines, release objects get created, release directories on the server carry the tag name, and the CHANGELOG delivers the content overview for each version.

The investment in a consistent versioning strategy does not pay off during normal operation, it pays off exactly when something goes wrong. During an incident, having a clean deployment history in place determines whether a rollback takes minutes or hours. Running Magento deployments without tags means having no rollback plan at all, only the hope that everything goes fine.

Release tags and deployment history, the essentials at a glance

Tag format

SemVer tags v1.4.2 as Protected Tags in GitLab, only maintainers may create them. The pipeline reacts only to this format.

Pipeline control

Secure deploy jobs with rules: if: $CI_COMMIT_TAG. Branches build and test, but never deploy directly.

Release directories

Use the tag name as the directory name: releases/v1.4.2. Prevents duplicate deployments and makes rollback trivial.

Deployment history

GitLab release objects, CHANGELOG.md and a server side RELEASES.log, three layers for full traceability.

10. Common mistakes with release tags

The most common mistake is creating tags directly on a branch before tests and quality checks have passed. A Protected Tag alone is not enough, the process needs to be designed so tags are only set after a successful pipeline run on the release branch. Anyone who tags first and tests afterward has reversed the causality and risks deploying broken code.

A second mistake concerns granularity: some teams tag every commit, others only major releases. Both are suboptimal. The right balance comes from a clear convention, for example minor releases for all feature deployments, patch releases for bug fixes and hotfixes, and major releases for deployments that include database migrations or breaking changes. This convention needs to be documented and known so the whole team tags consistently and meaningfully.

11. FAQ: Release Tags, Versioning and Deployment History

1Do I have to use SemVer?
SemVer is recommended because it instantly communicates what kind of changes a release contains. A custom format works fine as long as it is consistent and set up as a Protected Tag.
2How do I prevent tags without passing tests?
With a release branch workflow: merge first, wait for a green pipeline, then tag. The tag triggers the deploy job, the test job has already passed.
3Can I deploy the same tag twice?
The script checks whether the release directory exists and aborts on duplication. Every real release needs a new version number.
4Git tag vs. GitLab release?
Git tag equals a technical pointer to a commit. GitLab release equals a visible object with release notes, links and a changelog. Together they form a complete history.
5How long to keep old releases?
At least 3 to 5 releases, so a rollback can happen instantly during an incident. Older releases can be deleted automatically after a configurable retention period.
6Documenting database migrations in the history?
In the CHANGELOG as a breaking change with a major version increment, plus an explicit note in the GitLab release object. Critical for the rollback strategy.
7Tags for staging deployments?
With its own pattern, for example rc tags for release candidates. Same mechanism, different tag format, to clearly separate staging from production.
8Integrating the CHANGELOG into a GitLab release?
With release-cli in the pipeline job: read the matching section from CHANGELOG.md and pass it as the description parameter. Alternatively, read commit messages since the last tag.
9What happens if the deploy job fails?
The release directory stays incomplete, current still points to the previous release. No damage to the running system. The script needs to be idempotent.
10Is a RELEASES.log enough as a deployment history?
As a server side emergency record, yes. A complete history only emerges from the combination: git tags, GitLab releases, CHANGELOG and a server side log.