Publicar WordPress na mão funciona até o dia em que não funciona. Automatizei o deploy de um WordPress numa VPS — branch, pull request, merge publica sozinho — e as primeiras semanas do pipeline me ensinaram quatro lições que só aparecem quando os arquivos do servidor pertencem a outro dono. Nenhum tutorial de rsync com GitHub Actions avisa disso, porque todos partem do pressuposto de que o usuário do deploy já é dono de tudo. Na prática não é: quem publica o WordPress é o usuário do servidor web, e o usuário de deploy só participa do grupo dele. É exatamente aí que mora cada um dos sustos abaixo.
Lição 1 — Backup antes de sobrescrever, no lugar certo
A pipeline compacta tema e plugin no servidor antes de rodar o rsync, e guarda os 10 backups mais recentes — se o deploy sair errado, dá para voltar em segundos. No primeiro deploy real, antes de qualquer rsync, o próprio backup já falhou:
/var/www pertence ao usuário do servidor web, não ao usuário de deploy. Daria para resolver liberando escrita ao deploy dentro de /var/www, mas isso amplia o alcance da chave do CI sem necessidade — se o token do pipeline vazar, o atacante ganha escrita onde nunca precisou escrever. O backup foi para a pasta pessoal do usuário de deploy, que ele já é dono:
Lição 2 — O -a do rsync quebra quando os arquivos são de outro dono
Arquivos de produção pertencem a www-data; o usuário deploy só participa do grupo. Ele escreve no diretório, mas não pode alterar dono, grupo ou permissão de um arquivo que não é dele. E foi exatamente isso que o -a do rsync tentou fazer em cada arquivo:
-a é atalho para -rlptgoD: recursivo, links, permissões, tempos (mtime), grupo, dono e arquivos de dispositivo. Os quatro últimos — t, g, o e parte do p — exigem que o rsync seja dono do arquivo remoto, e ele não é. A correção é parar de pedir o que não é preciso:
Sem p/t/g/o, o rsync só precisa de escrita no diretório — que o grupo já tem. --checksum compara pelo conteúdo do arquivo em vez do mtime (perdido junto com o -t): sem ele, cada deploy retransmitiria tudo de novo, porque o mtime remoto nunca bate com o local. Dono e permissão corretos entram depois, num passo à parte com sudo pontual — chown/chmod liberados no sudoers só para esse caminho.
Lição 3 — O chmod do deploy anterior não pode quebrar o próximo
Deploy N passou limpo. Deploy N+1 falhou assim:
O rsync escreve num arquivo temporário e troca o nome no final (mkstemp + rename) — por isso só precisa de escrita no diretório, não no arquivo. O problema era o passo final do deploy anterior: um chmod 755 nos diretórios, que tira a escrita do grupo. Cada deploy bem-sucedido quebrava o seguinte. A correção fica antes do rsync e se repete no final, para o estado nunca regredir:
2775 é escrita de grupo mais o bit setgid — todo arquivo novo criado ali nasce do mesmo grupo do diretório, sem depender do umask de quem criou. Arquivo continua 644: quem precisa de escrita é o diretório, não o arquivo em si.
Lição 4 — O pipeline também vigia o que entra no repo
As três lições acima são sobre o servidor. A quarta é sobre o repositório: todo PR que toca tema ou plugin passa por checagem antes do merge, não só depois.
php -l roda em todo arquivo PHP do tema e do plugin — erro de sintaxe bloqueia o merge, não só o deploy. O PHPCS roda também, mas informativo: comenta no PR e não trava nada. Se o diff tentar subir wp-config.php, uma .pem, .sql, .log ou .htaccess, o pipeline recusa o merge — arquivo desses no repositório não é código, é vazamento esperando data. O deploy só dispara quando o PR mergeado tocou tema ou plugin; mudança em documentação não publica nada.
O fluxo completo
Junto, as quatro lições viram este fluxo, do merge ao ar: