Tynnaretrato de 22/09, 21:52
voltar à Biblioteca

ADR-0084 — Runner macOS no Mac do fundador para o compile-check nativo

22/09/2026 · aceito (decisão do fundador, em viagem, por chat)

docs/adr/ADR-0084-runner-macos-no-mac-do-fundador.md

ADR-0084 — Runner macOS no Mac do fundador para o compile-check nativo

Data: 2026-09-22 · Status: aceito (decisão do fundador, em viagem, por chat) · Revisão obrigatória: 2026-09-24 (quinta), troca de usuário.

Contexto

O check required mobile-compile rodava inteiro em macos-14, runner hospedado e pago pelo GitHub. Na prática ele decidia pular em 27 das últimas 30 execuções: o diff só toca apps/jep/ios ou apps/jep/android uma vez a cada ~90 dias, e o que ele compila é uma sonda (xcodebuild -list, gradlew tasks), não um build.

Em 22/09 a cobrança da conta GitHub falhou e o job passou a morrer em 4 s sem log ("recent account payments have failed or your spending limit needs to be increased"). Como é required, nenhum PR conseguia mergear — inclusive PRs de documentação (#56, #57). A fila inteira ficou refém de um minuto de macOS que ninguém ia usar.

Todos os outros 25 jobs do repo já rodam em runners self-hosted no Hetzner (missão "CI no Hetzner", 03/09). O mobile-compile era o único que dependia de runner pago, e o comentário no próprio workflow já registrava isso como bloqueio conhecido.

Decisão

  1. Partir o job em três: decidir (Linux self-hosted, todo PR, computa o diff com a política fail-closed existente), compilar (macOS self-hosted, só quando decidir manda) e mobile-compile (Linux, if: always(), o nome required, que lê os dois resultados por scripts/ci/mobile-compile-veredito.sh e reprova qualquer combinação incoerente). O gate não é afrouxado: mesmo predicado, mesma sonda, mesmo nome required. Muda só a máquina que decide.
  2. O runner macOS é o Mac do fundador (Apple Silicon, Xcode 26.6, JDK 21, Android SDK já instalados), registrado como tynna-mac-1 com a label tynna-mac, serviço launchd. Minuto self-hosted é gratuito: depois disto nenhum job do repo usa runner hospedado, e a cobrança do GitHub deixa de conseguir travar a esteira.
  3. Somente o job compilar pode usar tynna-mac. tests/standards/mobile-compile-split.test.ts trava isso: se decidir ou o veredito dependessem do Mac, todo PR ficaria em fila quando o Mac estivesse desligado. Com a separação, um Mac desligado só atrasa o PR raro que toca shell nativo — e timeout-minutes: 30 transforma a espera em vermelho, não em silêncio.

Risco aceito, com prazo

O runner foi instalado sob a conta pessoal do fundador (mlermen, admin), como LaunchAgent, porque criar um usuário dedicado exige sudo com senha e o fundador está longe do Mac até quinta. O twin não tem nem pede a senha.

O que isso significa, sem eufemismo: qualquer job que rode em tynna-mac executa código de PR com acesso ao Keychain do fundador (chave do Paperclip, tokens OAuth do Google e do Telegram — o security find-generic-password responde sem senha), aos arquivos da conta e à sessão gráfica. Um PR escrito por agente, ou um prompt injection num PR, teria essa porta. É a mesma classe de risco que o PR #48 fechou no Hetzner (runner fora de docker, sudo, wheel).

Mitigações em vigor até quinta:

  • O repo é privado e não recebe PR de fork; todo PR vem do twin, do Mergify ou do fundador.
  • Só um job usa a label, e ele só dispara quando o diff toca apps/*/ios|android — hoje, nenhum PR aberto faz isso.
  • tests/standards/mobile-compile-split.test.ts reprova qualquer outro job com tynna-mac.

O que não está mitigado: um PR pode editar o próprio workflow e rodar um job arbitrário em tynna-mac. O ciso-l2 revisa mudanças em .github/workflows/**, mas com veredito de agente, não bloqueio mecânico. Por isso o prazo.

Quinta (2026-09-24), no Mac, um comando: sudo bash scripts/ops/criar-runner-mac.sh. Cria o usuário padrão tynna-runner (sem admin, Keychain vazio), instala o runner como LaunchDaemon sob ele, e imprime os passos para remover o runner da conta mlermen. Card no Paperclip com a pendência. Se em 2026-10-01 o runner ainda estiver sob mlermen, este ADR está violado e o job compilar deve voltar a macos-14 (com a cobrança resolvida), não continuar assim.

Consequências

  • A fila volta a mergear sem depender de cobrança; #56 e #57 destravam com o merge deste PR.
  • Um PR que toque shell nativo enquanto o Mac estiver desligado fica vermelho por timeout em 30 min, com mensagem clara; re-run quando o Mac voltar.
  • O Mac vira parte da infraestrutura: entra no runbook de credenciais (quinta) e o _diag/ do runner é onde se olha quando o job não inicia.
  • config/escopos.yml continua sem o mobile-compile em jobs_pesados, de propósito: a decisão dele vive no script, e mexer no contrato dispararia roda_tudo. O comentário lá que fala em "macos-14 pago" fica desatualizado até o próximo PR que toque o contrato por outro motivo.

Alternativas descartadas

  • Resolver só a cobrança. Resolve hoje e volta a travar na próxima falha de cartão. A separação vale por si; a cobrança fica como fallback.
  • EAS Build. Troca minuto do GitHub por cota de outro serviço, para uma sonda que não é build.
  • Remover o check dos required. Seria "aceitar risco contra um gate" (G2) — e desnecessário, já que a separação mantém o gate.
  • Runner como mlermen em caráter permanente. Recusado acima; é o que o prazo existe para impedir.

Só leitura. Toda mudança nestes documentos é pull request.