Um playbook de falhas de CI para um projeto Rails de uma pessoa só
Eu vinha tratando o CI do blog-manager como um lembrete automático pessoal: check verde ou conserta. Isso funciona bem até que algo quebra na main e eu tenho que lembrar quais são minhas próprias regras. Então, no início de maio, sentei para escrever as regras em docs/ci.md, um playbook de falhas de CI para um projeto onde eu sou a equipe inteira. Isso é um retrato daquele dia; o playbook cresceu desde então.
Três coisas precisavam estar ali:
- O que o CI de fato roda.
- O que eu faço quando ele fica vermelho.
- O que “merge bloqueado” significa sem proteção de branch.
Esse terceiro item foi a surpresa.
O plano, antes de eu começar
Um issue do Plane listava cinco decisões a registrar: proteção de branch, política de flakes, notificações, reprodução local, regra de rollback. Eu presumi que proteção de branch seria a fácil. Ativar o toggle, exigir CI verde na main, pronto. O GitHub discordou.
Upgrade to GitHub Pro or make this repository public to enable this feature.
O GitHub não impõe status checks obrigatórios em repositórios privados de contas pessoais a menos que você pague pelo Pro. O repositório é privado (tem uma senha de registry do Gitea e alguns tokens de API em fixtures semeadas), e eu não vou tornar ele público por uma feature de $4 por mês. Então proteção de branch entrou no playbook como adiada, com justificativa:
Status da proteção de branch: adiado. O GitHub Pro é necessário para impor status checks obrigatórios em repositórios privados de contas pessoais. O custo não se justifica para um projeto solo. Revisitar se o repositório se mudar para uma organização do GitHub ou ficar público.
Isso significa que o gate de merge é auto-imposto. A regra do CLAUDE.md (nunca dar merge num PR vermelho) e uma nota de cinco linhas em docs/ci.md são o gate. É um comentário estrutural, mas é preciso. Sou o único que pode quebrar essa regra, e o único que tem que conviver com a quebra.
Flakes: pular com issue registrado, sem loops de retry
Hábito antigo: rodar de novo um job instável até ficar verde, e seguir em frente. A regra nova é um retry manual. Se ainda falhar, coloca em quarentena.
def test_something_flaky
skip "flaky - see #123"
# ...
end
O truque é deixar o skip visível. Um skip com uma tag de issue aparece na saída do teste e no quadro de issues. Um loop de re-execução esconde o problema no histórico de execuções que ninguém checa. Prefiro um amarelo óbvio a um verde invisível.
Quase enviei uma versão onde o skip ficava no nível da classe, antes do def. Isso levanta NoMethodError em vez de pular, porque skip só funciona dentro do corpo de um teste. A revisão de código pegou isso. Vale mencionar porque é uma pegadinha fácil.
A regra de rollback que reescrevi no mesmo dia
Minha primeira versão do playbook tinha uma regra simples: se a main estiver vermelha e uma correção limpa não estiver pronta em uma hora, reverte o commit culpado. git revert <sha>, push, merge, corrige direito numa branch. Simples.
Aí reli e estava errada para esse projeto. blog-manager ainda não está em produção. Não há usuários para proteger de uma main vermelha. A maioria das minhas quebras é infraestrutura pela metade: um runner que não está registrado, uma dependência faltando, um workflow que ainda estou conectando. Reverter isso esconde o problema em vez de resolvê-lo. Para um app solo pré-produção, o padrão honesto é consertar para frente.
Então reescrevi a seção algumas horas depois. A regra de reverter em uma hora ainda existe, mas só entra em ação quando as três coisas a seguir forem verdade: o app está em produção com usuários reais afetados, uma correção limpa vai levar mais de uma hora, e a quebra é visível para o usuário. Até lá, conserta para frente.
O motivo de essa regra estar escrita é a tentação do momento em que você acabou de quebrar a main: “eu só empurro a correção daqui a um minuto.” Às vezes esse minuto vira duas horas depois, e agora bisects ficam mais difíceis e qualquer commit novo cai numa base quebrada. Escrever a condição é como eu me impeço de negociar com ela às 2 da manhã.
Reprodução local que espelha o CI
A checagem que eu de fato rodo antes do push:
bin/brakeman --no-pager
bin/bundler-audit
bin/importmap audit
bin/rubocop
bin/rails db:test:prepare test
bin/rails db:test:prepare test:system
Seis comandos, cerca de 90 segundos nesse laptop, mapeando para os cinco jobs que o CI roda em paralelo (Brakeman e bundler-audit dividem um job). O objetivo não é substituir o CI. É pegar a besteira óbvia, uma violação perdida do RuboCop ou um import não usado, antes que o runner precise.
O que eu faria diferente
O playbook começou curto, cerca de 80 linhas de Markdown. Isso é intencional. Quanto mais longo, menos eu de fato leria durante um momento de main vermelha. A próxima coisa que quero adicionar é um cheat-sheet de “primeiros 5 minutos” bem no topo: os um ou dois comandos gh que me deixam decidir flake-ou-real sem precisar rolar a página.
O outro desdobramento foi deploy automático para staging a cada merge na main, que construí no dia seguinte. Isso ganhou sua própria seção no playbook, que é a maior parte do motivo de o documento ter praticamente dobrado de tamanho desde então.
Leitura relacionada
Movendo o CI para um runner self-hosted depois que o GitHub quebrou nosso billing
CI morto, deploys ao vivo: dobrando todo job no runner do homelab, apagando a maquinaria de compensação de runner hospedado, e o backlog de CVEs esperando atrás do gate.
Deploy automático do Rails 8 para staging com Kamal e um runner self-hosted do GitHub Actions
Fazendo todo merge disparar deploy automático para staging: um runner self-hosted, quatro paredes seguidas, e a pegadinha dos secrets do Kamal que demorou mais para ser decifrada.
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.
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 maiseSIM Airalo
eSIM de dados local para viagens - sem necessidade de trocar um SIM físico.
Este é meu link de indicação da Airalo. Você recebe um desconto no seu primeiro eSIM e eu ganho crédito da Airalo para o meu.
Saiba maisProton Mail
E-mail criptografado de ponta a ponta, com arquitetura de acesso zero.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba mais