Arrastar temas por FTP em 2026 costuma acabar igual: falta um ficheiro de inc/, o composer.lock veio de outro portátil, e a produção cai porque alguém sobrescreveu o wp-config.php. Continuous Integration e Continuous Deployment não são teatro de enterprise - são a forma de cada alteração no Git passar pelo mesmo build, pelos mesmos testes e pelo mesmo caminho até ao servidor.
Este guia é para WordPress com tema ou plugin no repositório (Composer, npm, eventualmente Docker), não para sites que só vivem no painel. Se o código custom ainda só existe na base de dados e nos media, leve temas e código próprio para o Git primeiro - sem isso, o CI/CD não tem o que construir.
Documentação dos runners: GitHub Actions. Em paralelo: Hardening WordPress. Para comandos pós-deploy: WP-CLI.
Porque não fazer deploy do WordPress por FTP
Três problemas voltam em lojas WooCommerce portuguesas e em entregas de agência em Lisboa ou Porto:
- Deriva de ambiente - localmente PHP 8.2 e Composer 2.7; no VPS PHP 8.1 sem
intl. Ocomposer installlocal «funciona»; a produção morre com class not found. - Sem rasto de auditoria - depois de um incidente não se sabe qual commit estava live. O painel do hosting mostra a data do ficheiro, não o SHA do Git.
- Segredos no disco - passwords SFTP no FileZilla, ou um
.envcommitado «só um momento», continuam a ser o caminho clássico de fuga - e caro sob o RGPD.
O CI/CD resolve isto mecanicamente: build numa imagem conhecida (shivammathur/setup-php ou Docker próprio), artefacto versionado por SHA, deploy por SSH com chave em Secrets.
Pipeline CI/CD WordPress com GitHub Actions
Fluxo típico para tema custom mais plugins via Composer:
- Trigger - push para
mainou merge de pull request revisto. Odevelopcostuma ir só para staging. - Build -
composer install --no-dev --optimize-autoloader,npm ci,npm run build. Fixe PHP e Node no workflow (php-version: '8.3',node-version: '24'). - Test - PHPUnit para plugins, eventualmente Playwright no checkout. Fail = paragem, zero deploy.
- Artefacto - empacote a release sem
.git, semnode_modules, comvendor/eassets/dist/compilado. - Deploy - rsync/SCP para
releases/<run_id>/, depois troca atómica do symlink.
Fixe versões que ainda recebem correções. O PHP 8.2 recebe correções de segurança só até 31 de dezembro de 2026, o PHP 8.3 até 31 de dezembro de 2027 (versões suportadas do PHP). O Node.js 20 chegou ao fim de vida a 30 de abril de 2026; o Node.js 22 é suportado até 30 de abril de 2027 e o Node.js 24 até 30 de abril de 2028 (calendário de versões do Node.js). A versão minor do PHP acompanha a produção, não a versão mais recente.
Esboço curto em GitHub Actions, com tags de versão principal para facilitar a leitura; a secção sobre reforço do workflow, mais abaixo, explica porque os workflows de produção fixam SHAs de commit:
name: deploy-production
on:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --no-dev --optimize-autoloader
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: npm
- run: npm ci && npm run build
- name: rsync release
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
# gravar chave, rsync para releases/$GITHUB_SHA, ln -sfnO script completo do symlink fica no repo como bin/release.sh - o workflow só o chama. O mesmo script funciona a partir do portátil se o Actions estiver em baixo.
Impedir dois deploys WordPress em simultâneo no GitHub Actions
Dois merges rápidos para main arrancam duas execuções do workflow. Sem bloqueio, ambas fazem rsync para releases/ e trocam current numa ordem que ninguém escolheu. Um grupo concurrency no GitHub Actions permite no máximo um workflow em execução por grupo; uma execução recém-colocada em fila espera como pending (documentação GitHub: controlar a concorrência).
Para deploys, contam dois detalhes. Por omissão, um grupo mantém uma execução ativa e uma pendente, e uma execução mais recente cancela a pendente mais antiga: ganha o último commit e o do meio é ignorado. cancel-in-progress: true cancelaria também o job em curso, o que pode parar o rsync a meio de um diretório de release. Mantenha false em produção e use true em workflows de teste em pull requests, onde só conta o commit mais recente.
concurrency:
group: deploy-production
cancel-in-progress: falseComo o symlink só troca depois de um rsync completo, uma execução pendente cancelada nunca toca em current.
Proteger a produção com ambientes do GitHub Actions
Acrescente environment: production ao job de deploy e configure o ambiente nas definições do repositório. Passam a valer três proteções (documentação GitHub: deployments e ambientes):
- Revisores obrigatórios: até seis utilizadores ou equipas, e uma aprovação liberta o job. Com o bloqueio de autoaprovação ativo, quem iniciou o deploy não o pode aprovar.
- Branches e tags de deploy: só
mainou tags de release podem fazer deploy paraproduction, por isso um workflow num feature branch não chega ao servidor de produção. - Segredos de ambiente: disponíveis só para jobs que referenciam o ambiente e, com revisores obrigatórios, só depois da aprovação. O
DEPLOY_SSH_KEYde produção fica aqui, não nos segredos do repositório.
Confirme o plano antes de contar com isto. No GitHub Free, Pro e Team, os revisores obrigatórios e os tempos de espera só funcionam em repositórios públicos. Branches de deploy e segredos de ambiente funcionam em repositórios privados no Pro e no Team, mas não no Free. Os repositórios WordPress de agências são quase sempre privados, por isso nesses planos não há porta de aprovação e são as regras de branch que garantem a proteção.
Reforçar o workflow de deploy no GitHub Actions
O job de deploy guarda uma chave SSH para a produção, por isso o ficheiro do workflow merece a mesma revisão que o código PHP. A referência do GitHub sobre uso seguro enumera as medidas que aqui importam:
- Fixe actions de terceiros no SHA completo do commit. Uma tag como
@v2pode ser movida ou apagada. Redirecionar um SHA exigiria uma colisão SHA-1. Tire o SHA do repositório da própria action, não de um fork. O GitHub aponta agora um lockfile de dependências do workflow como a opção mais forte para os repositórios que o adotem. - Dê ao
GITHUB_TOKENo mínimo. Definapermissions: contents: readno topo e aumente por job só onde um passo precisa de mais. Um deploy por rsync e SSH só precisa de ler o repositório. - Mantenha input não confiável fora de
run:. Um título de pull request ou nome de branch dentro de${{ }}num script shell abre caminho a injeção de código. Passe o valor por uma variável de ambiente intermédia. - Um segredo por valor. Dados estruturados, como um bloco JSON, podem quebrar a ocultação nos logs, por isso guarde host, utilizador, caminho e chave como segredos separados.
A mesma página nota que a ocultação só abrange segredos usados no job e acessíveis ao runner, por isso nunca faça echo de valores derivados, como uma chave descodificada.
Deploy do WordPress sem downtime com symlinks
Sobrescrever ficheiros «no sítio» em public_html significa que durante alguns segundos metade dos pedidos vê um functions.php antigo com um template novo. No WooCommerce, numa campanha de Natal ou Black Friday, isso pode corromper sessões de carrinho.
Layout atómico:
- base:
/var/www/site/ - releases:
/var/www/site/releases/20260920-142211/ - shared: uploads,
wp-config.php, object cache - fora da release current: symlink para a release ativa
Após rsync bem sucedido:
ln -sfn /var/www/site/releases/20260920-142211 /var/www/site/currentO root do Nginx aponta para .../current/public (ou equivalente). A troca é imediata. Apague releases antigas após N dias, ou guarde as três últimas para rollback.
Os uploads do WordPress têm de viver fora da release - caso contrário cada deploy apaga media ou copia gigabytes. Padrão clássico: shared/uploads ligado a wp-content/uploads em cada release.
Staging WordPress e estratégias de deploy comparadas
Em 2026, o caminho de um feature branch por pull request e staging até main e produção não é opcional para lojas e páginas de captura. O staging deve ter:
- a mesma versão de PHP e as mesmas extensões que a produção
- uma cópia da base (anonimizada quando há dados pessoais sob o RGPD)
- segredos de pagamento só em modo sandbox
URLs de preview por PR ajudam em redesign e CSS; migrações de esquema continuam a precisar de staging com dump real.
| Estratégia | Risco | Custo de manutenção | Quando faz sentido |
|---|---|---|---|
| FTP manual | Muito alto | Arranque barato, falhas caras | Só sandbox / aprendizagem |
| Git hook no servidor | Médio | Baixo | Portfólio pequeno, um programador |
| CI + rsync atómico | Baixo | Médio | Negócio, WooCommerce, agências |
| Blue/green ou dois VPS | Mínimo | Alto | Muito tráfego, SLA, pagamentos |
Blue/green (dois ambientes completos, troca no load balancer) raramente compensa abaixo de centenas de sessões em paralelo. A maioria das lojas PT-PT fica bem com symlink atómico e rollback de diretório - em VPS (Hetzner, DigitalOcean) ou hosting gerido com SSH.
Testes automáticos num pipeline CI/CD de WordPress
Um pipeline sem testes só envia código partido mais depressa. Quatro camadas servem um repositório WordPress, da mais rápida à mais lenta:
- Testes unitários com PHPUnit verificam funções isoladas de um plugin ou tema sem base de dados. Correm a cada push.
- Testes de integração carregam o WordPress contra uma base de dados de teste temporária.
wp scaffold plugin-testsgera a estrutura:phpunit.xml.dist,bin/install-wp-tests.shpara a suite de testes e a base de dados,tests/bootstrap.php, um teste de exemplo e um conjunto de regras.phpcs.xml.dist. O ficheiro de CI por omissão é para CircleCI;--ci=githubescreve antes uma configuração para GitHub Actions (WP-CLI: wp scaffold plugin-tests). - Análise estática com PHPStan e PHP_CodeSniffer apanha erros de tipos e violações da norma de código sem executar o código.
- Testes end-to-end com Playwright percorrem fluxos reais: formulário de contacto, carrinho e checkout WooCommerce. São a camada mais lenta, por isso corra-os em pull requests para
maine contra o staging, não a cada commit.
Um job a vermelho em qualquer camada para o workflow antes do passo do artefacto, e nada chega ao servidor.
Segredos e migrações de base de dados no deploy WordPress
Nunca faça commit de:
- chaves SSH
- passwords da base de dados
AUTH_KEY/SECURE_AUTH_KEYdowp-config.php- tokens de API de gateways (MB Way, Stripe, PayPal)
No GitHub ficam em Settings, depois Secrets and variables, depois Actions. Injete-os como env no passo de deploy. No servidor, o wp-config.php vive em shared/ e não faz parte do artefacto de CI.
WP-CLI depois da troca do symlink:
wp cache flushse houver object cachewp rewrite flushapós alterações de CPTwp plugin listcomo linha de smoke no log
Correr o WP-CLI no servidor de produção a partir do runner
Estes comandos têm de correr no servidor, contra a nova release. O WP-CLI faz isso a partir do runner com o parâmetro global --ssh ([<scheme>:][<user>@]<host>[:<port>][<path>]) ou com aliases no wp-cli.yml do projeto (manual do WP-CLI: correr comandos remotamente):
@staging:
ssh: [email protected]/var/www/site/current/public
@prod:
ssh: [email protected]/var/www/site/current/publicCom esse ficheiro no repositório, wp @prod cache flush corre em produção. O WP-CLI tem na mesma de estar instalado no servidor e acessível como wp: o runner apenas passa o comando por SSH.
Outros comandos que vale a pena pôr no passo a seguir à troca, todos na referência de comandos do WP-CLI:
wp core is-installednão escreve nada e responde pelo código de saída, por isso uma verificação falhada faz falhar o passo.wp core update-db --dry-runcompara as versões da base de dados antes dowp core update-dbreal após uma atualização do core.wp core verify-checksumscompara os ficheiros do core com os checksums do WordPress.org e apanha ficheiros alterados fora do Git.wp maintenance-mode activateedeactivatecobrem a release rara que tem de alterar o esquema enquanto nada escreve nele.
Planeie migrações de base de dados à parte do deploy de ficheiros. O rollback do symlink não desfaz ALTER TABLE. Em migrações destrutivas: backup primeiro (wp db export em staging e produção), depois migração, depois tráfego para o código que precisa do esquema - ou feature flag em PHP.
Dependências PHP: Composer install - em CI sempre --no-dev para produção e composer.lock bloqueado.
Erros comuns num pipeline DevOps WordPress
Da prática em VPS (Hetzner, DigitalOcean, OVH) e hosting gerido com SSH:
.gitignorecome ovendor/- a produção recebe o tema sem autoloader. Ou constrói o vendor no runner e mete-o no artefacto, ou corre Composer no servidor após o rsync (aí o servidor precisa de Composer e acesso ao Packagist).- Caminhos errados - o script assume
/var/www/htmle o hosting usa/home/user/domains/.... Fixe caminhos em Secrets por ambiente. - Opcache mantém código antigo - após
ln -sfn, faça reload do PHP-FPM ou chameopcache_resetvia WP-CLI/mu-plugin só para o utilizador de deploy. - Cron e filas - o Action Scheduler do WooCommerce pode correr jobs com código antigo durante alguns minutos; após releases grandes, observe failed jobs um pouco.
Smoke test pós-deploy (curl a partir do runner ou job separado):
- homepage HTTP 200
/wp-login.php200- um URL crítico de produto ou formulário
- HEAD opcional ao CDN se os assets tiverem hash no nome
Se o smoke falhar, o rollback automático do symlink custa menos do que o telefonema do cliente às 23:00.
Temas de blocos e must-use plugins no Git
Nem todo o WordPress cabe no mesmo pipeline. Conteúdo na base (artigos, grupos ACF sem export JSON, CSS do Elementor) continua fora da release. O CI/CD protege código; não substitui backups da base.
Divisões práticas:
- Tema clássico ou híbrido com Vite - build completo em CI, artefacto com
style.css+dist/. - Tema de blocos (FSE) - mantenha
theme.jsone templates HTML no Git; alterações do Site Editor voltam por PR, senão produção e Git divergem após o primeiro «Guardar». - Must-use plugins - vão com a release; não os atualize pelo painel. Bom sítio para healthchecks de deploy e para desativar o editor de ficheiros.
- Plugins de wordpress.org - fixe versões via Composer (
wpackagist) ou atualize-os num job revisto. Misturar «Update no painel» com «CI sobrescreveuplugins/» produz uma árvore aleatória.
Stacks de agência em PT misturam muitas vezes tema custom, WooCommerce via wpackagist e dois zips premium instalados por path repository no Composer. O pipeline tem de conhecer as três fontes, senão staging e produção diferem por um ZIP de há seis meses.
Checklist para deploy contínuo do WordPress
Antes de ligar o main à produção, passe esta lista uma vez - poupa um fim de semana:
- Repositório privado, ou confirme que o histórico não tem segredos (
git log -pnão mostra.envantigos). - Proteção de branch: PR obrigatório, workflow
testverde obrigatório. - Secrets separados:
DEPLOY_HOST_STAGING,DEPLOY_HOST_PROD, chaves SSH distintas. - Staging com a mesma versão de PHP que a produção (não «nós 8.3, o cliente 8.1»).
- Backup da base antes da primeira troca atómica - mesmo com rollback de código pronto.
- Monitorização de uptime (UptimeRobot, Better Stack ou equivalente) com alerta SMS/Slack no URL de produção.
- Runbook de uma página: como fazer
ln -sfnmanual para a release anterior, quem tem acesso ao servidor.
Só depois ative auto-deploy a partir de main. Até lá, «build + test» em PRs e workflow_dispatch manual para staging chega.
Resumo
CI/CD para WordPress não é «YAML mágico». É um contrato repetível: o mesmo PHP, o mesmo lockfile, o mesmo artefacto, uma troca sem janela de ficheiros a meio. Começa com Git e staging, acrescenta Actions para builds Composer/npm, termina com current atómico e smoke test. O FTP fica ferramenta de emergência, não o processo.
Se precisa de um programador WordPress que mantenha esse pipeline junto do código do tema para loja ou página de captura, a WPPoland trabalha no stack real - não em slides.







