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.
Table of Contents
- 1. Why release tags are essential
- 2. Semantic versioning for Magento releases
- 3. Protected tags and branch governance in GitLab
- 4. Triggering the pipeline only on tags
- 5. Deriving the release ID from the tag
- 6. Creating a GitLab release object
- 7. Maintaining a CHANGELOG and deployment log
- 8. Versioning compared: chaotic vs. structured
- 9. Summary
- 10. Common mistakes with release tags
- 11. FAQ
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.