Movendo o CI para um runner self-hosted depois que o GitHub quebrou nosso billing
O CI do blog-manager estava morto há dias. Não instável, não lento. Morto. Todo job no ubuntu-latest falhava em uns quatro segundos com uma anotação de billing antes de um único step rodar. Minutos do GitHub Actions em runners hospedados precisam de uma forma de pagamento funcionando, e a minha não estava naquele momento, então o workflow inteiro de CI simplesmente se recusava a começar.
A parte que realmente me preocupou: os deploys para staging continuaram funcionando o tempo todo. O pipeline de deploy estava vinculado à conclusão do workflow de build, não ao CI. O build já rodava num runner self-hosted que eu tinha montado meses atrás por um motivo completamente diferente (o registry do Gitea só resolve na rede interna, então runners hospedados pelo GitHub não conseguem alcançá-lo). Então os builds continuaram acontecendo, o staging continuou recebendo deploy, e nada estava de fato testando o código antes. É o tipo de configuração cujo defeito você só percebe quando vai procurar.
O que eu construí
A correção foi mover o resto do CI para o mesmo runner self-hosted e fazer o pipeline de fato depender dele. Concretamente:
- Dobrei os cinco jobs antigos de CI (duas varreduras de segurança, lint, testes, testes de sistema) no mesmo arquivo de workflow do build Docker, como três jobs:
checks,test,system-test. buildagora temneeds: [checks, test]. Se algum falhar, o build é pulado, e o deploy de staging, que dispara quando o workflow inteiro termina com sucesso, nunca acontece.system-testroda Chrome headless num contêiner LXC de um núcleo só. Eu não confiei que seria confiável logo de cara, então estácontinue-on-error: true, visível se falhar, mas sem bloquear nada por enquanto. Vou trocar isso assim que se provar em algumas execuções.
Decisões que tomei e por quê
Descartar a maquinaria de cache hospedada pelo GitHub por completo, em vez de tentar preservá-la. Os jobs antigos usavam ruby/setup-ruby com bundler-cache: true e um step de actions/cache para o RuboCop. Os dois existem para contornar o fato de runners hospedados pelo GitHub serem efêmeros, todo job começa de uma VM em branco, então você precisa buscar um cache para evitar reinstalar tudo. O disco de um runner self-hosted já é persistente. As gems da última execução estão… simplesmente lá. Então apaguei tudo isso e deixei o bundle install rodar contra o diretório de gems já populado do runner.
Isso me mordeu quase imediatamente, de um jeito que eu não esperava. bundler-cache: true não só armazena gems em cache, também coloca o Bundler num modo que falha de forma barulhenta se o Gemfile.lock não bater com o Gemfile. Tirar isso em favor de um bundle install puro e essa proteção desaparece silenciosamente; o Bundler simplesmente re-resolve e segue em frente. Só percebi isso porque fui atrás do comportamento equivalente. Corrigi com BUNDLE_FROZEN=true bundle install em vez disso, mesmo efeito, sem depender da maquinaria de runner hospedado pelo GitHub.
Pré-instalar só o que o runner de fato precisa uma vez, não o que todo job insiste em reinstalar. Os antigos jobs test e system-test rodavam sudo apt-get install libvips / chromium como um step, toda execução, porque numa VM nova essa é a única opção. Instalei os dois como pacotes de sistema únicos no host do runner e apaguei os steps de apt do workflow. Coisa pequena, mas é o mesmo tema: boa parte do que o CI hospedado pelo GitHub faz é compensar não ter memória entre execuções.
O que me surpreendeu
A primeira execução real de CI em semanas virou uma auditoria de segurança que eu não esperava. bundler-audit falhou com uma pilha de CVEs do nokogiri que eu não tinha visto localmente, porque meu banco de dados local de advisories estava desatualizado e a execução self-hosted nova puxou o atual. Depois de atualizar localmente e checar de novo, mais quatro apareceram atrás dele: msgpack, faraday (um de severidade alta), crass, concurrent-ruby. Nada disso foi causado pela migração do CI, as dependências simplesmente ficaram desatualizadas quietamente durante as semanas em que o CI não conseguiu rodar para pegar isso. Atualizar as cinco foi seu próprio commit pequeno antes que eu conseguisse sequer voltar a verificar as mudanças do pipeline. Esse é um argumento bem direto do motivo de todo esse exercício importar: o gate estava desligado, e já tinha deixado passar vulnerabilidades reais, mesmo que banais.
A outra surpresa foi quase um não-evento, e fico feliz por ter conferido em vez de presumir. Enquanto rastreava como o deploy-staging é disparado, surgiu um bug candidato que parecia sério: o gatilho workflow_run do GitHub bate com a branch base de um pull request ou com a branch head dele? Se for a branch base, então todo PR verde contra a main dispararia espuriamente um deploy de staging usando um commit que nunca de fato foi enviado ao registry. A documentação é genuinamente ambígua nisso. Em vez de confiar no meu instinto ou num resultado de busca, conferi o histórico real de execuções do workflow de deploy depois de duas execuções verdes de PR naquele dia. Nada disparou. Ele só dispara mesmo em pushes para a main, exatamente como deveria. Bom lembrete de que, para qualquer coisa com um raio de impacto real, “a documentação diz” e “eu tenho quase certeza” são ambos mais fracos do que simplesmente olhar o que de fato aconteceu.
O que vem a seguir
Os testes de sistema precisam de algumas execuções limpas antes que eu confie neles o suficiente para torná-los bloqueantes. E o runner em si ainda é configurado manualmente, Ruby via mise, o binário do runner, agora libvips e chromium, tudo instalado entrando via SSH e digitando comandos. Isso é aceitável para uma única máquina que montei uma vez, mas significa que reconstruí-la do zero significaria refazer tudo isso de memória. Registrei um follow-up para de fato codificar isso em Ansible.
Leitura relacionada
Um playbook de falhas de CI para um projeto Rails de uma pessoa só
Escrevendo as regras do que fazer quando o CI fica vermelho num projeto Rails solo, e a limitação do GitHub que transformou o gate de merge num comentário estrutural.
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
Proton 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 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 maisProton Pass
Gerenciador de senhas focado em privacidade, da equipe por trás do Proton Mail.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba mais