← Voltar ao blog

Pipeline de CI/CD do Magento 2 com GitHub Actions

Rodrigo Biazzi

Muitas lojas Magento ainda fazem deploy conectando no servidor, rodando git pull e depois composer install, setup:upgrade, setup:di:compile e setup:static-content:deploy em produção. Funciona, mas o site fica quebrado ou em manutenção por minutos, um passo que falha deixa tudo pela metade e nada confere o código antes de ele chegar lá.

Um pipeline no GitHub Actions resolve os dois lados. As verificações rodam em cada pull request, o build acontece em um runner de CI e o servidor só recebe um release pronto. Este artigo mostra um setup prático para o Magento 2.4.8 com PHP 8.3. Ajuste nomes e caminhos ao seu projeto.

O formato do pipeline

Eu divido o trabalho em três jobs:

  1. Checks em todo push e pull request: validação do Composer, coding standard, PHPStan e testes unitários.
  2. Build na branch main: instala as dependências sem pacotes de dev, compila o DI, faz o deploy do conteúdo estático e empacota o resultado.
  3. Deploy: envia o pacote ao servidor como um release novo, roda os upgrades de banco só se necessário, troca um symlink e limpa os caches.

O job de deploy depende do job de build, e o job de build depende dos checks. Uma falha em qualquer ponto interrompe o release.

Autenticação do Composer

Os pacotes do Magento Open Source e do Adobe Commerce vêm do repo.magento.com, que exige access keys. Guarde-as como um secret do repositório chamado COMPOSER_AUTH com este conteúdo:

{
    "http-basic": {
        "repo.magento.com": {
            "username": "your-public-key",
            "password": "your-private-key"
        }
    }
}

O Composer lê a variável de ambiente COMPOSER_AUTH automaticamente, então não há necessidade de escrever um auth.json no runner. Se você usa repositórios privados de extensões, adicione as credenciais deles no mesmo JSON.

O job de checks

name: magento

on:
  push:
    branches: [main]
  pull_request:

jobs:
  checks:
    runs-on: ubuntu-latest
    env:
      COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: bcmath, ctype, curl, dom, gd, intl, mbstring, pdo_mysql, soap, sockets, sodium, xsl, zip
          tools: composer:v2
          coverage: none

      - uses: actions/cache@v4
        with:
          path: ~/.cache/composer
          key: composer-${{ hashFiles('composer.lock') }}

      - run: composer validate --no-check-publish
      - run: composer install --no-interaction --no-progress

      - name: Coding standard
        run: vendor/bin/phpcs --standard=Magento2 app/code

      - name: PHPStan
        run: vendor/bin/phpstan analyse -c phpstan.neon

      - name: Unit tests
        run: vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist app/code

Rode o coding standard e o PHPStan em app/code e nos seus próprios pacotes Composer, não em todo o vendor. Verificar código do core gera milhares de mensagens que ninguém vai tratar. O setup de cada ferramenta está em o artigo sobre PHPCS, o artigo sobre PHPStan e no artigo sobre PHPUnit deste blog.

O arquivo de configuração de testes unitários que acompanha o Magento é dev/tests/unit/phpunit.xml.dist. Muitos projetos mantêm uma cópia própria que lista só o código customizado, o que é mais rápido e evita rodar testes do core.

Build sem banco de dados

Dois comandos produzem quase tudo que um Magento de produção precisa: setup:di:compile e setup:static-content:deploy. Nenhum deles precisa de banco, desde que a configuração certa esteja no repositório.

O setup:di:compile lê a lista de módulos de app/etc/config.php. O static content deploy também precisa saber quais temas e locales existem. Sem banco ele obtém isso também do config.php, depois que você exporta uma vez a partir de um ambiente funcionando:

bin/magento app:config:dump scopes themes

Isso escreve websites, stores e temas no config.php. Faça commit do resultado. A partir daí, o build pode rodar em qualquer lugar. O artigo sobre env.php e config.php explica por que isso pertence ao config.php e não ao env.php.

O job de build

  build:
    needs: checks
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    env:
      COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: bcmath, ctype, curl, dom, gd, intl, mbstring, pdo_mysql, soap, sockets, sodium, xsl, zip
          tools: composer:v2
          coverage: none

      - run: composer install --no-dev --no-interaction --no-progress --optimize-autoloader

      - run: bin/magento setup:di:compile

      - run: bin/magento setup:static-content:deploy en_US pt_BR -f --jobs=4

      - run: composer dump-autoload --no-dev --optimize --apcu

      - name: Package release
        run: |
          tar -czf release.tar.gz \
            --exclude=.git \
            --exclude=var \
            --exclude=pub/media \
            --exclude=app/etc/env.php \
            .

      - uses: actions/upload-artifact@v4
        with:
          name: release
          path: release.tar.gz

Liste os locales que as suas lojas realmente usam. A flag -f é necessária quando o runner não está em production mode, o que é o caso, já que não há env.php. Remova --apcu se o seu PHP-FPM não tem APCu habilitado. O static content deploy costuma ser o passo mais lento do build, então faça o deploy só dos temas que você usa com --theme se o projeto tiver temas sem uso.

O job de deploy

No servidor, use uma estrutura de diretórios com releases e arquivos compartilhados:

/var/www/shop/
  releases/
    20261110120000/
    20261109153000/
  shared/
    app/etc/env.php
    pub/media/
    var/
  current -> releases/20261110120000

