Preparing a Rollback Script for Magento Releases in GitLab
AI generated
CI/CD
.yml
GitLab · CI/CD · Magento · Rollback
Preparing a rollback script
for Magento releases in GitLab

A rollback that only gets written once things break is not a rollback at all. If the rollback script is not finished before the first deployment, it will be tested for the first time under pressure and stress, exactly when everything needs to happen fast.

12 min read Rollback script · Symlink · Cache flush · Maintenance mode · GitLab job GitLab CI/CD · Magento 2

1. The rollback philosophy: preparation instead of improvisation

A rollback is not a special case, it is a regular part of the deployment process. Anyone who runs deployments without a prepared rollback path does not have a deployment process, they have a hope. That sounds harsh, but it reflects the experience from many Magento projects: the rollback gets postponed until it is needed, and then it fails because of missing prerequisites, time pressure and lack of practice.

The rollback script must therefore be developed as the first part of the deployment system, not the last. It must be versioned in the repository, it must be available in the GitLab pipeline as a manual job, and it must be tested regularly on staging, at least once a quarter, ideally monthly. Only a rehearsed rollback is a real rollback.

The good news for Magento with a symlink based release structure: a file rollback is trivial. Switching the current symlink to the previous release directory takes less than a second. The hard part is not the technique, it is the decision, and the side effects such as cache state, queue messages and static content.

2. Prerequisites: what a rollback needs in place

For a rollback to work, three things must be in place. First, the previous release directory must exist on the server. If old releases are deleted too aggressively, there is nothing left to switch back to. At least the last release must always be kept, ideally three to five. Second, the symlink mechanism must be set up correctly. A rollback is only possible if deployment never overwrote files directly but always deployed into a new directory and then switched the symlink. Third, the rollback script must have the credentials to reach the server, the same SSH credentials the deploy script uses.

# Verify rollback prerequisites before any production deploy
rollback:check-prerequisites:
  stage: verify
  script:
    - |
      ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash -s <<'SSH'
      set -euo pipefail
      APP_PATH="${DEPLOY_PATH}"

      # Count available releases for rollback
      release_count=$(ls -1d "${APP_PATH}/releases/"*/ 2>/dev/null | wc -l)
      echo "[INFO] Available releases for rollback: ${release_count}"

      if [[ ${release_count} -lt 2 ]]; then
        echo "[WARN] Only ${release_count} release(s) available, rollback not possible after this deploy"
      fi

      # Verify current symlink exists and resolves correctly
      current_target=$(readlink -f "${APP_PATH}/current" 2>/dev/null || echo "MISSING")
      echo "[INFO] Current release: ${current_target}"

      test -d "${current_target}" \
        || { echo "[FAIL] current symlink target does not exist"; exit 1; }

      echo "[OK] Rollback prerequisites verified"
      SSH
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/

3. The complete rollback script

The rollback script has a clear structure: first the target release is determined, either through an explicitly passed parameter or automatically as the second to last release in the releases/ directory. Then all the necessary Magento steps are executed: enable maintenance mode, switch the symlink, flush the cache, align static content with the new release and disable maintenance mode. Every step is logged, errors cause an immediate abort.

#!/usr/bin/env bash
# scripts/rollback.sh: Roll back to a previous Magento release
# Usage: ./rollback.sh [release-identifier]
# If no release identifier is given, rolls back to the previous release
set -euo pipefail

readonly APP_PATH="${DEPLOY_PATH:?DEPLOY_PATH is not set}"
readonly DEPLOY_HOST_VAR="${DEPLOY_HOST:?DEPLOY_HOST is not set}"
readonly DEPLOY_USER_VAR="${DEPLOY_USER:?DEPLOY_USER is not set}"

# Determine target release, use argument or auto-detect previous
TARGET_RELEASE="${1:-}"

ssh "${DEPLOY_USER_VAR}@${DEPLOY_HOST_VAR}" bash -s <<SSH
set -euo pipefail
APP_PATH="${APP_PATH}"

