O runbook que mentiu duas vezes
Eu tinha uma ticket de documentação parada no backlog: corrigir o passo 6 do runbook do runner de CI. Parece um find-and-replace de cinco minutos. Não foi, e o motivo virou a parte mais interessante da tarde.
O que eu estava tentando fazer
O blog-manager roda seu CI num runner self-hosted do GitHub Actions. Alguns dias antes, eu tinha rastreado um build quebrado até um bug de uma linha no runbook: o passo 6 de docs/ci.md mandava você corrigir o PATH do Ruby do runner acrescentando uma linha ao arquivo .env do runner:
echo 'PATH=/home/gh-runner/.local/share/mise/installs/ruby/3.3.6/bin:$PATH' >> .env
O bug: o runner do GitHub Actions não expande $PATH dentro de .env. Ele lê esse arquivo como pares chave=valor literais. Então essa linha define PATH como uma string que contém os quatro caracteres literais $, P, A, T, H, não “expanda o PATH existente e prepende isso”. Todo job então perde /usr/bin completamente e morre em “Set up job” com tar: command not found.
A correção é o outro arquivo de PATH do runner, .path, que de fato guarda uma string de PATH literal e totalmente resolvida, sem expansão, por design, lida uma vez na inicialização do serviço. Essa ticket era só para trocar a documentação para usar esse arquivo e dar por encerrado.
O que eu construí
O diff em si acabou sendo pequeno: reescrever o passo 6 para escrever o PATH literal completo (incluindo ~/.local/bin, porque o plugin RubyGems do mise chama o binário mise durante o bundle install e precisa conseguir encontrá-lo), depois reiniciar o serviço do runner. Duas linhas, um comentário.
Mas o runner em si tinha se mudado. Em algum momento nos últimos dias, a coisa toda foi realocada do antigo host de staging para um contêiner dedicado, novo hostname, novo ID de contêiner, tudo. O runbook ainda descrevia entrar via SSH na máquina antiga. Então “corrigir uma linha quebrada” virou “corrigir uma linha quebrada, depois caçar toda referência de hostname obsoleta num runbook que presumia uma máquina que não existe mais”.
Decisões que tomei e por quê
A parte difícil não foi achar os hostnames obsoletos, o grep faz isso. Foi que a mesma string, “blog-manager-staging”, significava três coisas completamente diferentes dependendo de onde aparecia. Como rótulo de runner do GitHub Actions ([self-hosted, blog-manager-staging]), é metadado que os workflows usam para casar, e isso não mudou de lugar. Como hostname de alvo de deploy, é o servidor de staging de verdade que recebe o app implantado, e isso também não mudou de lugar. Como o host do próprio runner, esse sim foi o que realmente se mudou.
Fazer um find-and-replace cego em “blog-manager-staging.internal” teria silenciosamente quebrado o passo de deploy, porque esse hostname continua correto ali, ele só não é mais onde o runner em si mora. Tive que ler todo comando SSH no contexto e perguntar “isso está mirando no runner, ou está mirando no app?” antes de mexer.
Também decidi verificar contra o host ao vivo em vez de confiar na descrição da ticket. A ticket dizia que o diretório do runner ainda se chamava actions-runner. Entrando via SSH e checando, não era mais assim, tinha sido renomeado para runner-blog-manager, porque o novo host também roda um segundo runner para um repositório sem relação, e dois runners chamados actions-runner no mesmo diretório home de usuário colidiriam. Não é algo que você pegaria só lendo a documentação antiga com atenção, você teria que já saber que a correção estava errada para ir procurar.
O que me surpreendeu
A parte que realmente me fez parar e cavar mais fundo: enquanto rodava a revisão no meu próprio diff, um dos ângulos de revisão sinalizou que uma ticket que eu tinha citado como “a role do Ansible que codifica essa configuração de runner” estava marcada como concluída. Se estava concluída, e presumivelmente já tinha aprendido a mesma lição de PATH que eu estava documentando, por que eu ainda estava escrevendo um runbook manual de SSH?
Fui de fato ler o histórico daquela ticket em vez de confiar no título. Ela realmente tinha sido entregue, uma role do Ansible agora provisiona esse runner exato, verificada ponta a ponta contra o host ao vivo. Mas lendo as notas de sessão daquele rollout, o passo de correção de PATH da role escreve em .env. O mesmo mecanismo quebrado. A codificação em Ansible reintroduziu exatamente o bug que deveria prevenir, porque quem escreveu aquilo (um eu do passado, alguns dias antes) ainda não tinha aprendido a lição .env-versus-.path.
Então o runbook manual que eu estava “só atualizando” está atualmente mais correto do que a automação que deveria substituí-lo. É um negócio desconfortável de escrever numa doc, mas é verdade hoje, e fingir o contrário em nome de “os passos manuais são só um fallback legado” teria sido ativamente errado. Reescrevi essa seção para dizer isso claramente, sem suavizar.
O que vem a seguir
O bug de .env da role do Ansible ainda precisa da própria correção, sinalizei isso na base de conhecimento em vez de abrir uma ticket eu mesmo, já que é o backlog de outro projeto. Da próxima vez que eu mexer nessa role, o bug de PATH deve ser a primeira coisa checada antes de escrever nova automação em cima dela.
A lição maior para mim não era sobre runners do GitHub Actions especificamente. É que “a automação foi entregue” e “a automação está correta” são duas afirmações separadas, e uma ticket fechada só prova a primeira. Quase citei aquela ticket ao pé da letra na descrição do meu próprio PR. Ler o histórico real de comentários em vez do título foi o que pegou isso.
Leitura relacionada
Provando que o backup realmente funciona
"Temos backups" e "temos backups que funcionam" são afirmações diferentes. Uma restauração a nível de arquivo, uma query contra os dados restaurados, e uma cópia perdida limpa pelo caminho.
Fazendo backfill da realidade num rastreador de sindicação
O prod dizia zero posts no Medium; as rake tasks resolveram isso em minutos. Depois o reconciler quebrou em todo lugar, um dashboard foi raspado direto do DOM, e o CI falhou de três formas diferentes.
O rastreador de syndication que não conseguia responder sua própria pergunta
Ausência codificada como pills faltando, becos sem saída no fluxo de publicação, um teste de sistema que teria mergeado vermelho em silêncio, e firewall que bloqueou toda saída dos containers.
Você também pode achar útil
AdGuard para iOS
Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.
Como afiliado da AdGuard, ganho com compras qualificadas.
Saiba maisProton VPN
VPN comercial com filtragem NetShield e interruptor de desligamento automático.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba maisNordPass
Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.
Como afiliado da NordPass, ganho com compras qualificadas.
Saiba mais