Build, Test, Package, Deploy, Verify, Rollback
A GitLab pipeline made up of a single long script block is not a CI/CD process, it is automated chaos. Only once Build, Test, Package, Deploy, Verify and Rollback are modeled as distinct stages does a pipeline become traceable, rollback capable and workable for a whole team.
Table of Contents
- 1. Why pipeline stages are not cosmetic
- 2. Build stage: the reproducible foundation
- 3. Test stage: the quality gate before packaging
- 4. Package stage: bundling and signing artifacts
- 5. Deploy stage: an atomic release switch
- 6. Verify stage: the functional close of the pipeline
- 7. Rollback stage: a rehearsed way back
- 8. Controlling stage dependencies with needs
- 9. Stages compared side by side
- 10. Summary
- 11. FAQ
1. Why pipeline stages are not cosmetic
The stages directive in a .gitlab-ci.yml file is the first place where a team makes the logic of its deployment process visible. Anyone who crams every step into a single job loses the ability to rerun individual phases, debug them in isolation, or run them in parallel. Stages are not organizational decoration, they are the backbone of a traceable release process.
For Magento projects, this separation matters even more. Building a Magento shop involves Composer, DI compilation, a Node build and static content, none of which have anything to do with the actual deployment to the target server. When these steps are separated from the deployment steps, the same artifact can be rolled out to staging and production without rebuilding it. That is the core idea behind reproducible deployments.
A well designed stage chain also makes it clear what should happen when a step fails. If the test job fails, no deploy runs. If the verify job fails, the rollback stage is ready to go. That logic is readable and extensible in a pipeline with explicit stages, whereas in a monolithic script block it stays hidden and fragile.
2. Build stage: the reproducible foundation
The build stage has exactly one job: turning the source code into a complete, deployable artifact. For Magento projects that means composer install --no-dev, setup:di:compile, installing Node packages and running the Tailwind build. The result is stored as a GitLab artifact and reused by every subsequent job, identically on staging and production.
The critical point is that the build stage needs no connection to the production server. It runs entirely inside the runner, with no SSH access, no database connection and no knowledge of the target environment. That makes it testable, repeatable and independent. Anyone who already touches the server during the build stage is mixing two phases that need to stay conceptually separate.
stages:
- build
- test
- package
- deploy
- verify
- rollback
variables:
GIT_STRATEGY: fetch
COMPOSER_CACHE_DIR: .cache/composer
NPM_CONFIG_CACHE: .cache/npm
# Build stage: produce deployment artifact, no server access needed
build:magento:
stage: build
image: php:8.4-cli
cache:
key: composer-$CI_COMMIT_REF_SLUG
paths:
- .cache/composer/
- .cache/npm/
script:
- composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
- 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/frontend/
expire_in: 2 hours
when: on_success
3. Test stage: the quality gate before packaging
The test stage is the quality gate between build and deployment. This is where PHPStan, PHPUnit, PHPCS and security checks run, using the artifacts from the build stage as their basis. A job that fails in the test stage automatically prevents a broken artifact from ever being deployed. That is the value of a quality gate: not just finding problems, but blocking the process until they are fixed.
In Magento projects it pays off to split the test stage into parallel jobs: one for static analysis, one for unit tests, one for coding standards. With needs, these jobs can start at the same time as soon as the build job has finished. That shortens the overall pipeline runtime considerably without breaking the logic of the stages. Only once every test job succeeds is the package stage allowed to begin.
4. Package stage: bundling and signing artifacts
The package stage turns the built artifact into a transportable package. For Magento deployments using SSH and rsync, the package is typically the whole release directory, transferred to the target server via rsync. Alternatively, a tar.gz archive with a checksum can be produced, allowing the integrity of the package to be verified before it is unpacked on the server.
The package stage is also the right place for release metadata: the build ID, the commit hash, the branch name and the timestamp. This information is written into a release.json file that is deployed together with the package. That way the server always shows which pipeline run produced which release, without anyone having to look it up in GitLab.
5. Deploy stage: an atomic release switch
The deploy stage transfers the artifact to the target server and performs the symlink switch. In a clean release structure with releases/, current and shared/, the actual downtime critical moment is reduced to a single ln -sfn command. Everything before that, rsync, symlinks for shared files, Magento setup steps, happens inside the new release directory while the active current directory stays untouched.
For production deployments, the deploy stage should always be protected with when: manual or an explicit tag rule. That prevents an accidental merge into main from immediately triggering a production deploy. Staging deployments, by contrast, can run automatically on every merge into the develop branch, a pattern that enables fast feedback cycles without putting production at risk.
# Deploy stage: transfer artifact and switch symlink atomically
deploy:production:
stage: deploy
environment:
name: production
url: https://shop.example.com
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
when: manual
before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh && echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
script:
- RELEASE_ID="$(date +%Y%m%d-%H%M%S)-${CI_COMMIT_SHORT_SHA}"
- RELEASE_PATH="${DEPLOY_PATH}/releases/${RELEASE_ID}"
- ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "mkdir -p ${RELEASE_PATH}"
- rsync -az --delete --exclude='.git' ./ "${DEPLOY_USER}@${DEPLOY_HOST}:${RELEASE_PATH}/"
- ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "
set -euo pipefail &&
ln -sfn ${DEPLOY_PATH}/shared/app/etc/env.php ${RELEASE_PATH}/app/etc/env.php &&
ln -sfn ${DEPLOY_PATH}/shared/pub/media ${RELEASE_PATH}/pub/media &&
cd ${RELEASE_PATH} &&
php bin/magento setup:upgrade --keep-generated &&
php bin/magento cache:flush &&
ln -sfn ${RELEASE_PATH} ${DEPLOY_PATH}/current"
6. Verify stage: the functional close of the pipeline
The verify stage is the ending that pipelines most often forget. Technically, the deployment is finished once the symlink switch has happened, functionally it is not. A deployment is only truly successful once the health endpoint responds, the homepage loads, the cache status is correct and critical API routes react as expected. Moving these checks into a dedicated verify job makes them repeatable and logged.
A verify job that fails should automatically trigger the rollback stage, or at least raise an alert. That closes the gap between a technically deployed release and one that actually works. For Magento shops, these checks can be implemented with curl -f, bin/magento cache:status and simple HTTP assertions, with no external test frameworks required.
7. Rollback stage: a rehearsed way back
A rollback stage that only gets invented once something breaks is not a rollback, it is improvisation. Every production pipeline should have a rollback stage in place, even if it rarely runs. For Magento with a release structure, the mechanism is simple: the current symlink is pointed back at the previous release directory, the cache is cleared and the verify job runs again. That takes seconds and requires no rebuild.
In GitLab, the rollback stage can be implemented with when: manual and a variable for the target release. If something goes wrong, the team decides which release gets reactivated, and the pipeline carries out the switch. Important: the rollback job must assume the same SSH credentials and the same release structure as the deploy job in order to work reliably.
# Verify stage: confirm deployment is functionally complete
verify:production:
stage: verify
needs: ["deploy:production"]
script:
- curl -f --retry 5 --retry-delay 3 https://shop.example.com/health
- curl -f https://shop.example.com/
- ssh "${DEPLOY_USER}@${DEPLOY_HOST}"
"cd ${DEPLOY_PATH}/current && php bin/magento cache:status | grep -v 'disabled'"
when: on_success
# Rollback stage: reactivate previous release via symlink, no rebuild needed
rollback:production:
stage: rollback
rules:
- if: '$CI_PIPELINE_SOURCE == "web"'
when: manual
script:
- ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "
set -euo pipefail &&
PREV=\$(ls -1t ${DEPLOY_PATH}/releases/ | sed -n '2p') &&
ln -sfn ${DEPLOY_PATH}/releases/\${PREV} ${DEPLOY_PATH}/current &&
cd ${DEPLOY_PATH}/current &&
php bin/magento cache:flush"
8. Controlling stage dependencies with needs
The needs keyword in GitLab CI makes it possible to break out of the strict sequential order of stages and define a DAG (Directed Acyclic Graph) instead. A test job can start right after the build job without waiting for other jobs in the same stage. That shortens the pipeline runtime considerably, because parallel work becomes possible without giving up the logical order.
A typical use case in Magento pipelines: PHPStan, PHPUnit and PHPCS all start at the same time as soon as the build artifact is available. The package stage only starts once all three test jobs have finished successfully. Using needs and dependencies, this graph can be modeled explicitly without giving up stage barriers. The result is faster pipelines without losing any control structure.
9. Stages compared side by side
The choice of stage design has a direct effect on the speed, traceability and safety of the deployment process. The table below compares common antipatterns with the recommended stage patterns.
| Stage Decision | Antipattern | Recommended Pattern | Benefit |
|---|---|---|---|
| Separate build and deploy | composer install on the server | Build in the runner, deploy the artifact | Reproducible, no network access needed on production |
| Test blocking | Tests after deploy | Test stage before deploy | Failures block the deployment path |
| Production protection | Auto deploy on every push | when: manual + tag rule | No accidental production deploy |
| Verify step | Pipeline ends after deploy | Verify stage with curl + cache:status | Functional close instead of a purely technical one |
| Rollback | Improvised when something breaks | Rollback stage with when: manual | A rehearsed, documented way back |
The antipatterns in the table are not theoretical constructs, they show up regularly in real Magento projects. The common denominator: they emerge when a pipeline gets thrown together quickly and never gets structurally revisited. The recommended stage design costs a bit more planning up front, but it pays off with every deployment, and even more so with every incident.
10. Summary
A pipeline with sensibly designed stages is not a luxury reserved for large teams, it is the minimum requirement for a professional Magento deployment process. Build, Test, Package, Deploy, Verify and Rollback are not arbitrary labels, they describe the logical order of a deployment in which every phase has a clear responsibility and can fail without damaging the phases that follow.
The single biggest win is separating build and deploy: an artifact gets built once and rolled out to multiple environments. The second biggest win is the verify stage: the pipeline does not end after the symlink switch, it only ends once functionality has been confirmed. And the rollback stage ensures that the way back is not an emergency improvisation, but a deliberately designed part of the process.
Designing Pipeline Stages That Make Sense: The Essentials At a Glance
Build vs. Deploy
Build inside the runner with no server access. Produce the artifact once, deploy it identically to staging and production.
Test Gate
Test stage before deploy. PHPStan, PHPUnit and PHPCS run in parallel with needs. If one job fails, no deploy happens.
Verify Close
Verify stage after deploy with curl and cache:status. The pipeline only counts as successful once the application actually responds.
Rollback Readiness
Rollback stage with when: manual. Symlink back to the previous release, no rebuild, no stress test during an incident.