# Auto-detect previous release if none specified
if [[ -z "${TARGET_RELEASE}" ]]; then
  current_release="\$(readlink -f \${APP_PATH}/current)"
  current_name="\$(basename \${current_release})"
  # List releases sorted by name descending, skip current, take first
  TARGET_RELEASE="\$(ls -1d \${APP_PATH}/releases/*/ \
    | sort -r \
    | grep -v "\${current_name}" \
    | head -1 \
    | xargs basename)"
  echo "[INFO] Auto-detected rollback target: \${TARGET_RELEASE}"
fi

ROLLBACK_PATH="\${APP_PATH}/releases/\${TARGET_RELEASE}"

# Abort if target does not exist
test -d "\${ROLLBACK_PATH}" \
  || { echo "[FAIL] Release \${TARGET_RELEASE} not found on server"; exit 1; }

echo "[ROLLBACK] Switching from \$(basename \$(readlink \${APP_PATH}/current)) to \${TARGET_RELEASE}"

# Enable maintenance mode to prevent user requests during switch
cd "\${APP_PATH}/current"
bin/magento maintenance:enable || echo "[WARN] Could not enable maintenance mode"

# Atomic symlink switch to previous release
ln -sfn "\${ROLLBACK_PATH}" "\${APP_PATH}/current"
echo "[OK] Switched current to \${TARGET_RELEASE}"

# Flush all caches, the previous release may have different cache keys
cd "\${APP_PATH}/current"
bin/magento cache:flush
echo "[OK] Cache flushed"

# Disable maintenance mode
bin/magento maintenance:disable || echo "[WARN] Could not disable maintenance mode"
echo "[ROLLBACK COMPLETE] Now running \${TARGET_RELEASE}"
SSH

4. The rollback job in the GitLab pipeline

The rollback job is a manual job in the rollback stage of the pipeline. It does not run automatically, that would mean every pipeline could trigger a rollback. Instead, it can be triggered with a click in the GitLab interface without starting a new pipeline. The last pipeline that ran a successful deploy also contains the rollback job that rolls back on the same server.

The variant with when: manual and without allow_failure: false ensures that the pipeline is not marked as failed when the rollback job is not executed. The job is optional, it only runs in an emergency. In addition, a ROLLBACK_TARGET variable can be passed as a pipeline variable to specify a particular release as the rollback target.

rollback:production:
  stage: rollback
  environment:
    name: production
    url: https://shop.example.com
  variables:
    # Optional: override with specific release name via GitLab UI pipeline variable
    ROLLBACK_TARGET: ""
  script:
    - chmod +x scripts/rollback.sh
    - ./scripts/rollback.sh "${ROLLBACK_TARGET}"
  rules:
    # Available only on semver tags, same pipeline that deployed the release
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: manual
      allow_failure: true
  # Rollback job should run even if deploy verify failed
  needs:
    - job: deploy:production
      optional: true

5. Determining the previous release automatically

Automatically determining the previous release is a critical part of the rollback script. The script must not make assumptions about how releases are named, it has to decide based on the directories that actually exist on the server. The most reliable approach is to list all release directories in sorted order and skip the currently active release.

If the tag format is SemVer, a semantic sort can also be implemented: sort -V sorts by version number correctly and ensures that v1.9.0 comes before v1.10.0. This sort is more robust than alphabetical sorting, which produces wrong results with two digit minor versions. The script always clearly states which release was chosen as the rollback target before it performs the switch.

6. Cache flush and queue behavior during rollback

After the symlink switch, the Magento cache must be flushed completely. This applies even if the previous release used the same cache keys, because Redis may have cached data from the broken release that is incompatible with the old code. A bin/magento cache:flush after the rollback is mandatory, not an optional step.

The queue is more complex. Messages that are already sitting in RabbitMQ or in the Magento queue may have been placed there by consumers of the new release. These messages might not be processed correctly by the old code. The rollback script should stop queue consumers before the symlink is switched, and start them again after the rollback. In Magento environments with Supervisor or systemd, the consumer processes are managed by a process manager anyway, these need to be restarted explicitly after the rollback so they pick up the new application path.

7. Testing the rollback regularly

The most important sentence in this article is: a rollback that has never been tested is not a rollback. The rollback script must be run regularly on staging, not in a simulated environment but with the real script against the real staging server. That means: deploy a release, then trigger the rollback job, then verify that the previous state is active again.

