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 o FTP e o «só carregar» falham em auditorias
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 mínimo: push, build, test, deploy
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.2',node-version: '20'). - 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.
Esboço curto em GitHub Actions:
name: deploy-production
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-dev --optimize-autoloader
- uses: actions/setup-node@v4
with:
node-version: '20'
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.
Deploys atómicos em vez de sobrescrever ficheiros
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, previews e matriz de estratégias
Em 2026, feature branch → PR → staging → main/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.
Segredos, WP-CLI e base de dados
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: Settings → Secrets and variables → 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
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.
O que costuma partir o primeiro pipeline 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, must-use plugins e exceções ao Git «completo»
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 antes do primeiro deploy com Actions
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.







