Structuring Release Branches and Versioning
AI generated
git
HEAD
Git · Release Management · Versioning · CI/CD
Structuring Release Branches and Versioning
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.

12 min read Release Branches · Semantic Versioning · Cherry-Pick Git · CI/CD · Magento 2

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.

11. FAQ: Release Branches and Versioning

1What is a release branch and when should I create one?
A dedicated Git branch for the stabilization phase before a release, cut at code freeze. From then on only bugfixes, no new features.
2How does semantic versioning differ from arbitrary versioning?
Fixed MAJOR.MINOR.PATCH scheme: MAJOR for breaking changes, MINOR for new features, PATCH for bugfixes. The version number signals update risk immediately.
3Annotated vs. lightweight Git tag?
Annotated tag stores tagger, date and message as its own object. Lightweight tag is just a pointer with no metadata. Always annotated, ideally signed, for releases.
4Why sign release tags?
A valid GPG signature proves a release came from an authorized person and was not tampered with afterwards. Matters especially in regulated environments.
5What is backporting and when do I need it?
Brings a fix from the current development line into an older, still-supported release branch. Needed once customers run multiple versions in production.
6How does git cherry-pick work for backporting?
git cherry-pick -x <commit> applies a single commit onto the current branch and references the original ID. Resolve conflicts manually, then --continue.
7How many release branches should I maintain at once?
Depends on the support commitment, commonly two to three actively supported lines with a documented support matrix.
8CI/CD: treat release branches and tags differently?
A push to release/* triggers the staging deploy, a signed tag v*.*.* triggers the production deploy. Prevents accidental live deployments.
9What happens if I forget the merge-back?
Fixes are then missing from main and develop and resurface as a bug in the next release. Merge-back belongs firmly in the release checklist.
10How do I keep track of backports?
Consistent commit messages with cherry-pick -x, a changelog per branch, and ideally automated backport pull requests on merges to main.