how-i-build
Formigas ruivas em fila sobre folhas verdes, várias delas segurando as bordas de uma folha dobrada

As portas que eu tinha deixado abertas

Passei uma tarde fechando as portas que o Orbit tinha deixado abertas em produção. Nenhuma delas era um bug. Eram todas coisas que eu nunca tinha decidido, e que por isso ficaram no padrão — e o padrão de quase tudo é "aberto".

Cinco portas, cinco descobertas. As descobertas são mais interessantes que as portas.

Exigir o teste não é exigir os jobs

O primeiro item parecia trivial: não deixar um commit com o teste vermelho chegar em produção. O GitHub tem proteção de branch, é só marcar quais checks são obrigatórios.

Só que meu fluxo de CI pula o que não mudou. Se o PR só mexe no frontend, o job do backend nem roda. E aqui está a parte que eu não sabia: um job pulado não reporta status nenhum. Ele não fica verde nem vermelho — ele simplesmente não existe para quem está esperando. Marcar "backend" como obrigatório travaria para sempre todo PR que não toca no backend, esperando um resultado que nunca chega.

A saída é um job final que sempre roda, depende de todos os outros e falha se algum falhou:

ci-ok:
  needs: [changes, backend, web, docker]
  if: always()
  steps:
    - run: |
        for r in "${{ needs.backend.result }}" "${{ needs.web.result }}"; do
          case "$r" in
            success|skipped) ;;
            *) exit 1 ;;
          esac
        done

skipped conta como sucesso. É o único jeito de um check condicional poder ser obrigatório.

Descobri depois que proteção de branch em repositório privado é recurso pago no GitHub. Tudo bem: o cadeado que importa não é o do merge, é o do deploy. A hospedagem da API tem uma opção de só publicar depois que os checks passam, e ela é gratuita. Um commit ruim ainda pode entrar na branch, mas não chega em produção — que era o problema de verdade.

O rollback devolve o código, não o banco

O segundo item era uma regra, não código. O painel da hospedagem tem um botão de rollback que volta para a imagem anterior em um minuto. Eu confiava nele.

O que o botão não faz é voltar o banco. As migrations rodam na subida, então depois do rollback a versão antiga do código roda contra o schema novo. Se a migration só criou coluna, tudo bem: o código antigo ignora o que não conhece. Se ela apagou ou renomeou, o código antigo quebra na primeira consulta, e aí não existe botão que resolva.

Daí a regra que escrevi e coloquei no checklist de PR: um release só adiciona. Remover e renomear ficam para um release seguinte, depois que nenhuma versão no ar usa mais aquilo. Mudar o tipo de uma coluna virou cinco passos em vez de um. É mais chato e é o que mantém o botão de rollback sendo um botão de verdade.

A porta dos fundos que a CDN nunca viu

A API tem domínio próprio, mas a hospedagem também dá um endereço público direto. Quem descobrisse esse endereço falava com a API sem passar por nada.

O plano era o padrão: a CDN carimba um header secreto em toda requisição que passa por ela, e a API recusa quem não trouxer o carimbo. Escrevi o middleware, testei, e só então fui ver como o DNS estava configurado.

O registro da API apontava para a hospedagem sem proxy. A CDN só resolvia o nome — o tráfego ia direto, ela nunca via a requisição. Não havia onde carimbar nada. O server: cloudflare que eu via na resposta era da CDN da própria hospedagem, não da minha.

O passo zero, que não estava em plano nenhum, era ligar o proxy no registro. Trinta segundos de clique, depois de três horas escrevendo a parte difícil.

A segunda lição foi de ordem. Ligados fora de ordem, os dois lados se derrubam: API exigindo um carimbo que ninguém aplica é uma API que recusa todo mundo. Então o middleware nasceu desligado:

func OriginSecret(secret string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		if secret == "" {
			return next
		}
		...

Segredo vazio, middleware transparente. Dá para publicar a API primeiro, criar a regra na CDN depois, e só então preencher o segredo. E para desligar em emergência, basta esvaziar o valor — sem deploy.

As rotas de saúde ficaram de fora da checagem, porque o health check da própria hospedagem bate direto no serviço, sem passar pela CDN. Se eu tivesse esquecido disso, a plataforma concluiria que a API está morta e ficaria reiniciando ela para sempre.

Um erro que ninguém vê

A terceira porta era a mais silenciosa: um erro 500 morria no log da hospedagem. Eu só descobriria se fosse olhar, e eu não ia olhar.

Ligar um rastreador de erros foi o item mais simples de todos, e o que mais mudou a sensação de estar no ar. Agora um panic chega com a pilha, a rota e o identificador da requisição, em vez de virar uma linha que ninguém lê.

A única decisão real foi o que não ligar. O plano gratuito dá cinco mil eventos por mês, e rastreamento de performance e gravação de sessão comem essa cota depressa para responder perguntas que o log já responde. Deixei os dois em zero. Um monitor que estoura a cota no dia 12 é pior que não ter monitor.

O monitor que dizia 200

A última porta foi a que mais me ensinou.

Eu já tinha um vigia próprio: um worker que pinga meus serviços e me avisa quando algum cai. O problema é que ele não pode avisar sobre si mesmo. Se ele parar, o silêncio é idêntico a "está tudo bem".

Então coloquei um monitor de fora, num serviço que não divide nada com a minha infra, apontado para a página de status do worker. Ela responde 200, o monitor fica feliz, e eu fiquei tranquilo.

Errado. A página é servida de um cache de estado: ela responde 200 mesmo que nenhum ping tenha acontecido nas últimas seis horas. Ela mostraria dado velho, com aparência perfeita, e o monitor de fora não veria diferença nenhuma. Eu tinha construído um vigia para o vigia que olhava para o lado errado.

A correção tem vinte linhas: uma rota de saúde que devolve 500 quando o último ciclo passou do prazo.

{
  "ok": true,
  "crons": {
    "fast":  { "ageMinutes": 3,   "limitMinutes": 30 },
    "daily": { "ageMinutes": 862, "limitMinutes": 1560 }
  }
}

O monitor externo passou a apontar para essa rota. Agora ele não pergunta "o servidor responde?", pergunta "o trabalho está acontecendo?". São perguntas diferentes, e só a segunda importa.

O que ficou

A regra que eu tiro dessas cinco portas é uma só: o padrão nunca é seguro, porque o padrão não foi escolhido por ninguém. O deploy sobe sem perguntar, a porta dos fundos fica aberta, o erro vai para um log que ninguém lê, e o monitor responde a pergunta mais fácil em vez da certa.

E tem a segunda, que aprendi apanhando: verifique o degrau antes do degrau. Três horas de middleware antes de descobrir que o tráfego nem passava por onde eu achava. Meia hora olhando a configuração teria mudado a ordem do trabalho inteiro.

As formigas da foto passam o dia segurando a borda de uma folha para fechar o ninho. Nenhuma delas segura sozinha, e nenhuma delas escolheu a folha: elas fecham o que está aberto, uma de cada vez.