Building on CI
Turn a commit into a deployable artefact: composer install with access keys, then di:compile and static deploy with no database.
composer install --no-dev && bin/magento setup:di:compile && bin/magento setup:static-content:deploy -f -j 4A build server turns one commit into one deployable tree. Magento needs it because the two slowest steps of a release produce files rather than touch the database: generating the dependency injection code, and compiling static view files. Both run with nothing but the codebase and a PHP 8.4 CLI.
The three commands
composer install --no-dev installs exactly what composer.lock pins. Magento's own packages come from repo.magento.com, which needs your Commerce access keys. bin/magento setup:di:compile writes the generated/ directory. bin/magento setup:static-content:deploy -f writes pub/static; the -f is required because static deployment otherwise refuses to run outside production mode.
Why the build box needs config.php
Static deployment has to know which themes and locales exist. On a live system that list is in the database, which the build box does not have. Running bin/magento app:config:dump once on production writes it into app/etc/config.php. Commit that file and the build server can read it without a connection.
The artefact
What you ship is the whole tree, including vendor/, generated/ and pub/static, with .git, var/ and pub/media excluded. If config.php is missing a locale, static deployment builds only the locales it can see, and the second storefront loads with no CSS and no visible error.
Example
# .ci/build.sh - runs on the build server; no database, no Redis, no OpenSearch
set -e
# access keys from the CI secret store, never from a committed auth.json
export COMPOSER_AUTH="$MAGENTO_COMPOSER_AUTH"
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
# writes generated/ : interceptors, factories, the compiled DI config
bin/magento setup:di:compile
# writes pub/static ; -f because the box is not in production mode
# locales come from app/etc/config.php, or are listed explicitly
bin/magento setup:static-content:deploy -f -j 4 en_US de_DE
tar --exclude=./.git --exclude=./var --exclude=./pub/media \
-czf "/artifacts/$CI_COMMIT_SHA.tar.gz" .
# Compilation was started.
# ...
# Generated code and dependency injection configuration successfully.
#
# Successful: 2714 files; errors: 0
# ---
# Execution time: 93.4 secWhen to use it
- Build a release tarball on CI so production never runs composer or a compiler.
- Keep static content deployment off the web nodes, where it competes with traffic for CPU.
- Fail a pipeline on a DI compilation error instead of discovering it after the swap.
- Add a store view in a new locale and have CI build its static files.
More examples
Access keys as a build-time secret
This is the shape of auth.json, and the same JSON is what the COMPOSER_AUTH environment variable holds. The username is the public key from your Adobe Commerce account and the password is the private key. On CI, prefer the environment variable: auth.json sits in the project root next to composer.json and is one careless `git add -A` away from being public. If you keep both, the environment variable wins, because Composer merges COMPOSER_AUTH again after it has read the project's auth.json.
{
"http-basic": {
"repo.magento.com": {
"username": "<public key>",
"password": "<private key>"
}
}
}The locale list the build server reads
Reach for this the first time static content deployment on CI produces only one locale. The file is generated, not hand-written: run `bin/magento app:config:dump` on the live system, commit the result, and re-run it whenever you add a store view or a locale. It also pins the enabled module list, so CI compiles the same modules production runs.
<?php
// app/etc/config.php - generated by app:config:dump on production, then committed
return [
'modules' => [
'Magento_Store' => 1,
'Vendor_Loyalty' => 1,
],
'scopes' => [
// website, group and store rows, exactly as dumped from production
],
'system' => [
'default' => [
'general' => ['locale' => ['code' => 'en_US']],
],
'stores' => [
'de' => [
'general' => ['locale' => ['code' => 'de_DE']],
],
],
],
];The same three steps as a CI job
Shows where the secret is injected and where the artefact leaves the pipeline. The useful property is that a compilation error fails the job: you find out before anything is on a web node, rather than after the symlink has moved. Pin the PHP minor version here to the one production runs, because DI compilation output is PHP-version specific.
# .github/workflows/build.yml
name: build
on: [push]
jobs:
artefact:
runs-on: ubuntu-latest
env:
COMPOSER_AUTH: ${{ secrets.MAGENTO_COMPOSER_AUTH }}
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- run: bash .ci/build.sh
- uses: actions/upload-artifact@v4
with:
name: release
path: /artifacts/*.tar.gz
Discussion