Stable releases from code freeze to deployment
Shipping releases straight from main or develop risks unstable versions and blocked feature work. Structured release branches separate stabilization from new development, semantic versioning makes changes traceable, signed tags create trustworthy shipping points, cherry-pick backporting safely brings bugfixes into older supported versions, and CI/CD pipelines react automatically and predictably to branches and tags.
Table of Contents
- 1. What release branches are and why stabilization matters
- 2. Creating a release branch: code freeze from develop
- 3. Semantic versioning: choosing MAJOR.MINOR.PATCH correctly
- 4. Tagging releases: annotated and signed Git tags
- 5. Backporting: bringing bugfixes back via cherry-pick
- 6. Maintaining multiple release branches in parallel
- 7. Coordinating release branches with CI/CD pipelines
- 8. Merge-back: release branch into main/develop
- 9. Release branches compared side by side
- 10. Summary
- 11. FAQ
1. What release branches are and why stabilization matters
A release branch is a dedicated Git branch that exists solely to stabilize an upcoming version, separate from ongoing feature development on main or develop. Once a release branch exists, only bugfixes, documentation and release preparation should land on it, no new features. That separates two concerns that would otherwise collide on a single branch: whether a version is stable enough for customers, and what gets built next.
Without that separation, a familiar pattern emerges: a team squeezes in one more feature right before the planned release date, a merge breaks the test suite, and the release slips by days. A release branch prevents that, because feature work keeps running undisturbed on main or develop while the release branch only ever shrinks, never grows. For Magento projects supporting several customer environments at once, this is especially valuable, because hotfixes can be built against a specific, frozen state instead of a constantly moving main branch.
2. Creating a release branch: code freeze from develop
The release branch is created at the code freeze date, the point at which the feature scope of the version is locked in. The common naming convention is release/2.5.0, branched off develop with git checkout -b release/2.5.0 develop. From that moment on, new features continue to be developed exclusively against develop, while the release branch receives only fixes that do not put the version's sign-off at risk.
Right after creation it is worth adding a version-bump commit that updates version numbers in composer.json or a VERSION file, so build artifacts are labeled correctly from the start. From here on, a four-eyes principle usually applies to merges into the release branch: every fix goes through review and automated tests before it enters the stabilization phase, because every change raises the risk to the upcoming release date.
#!/usr/bin/env bash
# Create a release branch at code freeze from develop
set -euo pipefail
git checkout develop
git pull origin develop
# Branch name convention: release/MAJOR.MINOR.PATCH
git checkout -b release/2.5.0 develop
# Bump version in project files
sed -i 's/"version": ".*"/"version": "2.5.0"/' composer.json
git add composer.json
git commit -m "chore(release): bump version to 2.5.0"
git push -u origin release/2.5.0
3. Semantic versioning: choosing MAJOR.MINOR.PATCH correctly
Semantic Versioning structures version numbers as MAJOR.MINOR.PATCH, where each position carries a clear meaning. MAJOR increases for incompatible changes that can break existing code. MINOR increases for new, backward-compatible functionality. PATCH increases for backward-compatible bugfixes with no new functionality. This convention tells consumers of a library or module immediately how risky an update is, without having to read the changelog.
The decision about which segment to bump should not be subjective, but driven by a fixed checklist: was a public method removed or its signature changed, that is MAJOR. Was a new feature added additively, that is MINOR. Was only a bug fixed, that is PATCH. Pre-release identifiers such as 2.5.0-rc.1 or 2.5.0-beta.2 mark intermediate states that are not yet considered stable, and build metadata such as 2.5.0+build.42 documents extra build information without affecting version precedence rules.
4. Tagging releases: annotated and signed Git tags
A Git tag marks an immutable point in history as a release. Annotated tags, created with git tag -a v2.5.0 -m "Release 2.5.0", store the tagger, date and message as their own object in the Git database, clearly distinguishing them from lightweight tags, which are only a pointer to a commit. Releases should always use annotated tags, because they appear correctly as full objects in git describe and release tooling.
Signed tags add another layer of trust: git tag -s v2.5.0 -m "Release 2.5.0" signs the tag with the releaser's GPG key, and git tag -v v2.5.0 later verifies the signature against the public key. For regulated environments or projects with many committers, a valid signature proves that a release genuinely originated from an authorized person and was not tampered with afterwards. Tags are pushed separately with git push origin v2.5.0, since git push does not include tags automatically by default.
# Create an annotated tag for the release
git checkout release/2.5.0
git tag -a v2.5.0 -m "Release 2.5.0: stabilization complete"
# Sign the tag with your GPG key (recommended for production releases)
git tag -s v2.5.0 -m "Release 2.5.0: stabilization complete"
# Verify a signed tag before deploying
git tag -v v2.5.0
# Tags are not pushed automatically, push explicitly
git push origin v2.5.0
# List all release tags in semver order
git tag --list "v*" --sort=-v:refname
5. Backporting: bringing bugfixes back via cherry-pick
Backporting means taking a bugfix that was first developed on main or develop and deliberately bringing it into an older, still-supported release branch. The standard tool for this is git cherry-pick, which applies a single commit as a standalone change onto another branch without pulling in the entire history in between. This is essential when a customer is still running version 2.4.x but the fix was originally written against the current development line.
In practice, git cherry-pick -x is the recommended flag, because it references the original commit ID in the new commit message and creates traceability across branches. Conflicts during a cherry-pick are normal when the code has changed structurally since the fix was written, and are resolved like a regular merge conflict before git cherry-pick --continue completes the operation. For multiple affected branches, a small script that applies the cherry-pick to each target branch in sequence, and clearly reports branches that fail, is worth setting up instead of failing silently.
# Backport a bugfix commit from develop to an older release branch
git checkout release/2.4.x
git pull origin release/2.4.x
# -x appends "(cherry picked from commit <sha>)" for traceability
git cherry-pick -x 8f3a1c2
# Resolve conflicts manually, then continue
git status
git add src/app/code/Mironsoft/Checkout/Model/QuoteValidator.php
git cherry-pick --continue
git push origin release/2.4.x
6. Maintaining multiple release branches in parallel
Larger projects often maintain several release branches in parallel, for example 2.4.x for existing customers and 2.5.x for the current version, each with its own support commitment. A clear support matrix that defines how long each line still receives security fixes prevents confusion about where a fix actually belongs. Without that matrix, fixes end up randomly in whichever branch a developer happens to have open, and older customers go unserved.
Keeping track is easier with a changelog entry per branch and a convention of labeling every backport commit with its target version. Some teams automate this with bots that, on a merged pull request against main, automatically open backport pull requests against all actively supported release branches, so cherry-picks do not have to be applied manually. A manual review step still matters, though, because an automatic cherry-pick against heavily diverged code can silently produce incorrect results.
#!/usr/bin/env bash
# Apply the same backport commit to all actively supported release branches
set -euo pipefail
COMMIT_SHA="8f3a1c2"
SUPPORTED_BRANCHES=("release/2.3.x" "release/2.4.x" "release/2.5.x")
for branch in "${SUPPORTED_BRANCHES[@]}"; do
echo "Backporting $COMMIT_SHA to $branch"
git checkout "$branch"
git pull origin "$branch"
if git cherry-pick -x "$COMMIT_SHA"; then
git push origin "$branch"
echo "OK: $branch updated"
else
echo "CONFLICT: $branch needs manual resolution" >&2
git cherry-pick --abort
fi
done
7. Coordinating release branches with CI/CD pipelines
CI/CD pipelines should treat release branches and tags differently, because they represent different levels of trust. A push to a release/* branch typically triggers a deploy to a staging or UAT environment, where customers or QA give final sign-off on the stabilization work. A new, signed tag in the format v*.*.*, on the other hand, triggers the production deploy, because a tag marks a deliberately released, immutable state and is therefore a more reliable trigger than an ever-changing branch.
This separation significantly reduces the risk of accidental production deployments, because a plain commit on the release branch never goes live automatically, only an explicit tag creation does. In GitLab CI or GitHub Actions this can be controlled with rules that check the ref type and naming pattern. For Magento deployments this means concretely: staging deploys run on every push to the release branch including composer install and setup upgrade, while the production deploy only runs after the tag, with extra sign-off steps such as maintenance mode and cache warm-up.
# .gitlab-ci.yml excerpt: branch-triggered staging, tag-triggered production
stages:
- test
- deploy-staging
- deploy-production
deploy_staging:
stage: deploy-staging
script:
- composer install --no-dev --optimize-autoloader
- bin/magento setup:upgrade
- bin/magento cache:flush
rules:
- if: '$CI_COMMIT_BRANCH =~ /^release\/.*/'
deploy_production:
stage: deploy-production
script:
- bin/magento maintenance:enable
- composer install --no-dev --optimize-autoloader
- bin/magento setup:upgrade
- bin/magento setup:di:compile
- bin/magento cache:warm-up
- bin/magento maintenance:disable
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
when: manual
8. Merge-back: release branch into main/develop
After a successful release, the release branch must be merged back into main and develop so that every fix applied during stabilization also reaches ongoing development. If this step is forgotten, the same bug resurfaces later, because the fix only existed on the release branch and continued development on develop never received it. The merge-back should be a fixed part of the release checklist, not an optional afterthought.
In practice, the merge-back works cleanest as a regular merge commit rather than a rebase, because that keeps the release branch's history, including all hotfix commits, traceable. Conflicts arise mainly when develop has already changed the same area of code since the branch point, and are usually a good signal that a fix was already solved elsewhere anyway. After the merge-back, the release branch can be archived for completed major or minor versions, while actively supported lines such as 2.4.x remain as long-lived branches.
9. Release branches compared side by side
The following table contrasts risky, unstructured release practices with the recommended, structured alternatives. The differences look small at first glance, but in practice they decide whether a hotfix can be shipped safely in a few minutes or triggers a night of manual corrections.
| Task | Risky / unstructured | Structured practice | Benefit |
|---|---|---|---|
| Shipping a hotfix | Hotfix directly on main | Hotfix branch from a release tag | Fix targets exactly the released state |
| Assigning version numbers | No versioning / arbitrary numbers | Semantic versioning with a fixed bump rule | Version number reveals update risk |
| Marking a release | Lightweight tags without a message | Annotated, signed tags | Immutable, provable shipping point |
| Bringing a fix into an older version | Fix retyped manually in every branch | Cherry-pick with -x and backport reference | Traceable, no diverging implementations |
| Deploying to production | Deploy on every commit without sign-off | Tag-triggered production deploy after sign-off | No accidental live deployments |
The common thread in the structured column: every step leaves a traceable trail in the Git history, from branch naming to tag signature to the commit reference in a cherry-pick. That traceability is exactly what an audit or a debugging session months later needs, when it has to be established which version received which fix and when.
Mironsoft
Release management, versioning strategy and CI/CD pipelines for Magento teams
Ready to structure your release process before the next hotfix hits?
We build release-branch workflows, semantic versioning conventions and CI/CD pipelines that cleanly separate staging and production deployments, so your Magento team ships releases predictably and without late-night firefighting.
Branching strategy
Setting up release, hotfix and support branches that match your support timeline
Versioning & tagging
Introducing semantic versioning, signed tags and automated changelogs
CI/CD integration
Configuring branch- and tag-triggered pipelines for staging and production
10. Summary
Structured release branches solve a recurring problem: stabilization and ongoing development must not share the same branch, or they end up blocking each other. A release branch, cut at code freeze, only accepts bugfixes from that point on, while new features keep flowing undisturbed on develop. Semantic versioning with MAJOR.MINOR.PATCH makes every version number meaningful, and annotated, signed tags mark immutable, trustworthy shipping points.
Cherry-pick backporting deliberately brings fixes into older supported lines without pulling in the full history, and a clear support matrix prevents customers on older versions from going unserved. CI/CD pipelines that treat branch pushes for staging and tag creation for production differently significantly reduce the risk of accidental live deployments. The merge-back into main and develop closes the loop and ensures that no fix created during stabilization gets lost in ongoing development.
Release branches and versioning, the essentials at a glance
Release branches
Branch off develop at code freeze, from then on only bugfixes and release prep, no new features.
Semantic versioning
MAJOR for breaking changes, MINOR for new features, PATCH for bugfixes. A fixed bump rule instead of gut feeling.
Tags & signing
Annotated tags with git tag -a, signed with git tag -s. An immutable, trustworthy shipping point.
CI/CD triggers
A branch push triggers the staging deploy, a tag creation triggers the production deploy. Separates preview from going live.