Pipeline de CI/CD do Magento 2 com GitHub Actions
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:
- Checks em todo push e pull request: validação do Composer, coding standard, PHPStan e testes unitários.
- 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.
- 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.phpsó 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
Magento 2 em production mode: como mudar e fazer o deploy
O que o production mode do Magento 2 muda, como trocar com segurança e uma sequência de deploy para o 2.4.8 que tira compilação e static deploy do downtime.
Deploy do Magento 2 sem downtime em um único servidor
Um deploy sem downtime para o Magento 2.4 em um servidor: build numa pasta de release, compile fora do live, troca de symlink e rollback em segundos.
Ambiente local do Magento 2 com Docker e Warden
Monte um ambiente local do Magento 2.4.8 com Docker e Warden: env-init, o arquivo .env, instalar ou importar um projeto, Xdebug e os comandos do dia a dia.
Backup e restore no Magento 2: o que salvar e como
Como fazer backup e restore de uma loja Magento 2: dumps de banco que importam sem erro, mídia, env.php e a chave de criptografia, cópias offsite e restores testados.