Pular para o conteúdo principal
11 min de leitura
  • 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:

  1. 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 ci roda igual na sua máquina.
  2. 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.
  3. Deploy manual documentado e testado. Um docker build, um push para 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.
  4. 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 push a mais e resolve a continuidade do código por quase nada.
  5. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.