O document root do web server aponta para current/pub. Cada deploy descompacta em uma pasta nova, faz o link dos arquivos compartilhados e só então move o symlink current. O job:

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: release

      - uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}

      - name: Upload and activate
        env:
          HOST: ${{ secrets.DEPLOY_HOST }}
        run: |
          RELEASE=$(date +%Y%m%d%H%M%S)
          scp -o StrictHostKeyChecking=accept-new release.tar.gz deploy@$HOST:/tmp/release.tar.gz
          ssh deploy@$HOST "bash /var/www/shop/deploy.sh $RELEASE"

Mantenha a lógica do lado do servidor em um script versionado em vez de um comando SSH inline longo. Um deploy.sh mínimo:

#!/usr/bin/env bash
set -euo pipefail

BASE=/var/www/shop
REL="$BASE/releases/$1"

mkdir -p "$REL"
tar -xzf /tmp/release.tar.gz -C "$REL"
rm /tmp/release.tar.gz

ln -sfn "$BASE/shared/app/etc/env.php" "$REL/app/etc/env.php"
ln -sfn "$BASE/shared/pub/media" "$REL/pub/media"
ln -sfn "$BASE/shared/var" "$REL/var"

cd "$REL"
if ! php bin/magento setup:db:status; then
    php bin/magento maintenance:enable
    php bin/magento setup:upgrade --keep-generated
fi

ln -sfn "$REL" "$BASE/current"
php bin/magento maintenance:disable || true
php bin/magento cache:flush

ls -1dt "$BASE"/releases/* | tail -n +6 | xargs -r rm -rf

O setup:db:status retorna um exit code diferente de zero quando há schema ou data patches pendentes. Só então o script habilita a manutenção e roda o setup:upgrade. A flag --keep-generated mantém o código compilado do build. Em deploys sem mudanças de banco não há janela de manutenção nenhuma.

Dois detalhes passam batido com facilidade. Primeiro, o OPcache do PHP pode continuar servindo arquivos antigos depois que o symlink move. Recarregue o PHP-FPM ou use configurações de OPcache que respeitem o realpath, para que o novo release seja pego. Segundo, uma pasta var compartilhada significa que cache e metadados gerados sobrevivem entre releases, então limpe os caches depois da troca, como acima. Se você usa Varnish, purgue ele também.

Uma discussão mais profunda sobre maintenance mode, mudanças de banco seguras para rodar antes da troca e rollback está em deploys sem downtime no Magento 2.

Cron e queue consumers

As entradas de cron e os gerenciadores de processo precisam apontar para current, nunca para uma pasta de release específica. Senão eles continuam rodando código antigo depois da troca, ou falham quando o release antigo é apagado. Use uma linha de crontab que rode /var/www/shop/current/bin/magento cron:run e configure os consumers do supervisor ou do systemd do mesmo jeito.

Queue consumers de longa duração também seguram o código antigo em memória. Depois de um deploy, reinicie-os para que carreguem o novo release. Se os consumers são iniciados pelo cron com um limite de mensagens, eles vão sair e reiniciar sozinhos, mas um restart no script de deploy deixa o momento previsível. O mesmo vale para qualquer worker PHP que fique vivo entre requests.

O que adicionar depois

Quando o pipeline básico estiver estável, é fácil crescer. Testes de integração podem rodar em um job com MySQL e OpenSearch como service containers. Um smoke test depois do deploy pode requisitar a home, uma página de categoria e o carrinho e falhar o job se algum retornar erro. Um passo de notificação pode postar o nome do release e o commit no chat do time. Adicione isso um de cada vez e mantenha o pipeline rápido o bastante para que as pessoas não comecem a pulá-lo.

Fazendo rollback

Como os releases antigos ficam no disco, um rollback é mover o symlink de volta e limpar os caches. Isso só funciona direito se as mudanças de banco do release ruim eram retrocompatíveis. Quando um release adiciona colunas ou tabelas, o código antigo geralmente as ignora. Quando ele remove ou renomeia, o código antigo quebra. Planeje mudanças de schema em duas etapas quando possível: adicione primeiro, remova depois em um release separado.

Secrets e ambientes

  • Use GitHub environments com required reviewers para produção, para que um merge não faça deploy sem alguém apertar um botão.
  • Dê à chave SSH de deploy acesso a um usuário que só consiga escrever na pasta da loja e recarregar o PHP-FPM.
  • Mantenha o env.php só no servidor. O pipeline nunca precisa de senhas de banco nem de chaves de criptografia.
  • Rode o mesmo workflow primeiro contra um ambiente de staging, com seus próprios secrets.

FAQ

O setup:di:compile precisa de conexão com o banco?

Não. Ele precisa do código e do app/etc/config.php com a lista de módulos. Por isso consegue rodar no CI. O static content deploy também funciona sem banco se temas e scopes forem exportados para o config.php com app:config:dump.

Como dou ao GitHub Actions acesso ao repo.magento.com?

Guarde um JSON no estilo do auth.json com suas chaves pública e privada do Marketplace como secret do repositório e passe ao Composer na variável de ambiente COMPOSER_AUTH. Não faça commit do auth.json.

O pipeline deve rodar setup:upgrade em todo deploy?

Só quando for necessário. O setup:db:status diz se há schema ou data patches pendentes. Se nada mudou, você pode pular o maintenance mode e o setup:upgrade por completo.

Posso fazer o build no servidor de produção?

Pode, mas ele disputa CPU com o tráfego real e deixa o site quebrado se o build falhar no meio. Fazer o build no CI e enviar um release pronto evita os dois problemas.

Atendo como freelancer em Magento e Adobe Commerce, incluindo pipelines de build e deploy. Se precisar de ajuda com isso, fale comigo.

Artigos relacionados