Building on CI

Turn a commit into a deployable artefact: composer install with access keys, then di:compile and static deploy with no database.

Syntaxcomposer install --no-dev && bin/magento setup:di:compile && bin/magento setup:static-content:deploy -f -j 4

A 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

Example · bash
# .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 sec

When 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.

Example · 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.

Example · php
<?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.

Example · yaml
# .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

  • Be the first to comment on this lesson.