- GitHub Actions
- CI/CD
- Self-hosted Runner
- DevOps
- Contingência
- Infraestrutura
- Confiabilidade
Contingência de CI/CD: por que o runner self-hosted não salvou ninguém na queda do GitHub Actions
Quando o GitHub Actions caiu por nove horas, quem tinha runner self-hosted parou junto — e, quando voltou, a fila de eventos perdidos não voltou com ele. Contingência de CI costuma cobrir a máquina que roda o build e esquecer a camada que sabe qual build precisa rodar.
Quinta-feira, 6 de agosto de 2026, 15h22 UTC. O GitHub marca o Actions como "degraded performance". Vinte minutos depois, a disponibilidade também cai. Dali até a mitigação foram quase nove horas, e o incidente só foi encerrado às 2h04 da manhã do dia seguinte.
Aqui o nosso pipeline parou junto com o resto do mundo por algumas horas. E eu não quero transformar isso em post de reclamação, porque cair faz parte e o Actions continua sendo uma peça de infraestrutura absurdamente boa pelo que custa. O que me pegou foi outra coisa: passei o dia vendo gente dizer que estava tranquila porque tinha runner self-hosted.
Não estava.
O runner próprio te dá a máquina, não a decisão
O texto oficial do incidente é bem específico nesse ponto: "Both GitHub-hosted and self-hosted runners are affected". E, mais adiante, que quem usa runner próprio poderia ver erro ou rate limiting no momento em que o runner se registra.
Isso diz muita coisa sobre onde o serviço realmente mora. Quando a gente sobe um runner self-hosted, o que entra para dentro de casa é a computação: o processador que roda pnpm test, o disco que guarda o cache, a rede que baixa a imagem. Só que rodar o build é a parte final de uma cadeia bem maior. Antes disso alguém precisa receber o evento de push, decidir que aquilo dispara um workflow, ler o YAML, montar a matriz de jobs, escolher para qual runner cada job vai, autenticar esse runner, entregar o token do checkout e depois receber o resultado de volta para pintar o check verde no PR.
Nada disso acontece na sua máquina. Tudo isso é o serviço.
Ou seja: o runner self-hosted não te tira da dependência, ele muda a natureza dela. Você deixou de depender da capacidade do GitHub e passou a depender do despacho do GitHub. E foi exatamente o despacho que quebrou, o que explica por que os dois tipos de runner caíram no mesmo minuto.
Teve um detalhe ainda mais desconfortável para quem tinha runner próprio. Nos updates do incidente, o GitHub avisou que alguns pods do Actions Runner Controller ficaram presos em estado idle e que a recuperação precisava de mão humana: apagar os pods no kubectl ou fazer redeploy da aplicação do ARC. Quem investiu em runner próprio para ter mais controle recebeu, no pior momento do dia, uma tarefa manual a mais. O próprio GitHub reconheceu isso e disse que as próximas versões do Runner e do ARC vão trazer recuperação automática.
O que doeu mesmo foi depois
Agora o ponto que quase ninguém comentou, e que é o mais caro da história inteira.
No update de encerramento, o GitHub avisou que alguns eventos que disparam workflow — incluindo push e pull request — não foram processados durante o incidente e não podem ser reproduzidos automaticamente. A recomendação foi essa: empurre um commit novo, atualize o PR, ou vá lá e mande rodar o workflow na mão.
Vale ler de novo com calma, porque a implicação é grande. Não é que os builds falharam. Falhar é ótimo: falha é vermelho na tela, é notificação, é alguém percebendo. O que aconteceu foi mais silencioso — uma parte dos eventos simplesmente não virou nada. Nenhum run, nenhum check pendente, nenhum registro. Do lado de fora, aquele commit parece que passou.
Então o estrago não terminou às 2h04 quando o incidente foi encerrado. Ele continuou na manhã seguinte, na forma de uma pergunta que ninguém no time sabia responder: o que exatamente ficou para trás?
Quem estava com "esperar passar" como plano de contingência descobriu que esperar passar recupera o serviço, mas não recupera o trabalho.
A parte que ninguém duplica
Aqui é onde eu quero chegar.
Quando a gente senta para pensar em contingência de CI, o instinto manda replicar aquilo que é visível e fácil de desenhar no quadro: a máquina que roda o build. Compro um runner. Subo um Jenkins. Deixo um GitLab CI de reserva. Tudo isso é dinheiro e trabalho investidos na camada de execução.
Só que a camada de execução não foi a que caiu. E, se você olhar os três fatos do incidente lado a lado, todos apontam para o mesmo lugar: os dois tipos de runner caíram juntos porque o que quebrou foi o despacho; os pods do ARC travaram porque perderam o vínculo com quem manda neles; e os eventos perdidos não voltaram porque a fila deles não existe em nenhum outro lugar.
Todo mundo duplica a parte que roda o build. Ninguém duplica a parte que sabe qual build precisa rodar — e foi essa que caiu.
Essa camada é bem menos glamourosa que um runner: é a fila de eventos, é a associação entre commit e execução, é o histórico de o que rodou e o que não rodou. Ela não aparece em nenhum diagrama de arquitetura, ninguém coloca ela no orçamento, e é a única coisa de que você realmente não tem cópia.
E olha que ela é barata de reconstruir depois, se você tiver pensado nisso antes. Não precisa de segundo CI, nem de Kubernetes, nem de nada caro. Precisa de uma pergunta respondível: quais commits entraram e não têm execução associada?
# Commits que entraram na main na janela do incidente
# e que não têm nenhum workflow run associado.
git log --since="2026-08-06 15:00" --format=%H origin/main | while read sha; do
runs=$(gh run list --commit "$sha" --json databaseId --jq 'length')
[ "$runs" -eq 0 ] && echo "sem run: $sha"
done
São seis linhas. Não impedem o CI de cair e não aceleram em um minuto a recuperação do GitHub. Mas transformam "acho que está tudo certo" em uma lista finita de commits para reprocessar, e isso é a diferença entre encerrar o incidente e achar que encerrou. Faz sentido?
O pipeline que caberia em qualquer lugar
Dito isso, tem a metade da contingência que é sobre para onde levar o build. E aqui vale enunciar a versão menos óbvia do problema.
O que prende a maioria dos times ao Actions não é o Actions. É ter escrito o build inteiro dentro dele: trinta passos de YAML, quinze uses: do marketplace, segredos amarrados na sintaxe do fornecedor e lógica de negócio espalhada em if: de workflow. Quem faz assim não tem um plano B ruim, tem uma reescrita pela frente. E reescrita não se faz no meio de um incidente.
O desenho que sobrevive é o contrário: a lógica mora em script, e o CI é uma casca fina que chama esse script.
# Makefile — o build de verdade, que roda igual na sua máquina e no CI.
ci: lint typecheck test build
lint:
pnpm biome check .
typecheck:
pnpm tsc --noEmit
test:
pnpm vitest run --coverage
build:
docker build -t $(IMAGE):$(SHA) .
E aí a casca, em três lugares diferentes:
# .github/workflows/ci.yml
- uses: actions/checkout@v4
- run: make ci
# .gitlab-ci.yml
ci:
script: make ci
// Jenkinsfile
stage('ci') { steps { sh 'make ci' } }
Repare que o miolo não muda. Muda a linha que sabe fazer checkout e a linha que sabe chamar make. É por isso que eu não gosto da conversa de "trocar o Actions pelo Jenkins": trocar de CI não deveria ser um projeto, deveria ser uma tarde. E, sendo bem honesto, trocar o Actions por um Jenkins mal cuidado é péssimo negócio — você troca a indisponibilidade do GitHub, que tem time de plantão, pela sua própria: patch de segurança atrasado, plugin que quebra no upgrade, disco enchendo, sem escala elástica e um ponto único de falha que ninguém monitora. Somadas no ano, as horas que o Actions te deixa na mão provavelmente custam menos que isso.
Jenkins e GitLab CI aqui não são recomendação de troca. São a prova de que o desenho portável funciona.
Portabilidade não é ter dois CIs ligados. É ter um pipeline que caberia em qualquer um.
É o mesmo raciocínio que eu segui construindo a Lisa, a CLI que eu mantenho: a lógica de abrir PR, checar CI e fazer merge é uma só, e existe uma camada fina de tradução por plataforma para GitHub, GitLab e Bitbucket. Quando eu preciso perguntar se o CI passou, a pergunta é sempre a mesma; o que muda é se ela vira um gh pr checks ou uma chamada na API de pipeline do GitLab. Inclusive, é um exemplo bom justamente porque não é perfeito: no Bitbucket essa checagem devolve unknown, porque não existe um caminho barato a partir da URL do PR. A abstração aguenta a lacuna sem contaminar o resto.
A escada, do barato ao caro
Contingência é dosagem, e eu acho que a melhor forma de decidir é olhar isso como degraus, sabendo o preço de cada um:
- Pipeline portável. O build vira script, o CI vira casca. Custa algumas horas e você vai usar todo dia, mesmo sem incidente nenhum, porque
make ciroda igual na sua máquina. - Saber o que ficou para trás. As seis linhas lá de cima, ou o equivalente na sua stack. Custa uma tarde e é o único degrau que ataca o dano que sobra depois que o serviço volta.
- Deploy manual documentado e testado. Um
docker build, umpushpara o registry, um comando de restart. A palavra que carrega o degrau é testado: runbook que ninguém executou nos últimos seis meses é ficção. Bota no calendário e roda uma vez por trimestre, mesmo sem incidente. - Mirror do repositório. Git é distribuído, então o código em si já está no notebook de todo mundo. O que você perde numa queda maior não é o código, é o hub: PR, issue, pipeline, registry. Um espelho em outro provedor é um
git pusha mais e resolve a continuidade do código por quase nada. - CI secundário ligado de verdade. Aqui o preço sobe de patamar: dois conjuntos de segredos, duas configurações de runner, dois lugares para manter quando alguém mexe no build. Só compensa se o degrau 1 já estiver pronto, senão você vai manter dois pipelines divergentes.
Os quatro primeiros degraus, somados, dão menos de uma semana de trabalho e não criam nada novo para operar. É até onde eu iria na maioria dos projetos que passam pela minha mão, e é onde eu recomendo parar para a esmagadora maioria dos times.
Do quinto degrau em diante — CI secundário, Jenkins em alta disponibilidade, runner em duas nuvens — a conversa deixa de ser técnica e vira financeira. O critério é uma pergunta só, e ela não é minha para responder: quanto custa para você uma hora sem conseguir fazer deploy?
Para um SaaS que entrega correção de bug crítico várias vezes ao dia, o número é alto e o degrau cinco se paga. Para um produto que faz release semanal planejado, nove horas paradas custam um remanejamento de agenda e nada mais. Caso você não saiba responder essa pergunta com um número, o próximo passo não é montar contingência: é descobrir o número. Contingência dimensionada por medo sai muito mais cara que contingência dimensionada por conta.
E, para deixar claro que isso não é papo de evento isolado: o histórico de incidentes do próprio GitHub registra seis incidentes tocando Actions entre 17 de julho e 7 de agosto. Três semanas. Essa é a taxa normal de qualquer serviço dessa escala, e é em cima dela que vale planejar — não em cima do pico de nove horas, que é o número que assusta e justamente por isso é o pior conselheiro.
Fechando
Contingência de CI/CD costuma ser discutida como um problema de redundância de máquina, e essa é justamente a metade fácil. A metade que morde é a que não aparece: o estado.
Se eu tivesse que deixar isso em quatro frases:
- Runner próprio é capacidade, não independência. Ele te dá o processador, mas quem enfileira, despacha, autentica e reporta continua do outro lado. Foi por isso que os dois tipos de runner caíram no mesmo minuto.
- O incidente não acaba quando o serviço volta. Evento que não virou execução não se recupera sozinho. Tenha um jeito de listar o que ficou para trás antes de precisar dele.
- O build mora em script, o CI é casca fina. Se trocar de CI é um projeto, você não tem plano B, tem uma reescrita. E reescrita não se faz durante o incidente.
- O tamanho da contingência é o custo da parada. Faça os quatro primeiros degraus, que são baratos e você usa todo dia. Do quinto em diante, só depois de ter o número na mão.
No fundo é o mesmo desconforto que eu já tinha descrito falando de release de app mobile: a gente nunca controla quando o outro lado atualiza, só controla o que ele recebe. Ali o outro lado é o usuário que não instalou a versão nova; aqui é a plataforma que despacha os seus jobs. Muda o objeto, não muda a natureza do problema.
O que essa queda me deixou não foi vontade de sair do GitHub. Foi a percepção de que eu tinha pensado a contingência inteira em cima de onde o build ia rodar, e nenhum minuto em cima de como eu descobriria o que não rodou. A segunda parte custa uma tarde de trabalho. Naturalmente, é a que eu vou usar primeiro na próxima.
E caso você tenha chegado até aqui sem conseguir responder quanto custa uma hora sem deploy no seu caso, essa é exatamente a conversa que eu gosto de ter. Me manda uma mensagem que a gente pensa junto.