Build, Test, Package, Deploy, Verify
A .gitlab-ci.yml with only three jobs and no defined stages is not a deployment process, it is a script that will eventually break. Deploying Magento cleanly requires a complete pipeline structure: reproducible build artifacts, defined approvals, server side release directories, controlled Magento steps and a functionally complete verify job.
Table of contents
- 1. Why a complete pipeline structure is necessary
- 2. Defining stages: the logical order of a deployment
- 3. The build job: artifacts instead of server state
- 4. The test job: quality assurance before deployment
- 5. The package job: sealing and signing the artifact
- 6. The deploy job: switching releases on the target server
- 7. The verify job: the functional closure of the pipeline
- 8. Rollback: the rehearsed way back
- 9. Pipeline patterns compared
- 10. Summary
- 11. FAQ
1. Why a complete pipeline structure is necessary
A .gitlab-ci.yml for Magento is not a technical detail on the edge of the project, it is the central document of the entire deployment process. It defines what happens in which order, who grants approval, which artifacts get produced and how the system is rolled back after a failed deploy. Anyone who treats this file as a loose collection of commands ends up building a system that fails on every release scenario that deviates from the norm, not because of the technology, but because of a missing process structure.
In Magento projects the pipeline structure is especially critical because several interdependent systems are involved: Composer dependencies, generated DI code, compiled frontend assets, database migrations, static content, shared files and cache layers. If these steps are not executed in a clear order with defined dependencies, the result is a deployment that happens to work, but that nobody can actually reason about.
The goal of a complete .gitlab-ci.yml is not to cover every possible case, it is to make the standard path so explicit that deviations become visible immediately. A pipeline that reports an error in the build job transparently is more valuable than a pipeline that runs silently all the way to the deploy job and then breaks on production.
2. Defining stages: the logical order of a deployment
The stages declaration at the top of the .gitlab-ci.yml is more than documentation, it determines the order in which jobs run and which stage has to complete before the next one begins. For Magento a structure of at least six stages is recommended: build, test, package, deploy, verify and rollback. This order mirrors the actual flow of a deployment and makes it visible to the whole team.
The variables section at the top of the file defines project wide settings such as cache directories, git strategy and default values. These values are inherited by every job and can be overridden at job level. A clear separation between values that are allowed to live in the YAML file and values that must be stored as CI/CD variables in GitLab is mandatory, secrets must never sit inside the YAML file itself.
# .gitlab-ci.yml: Complete Magento pipeline structure
stages:
- build
- test
- package
- deploy
- verify
- rollback
# Global pipeline variables (non-secret)
variables:
GIT_STRATEGY: fetch
GIT_DEPTH: "10"
COMPOSER_CACHE_DIR: ".cache/composer"
NPM_CONFIG_CACHE: ".cache/npm"
MAGENTO_LOCALE: "de_DE"
RELEASE_RETENTION: "5"
DEPLOY_TIMEOUT: "300"
# Reusable SSH setup anchor
.ssh_setup: &ssh_setup
before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
The include directive lets you split the pipeline across multiple files and maintain shared job templates in their own files. For Magento projects with several environments, splitting into a common base plus environment specific overrides works well. Anchors and YAML merging (<<: *anchor) reduce repetition within a single file, but they do not solve reuse across files, that is what include is for.
3. The build job: artifacts instead of server state
The build job is the heart of a reproducible pipeline. It runs every step needed to turn the repository content into a complete, deployable artifact, without depending on the state of any particular server. In practice that means composer install with --no-dev and --prefer-dist, the frontend build with npm and Tailwind CSS, and setup:di:compile for the Magento dependency injection layer.
Artifacts are defined through the job's artifacts section. GitLab automatically passes these files on to the following jobs in the pipeline. The list of artifact paths has to be precise: too few paths mean later jobs run into missing files, too many mean unnecessarily large artifacts get transferred between jobs. The expiry (expire_in) keeps old artifacts from piling up on the GitLab server's storage.
# Build stage: produces all deployment artifacts from source
build:magento:
stage: build
image: php:8.4-cli
cache:
key: "${CI_COMMIT_REF_SLUG}-composer"
paths:
- .cache/composer/
policy: pull-push
before_script:
- apt-get update -qq && apt-get install -y -qq git unzip libzip-dev
- docker-php-ext-install zip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- node --version && npm --version
script:
# Install PHP dependencies without dev packages
- composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
# Build Tailwind CSS assets
- npm ci --prefix app/design/frontend/Mironsoft/default/web/tailwind
- npm run build --prefix app/design/frontend/Mironsoft/default/web/tailwind
# Compile Magento DI (generates interceptors and factories)
- bin/magento setup:di:compile
# Deploy static content for configured locales
- bin/magento setup:static-content:deploy "$MAGENTO_LOCALE" -f --jobs=4
artifacts:
name: "magento-build-${CI_COMMIT_SHORT_SHA}"
paths:
- vendor/
- generated/
- pub/static/
- app/etc/config.php
exclude:
- vendor/**/.git
expire_in: 1 day
only:
- main
- /^release\/.*/
4. The test job: quality assurance before deployment
The test job runs after the build job and has access to its artifacts. It runs static code analysis and automated tests against the built code, not against the raw repository state. That matters because some errors only become visible after setup:di:compile: incorrectly typed constructor parameters, missing classes in generated/, or plugin conflicts that only surface at compile time.
The most important test jobs for Magento are PHPStan with Magento specific rules, PHPCS with the Magento coding standard, and unit tests with PHPUnit. PHPStan at level 6 or higher catches type errors that simply should not be acceptable in a typed PHP 8.4 project anymore. PHPCS makes sure new code follows Magento's standards and doesn't introduce obvious quality issues. All of these checks should fail with a non zero exit code so the pipeline blocks on violations.
5. The package job: sealing and signing the artifact
The package job bundles the build artifacts into a single, sealed package that gets passed to the deploy job. For Magento a tar.gz archive that contains every required directory and carries a checksum (SHA-256) is recommended. That checksum makes it possible to verify the package's integrity before deployment and to prove it afterward.
The package job also generates metadata: a release ID (timestamp based or taken from a git tag), the commit SHA, the branch name and the pipeline ID. This metadata gets written into a release.json file inside the package, so that on the server it is always possible to trace which pipeline run a given release came from. That is indispensable for rollbacks and incident analysis.
6. The deploy job: switching releases on the target server
The deploy job transfers the package to the target server and switches the current release symlink. It is deliberately configured as when: manual for production, which forces an explicit approval by an authorized person before code gets activated in the production environment. For staging the job can run automatically as soon as test and package have completed successfully.
The actual deployment process follows the symlink model: the new release is unpacked into a timestamp based directory under releases/, shared files (env.php, pub/media, var/log) are mounted as symlinks, and only once every preparation step has completed is the current symlink atomically switched to the new release. The web server configuration always points at current, the symlink switch is the actual zero downtime operation.
# Deploy stage: transfers artifact and switches release symlink
deploy:production:
stage: deploy
<<: *ssh_setup
environment:
name: production
url: https://shop.example.com
script:
- |
# Calculate release identifier from pipeline timestamp
RELEASE_ID=$(date +%Y%m%d-%H%M%S)
RELEASE_PATH="${DEPLOY_PATH}/releases/${RELEASE_ID}"
# Transfer build artifact to release directory
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "mkdir -p ${RELEASE_PATH}"
rsync -az --delete \
--exclude=".git" \
--exclude="var/cache" \
./ "${DEPLOY_USER}@${DEPLOY_HOST}:${RELEASE_PATH}/"
# Link shared paths and activate release on server
ssh "${DEPLOY_USER}@${DEPLOY_HOST}" bash << 'REMOTE'
set -euo pipefail
cd "${RELEASE_PATH}"
# Link persistent shared files into new release
ln -sfn "${DEPLOY_PATH}/shared/app/etc/env.php" app/etc/env.php
ln -sfn "${DEPLOY_PATH}/shared/app/etc/config.php" app/etc/config.php
ln -sfn "${DEPLOY_PATH}/shared/pub/media" pub/media
ln -sfn "${DEPLOY_PATH}/shared/var/log" var/log
ln -sfn "${DEPLOY_PATH}/shared/var/session" var/session
# Run database migrations (only if schema changes exist)
bin/magento setup:upgrade --keep-generated
# Flush all cache types before activating
bin/magento cache:flush
# Atomic symlink switch: this is the zero-downtime moment
ln -sfn "${RELEASE_PATH}" "${DEPLOY_PATH}/current"
# Remove releases exceeding retention limit
ls -dt "${DEPLOY_PATH}"/releases/*/ | tail -n +$((RELEASE_RETENTION + 1)) | xargs rm -rf
REMOTE
when: manual
only:
- main
- tags
7. The verify job: the functional closure of the pipeline
The verify job closes the pipeline functionally. A deploy job that exits with code 0 only means the SSH commands ran successfully, not that the shop actually works correctly. The verify job runs HTTP checks against the live shop, checks the cache status on the server and confirms that critical pages respond without errors. If the verify job fails, that is the signal to run the rollback job immediately.
The verify job should contain at least three checks: an HTTP 200 check against the health endpoint, a check of the homepage, and a look at the Magento error log on the server. The job can be extended with checks against critical category pages, the checkout flow and the admin interface. The more checks a verify job contains, the earlier a problem is caught after a deployment, before customers report it.
8. Rollback: the rehearsed way back
The rollback job is not an emergency plan, it is a regular part of the pipeline. It is configured as when: manual and can be triggered at any point after a deploy job, ideally within seconds, not minutes. The rollback mechanism is simple: the current symlink is reset to the previous release directory, the cache is flushed, and the verify job runs again.
The rollback model, however, only works reliably if database migrations are backward compatible, rolling back the code while the database schema has an incompatible change is not a real rollback. That is why setup:upgrade in the pipeline should always follow the expand contract pattern: new columns start out optional, old columns are only removed in a later release. That way the rollback path for file changes always stays open.
9. Pipeline patterns compared
The difference between an improvised and a structured .gitlab-ci.yml for Magento becomes especially clear the first time a pipeline runs under unfamiliar conditions, a new developer, a new server, or a branch that deviates from the norm. Structured pipelines are explicit enough to keep working reliably in these situations.
| Aspect | Improvised pipeline | Structured pipeline | Impact |
|---|---|---|---|
| Stage order | Build and deploy in one job | Explicit stages declaration | Visible dependencies and approval points |
| Artifacts | No artifact, build happens on the server | Artifact from build job, passed along | Reproducibility and separation of build/deploy |
| Production approval | Automatic on every push | when: manual for production | Controlled deploy, no accidental release |
| Verify step | None | HTTP check + cache check + log check | Errors caught before customers are affected |
| Rollback | Manual, no rehearsed path | Rollback job, when: manual | Way back in seconds, not hours |
The table shows that the differences do not lie in the complexity of individual commands, but in how explicit the process is. A structured .gitlab-ci.yml costs more effort at the initial design stage, but it prevents the frantic emergency operations that improvised pipelines produce on a regular basis. The return on investment is that every deploy becomes plannable and traceable, not just the first one.
10. Summary
A complete .gitlab-ci.yml for Magento consists of six stages that mirror the actual flow of a deployment: build produces reproducible artifacts from the repository. Test checks the built code against static analysis and unit tests. Package bundles and seals the artifact. Deploy transfers the package, switches the symlink, and runs Magento specific steps. Verify checks the shop after deployment. Rollback restores the previous state when a problem is detected.
The most important rule in practice: nothing that runs on production may stay implicit in the pipeline. Every decision, which artifacts get produced, when a deployment gets approved, which checks run after the deploy, must be explicit in the YAML file. Only then is the pipeline a reliable deployment standard, rather than a fragile construct that only works under ideal conditions.
Complete .gitlab-ci.yml for Magento: the essentials at a glance
Stage order
build → test → package → deploy → verify → rollback. Every stage must complete before the next one begins. No shortcuts in production.
Artifact principle
The build job produces one complete artifact. Every following job uses that same artifact, no second build on the server.
Approval & rollback
Production deploy always as when: manual. The rollback job must be rehearsed and executable in seconds, not improvised in an incident.
Verify as a requirement
Without a verify job the pipeline is technically finished but not functionally complete. HTTP check, cache check and log check are the minimum requirements.