This test should be part of the regular team routine and documented in the deploy checklist. Ideally a chaos engineering approach is chosen: once a month, before the planned deployment, a rollback test is run on staging. The team sees how long the rollback takes, which steps run and whether the verify job is green afterward. This exercise is cheaper than an unplanned rollback under production pressure.

8. Rollback strategies compared

There are several rollback strategies that require different amounts of preparation and come with different constraints. The comparison helps decide which strategy fits your own Magento project.

Strategy Duration Prerequisite Suitability
Symlink rollback < 1 minute Release directory present Always recommended
New deployment (old tag) 5 to 15 minutes Tag present, pipeline green When no release directory is left
Database rollback 30+ minutes Backup before the deploy Only if DB migrations require a rollback
git revert plus deploy 15 to 30 minutes New commit plus new pipeline When no older release is available
Server snapshot 5 to 20 minutes Snapshot taken before the deploy Emergency without a release structure

9. Summary

A rollback script for Magento in GitLab is not an optional feature, it is a mandatory part of every deployment process. The script switches the current symlink to the previous release, flushes the cache, stops and starts queue consumers and clearly reports what is happening. The associated GitLab job can be triggered manually from the last deploy pipeline and does not require a new pipeline.

The key to a successful rollback lies in three things: preparation before the first deployment, testing on staging at regular intervals, and clear documentation of who is allowed to trigger the rollback job under which circumstances. A rollback that has not been rehearsed is a gamble in an emergency, not a plan.

Rollback script for Magento: the essentials at a glance

Core mechanism

Symlink switch to the previous release. Takes less than a second. Requires release directories to be present.

Mandatory steps

Enable maintenance mode, switch symlink, flush cache, restart queue consumers, disable maintenance mode. Keep the order.

GitLab integration

Manual job in the rollback stage of the deploy pipeline. No new pipeline needed. ROLLBACK_TARGET variable for specific releases.

Test cadence

At least monthly on staging. Rollback test as a fixed part of the deploy checklist. An unrehearsed rollback is not a real rollback.

10. Common rollback mistakes

The most common mistake is forgetting to flush the cache after the symlink switch. Redis may hold cached objects from the broken release that are incompatible with the old code. A Magento system that runs after a rollback without a cache flush behaves unpredictably, sometimes correctly, sometimes with 500 errors that are hard to diagnose.

A second mistake is missing verification that the rollback target actually exists. A rollback script that tries to switch to a release that is no longer present, leaving a half completed state, makes the damage worse. The script must check the target directory and abort with a clear error if it is absent, before the current symlink is changed. The order matters: check first, then switch.

11. FAQ: Rollback script for Magento in GitLab

1How long does a symlink rollback take?
Symlink switch in under a second. Total with maintenance mode, cache flush and queue restart is 30 to 120 seconds.
2Is a rollback possible without a GitLab job?
Yes, via direct SSH access. But the GitLab job logs the time and the person who ran it, important for traceability.
3What happens to the database during a rollback?
Nothing. A file rollback does not touch the database. Releases with DB migrations need a separate rollback strategy.
4Who is allowed to trigger the rollback?
Only maintainers, the same role that can trigger deployments. Controlled via GitLab project roles and protected environments.
5Rollback target release no longer on the server?
Fallback: manually restart the old release pipeline. Or use a server snapshot from the cloud provider. A retention policy prevents this problem.
6Queue consumers after a rollback?
Restart them with Supervisor or systemd. Consumers read code through the current symlink, after a restart they automatically use the new (old) code.
7Is maintenance mode needed during a rollback?
Yes, briefly for the symlink switch and cache flush. Prevents requests hitting an inconsistent state. Only lasts 10 to 30 seconds.
8Trigger a rollback from a different pipeline?
Possible via a manually triggered pipeline. Simpler: trigger the rollback job directly from the last deploy pipeline.
9How do I document a rollback?
Automatically through the GitLab job log plus RELEASES.log on the server plus a Slack notification from the job.
10Rollback versus revert?
Rollback: immediate code switch to an old state without a new commit. Revert: a new commit undoes the changes, then deploy. Rollback is faster, revert is cleaner for history.