Pular para conteúdo

0003 — Fábrica de imagens de CI por stack+versão

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-06-25 · Decidido em: 2026-06-22

Contexto

Os templates de CI (ci.yml, docs.yml) rodavam em node:22-bookworm-slim e montavam o ambiente do zero a cada run: apt/pip, download de JDK e — em Flutter — git clone do SDK (o maior custo isolado, pago 2× em workflows separados). Medições reais (bi-transporte, via loop §6): ci.yml ~12m, docs.yml ~5,5m, a maior parte só setup. Além disso o runner Forgejo da org não resolve actions de marketplace (subosito/flutter-action → 404), então o bloco Flutter dependia de git clone manual.

Decisão

Correção 2026-06-25 — fonte das versões (Opção A do Gustavo): o desenho original mandava a fábrica agregar os app.json do bucket para descobrir as versões. Isso cria um deadlock de bootstrap: o app.json só chega ao bucket via docs.yml, que roda em ci-<stack>:<v> — a imagem que a fábrica deveria ter buildado. O 1º app version-pinned (bi-transporte, flutter: 3.44.0) travou exatamente aí. Corrigido: a matriz-baseline.json é a fonte única das versões; a casa homologa cada versão lá (PR + push → a fábrica builda) antes de o app apontar o container:. A leitura do bucket foi removida. O núcleo da decisão (imagens por stack+versão, registry, opt-in) continua de pé; só o mecanismo de descoberta mudou. (§6 do bi-transporte.)

Imagens de CI por stack + versão, pré-construídas e servidas no registry do Forgejo da org, montadas por uma fábrica central automatizada:

  • Camadas: ci-base (node + git + python3 + rclone + curl — apps de docs/Node/script e pai conceitual), ci-java:<v> (FROM eclipse-temurin:<v>-jdk
  • node/git/python), ci-flutter:<v> (FROM cirruslabs/flutter:<v> — SDK bootstrapado, mata o git clone).
  • Java 25 é o LTS da casa (fixado na engenharia; antes não estava escrito).
  • Declaração no app.json: campo toolchain: { java, flutter }, OBRIGATÓRIO para app Java/Flutter (o validador falha sem ele — a fábrica precisa da declaração para buildar a imagem-versão; sem ela o container: aponta para imagem inexistente e o CI quebra com erro de pull obscuro). App docs-only (ci-base, sem versão) não declara. A fábrica builda a matriz a partir da ci-images/matriz-baseline.json (fonte única — ver Correção 2026-06-25 acima); o toolchain do app é declaração validada, não gatilho de build.
  • Matriz por stack+versão, NÃO por app (imagem por app explodiria registry e manutenção — princípio "Simplicidade primeiro"). Apt-extra raro de um app fica como install por-run, não vira imagem.
  • Versão da stack = fonte única no repo do app (.fvmrc p/ Flutter; manifesto p/ Java): o container: referencia a tag igual à declaração; nada de FLUTTER_VERSION no Coolify (resolve o drift de 4 fontes — feedback item 4).

Alternativas consideradas

  • Imagem pública direto no container: (cirruslabs/flutter, eclipse-temurin) sem registry: ~80% do ganho, zero infra — mas Java precisa de node por-run (sem imagem pública com node+JDK) e o pull público não é cacheável de forma garantida. Preterida porque o Gustavo optou pela fábrica org-wide.
  • Imagem por app (declara tudo, monta sob medida): explode registry/manutenção; contraria simplicidade. Preterida.

Consequências

  • Pré-requisitos de infra (do Gustavo, fora deste repo): o runner precisa buildar+empurrar imagem (socket do Docker no act_runner) e o Traefik precisa do respondingTimeouts elevado para o push grande não dar 499 (feedback item 7). Sem os dois, a fábrica não publica e o container: dos apps não resolve. Otimização aberta: imagem Flutter slim web-only (sem Android SDK) reduz tamanho e risco de 499.
  • Bump da imagem = editar ci-images/ (Dockerfile/baseline) → a fábrica rebuilda; o app passa a referenciar a tag nova quando migra (oportunista).

Adendo 2026-07-27 — Gradle assado (feedback §6, bi-transporte-xls v2.1.3). O ./gradlew dos apps baixava a distribuição do Gradle de services.gradle.org (redireciona p/ host de asset do GitHub) no 1º run — em cache frio, um UnknownHostException transiente ali pinta o gate de vermelho aleatório. Era a exceção acidental ao "zero download por run": a ci-java assava o JDK mas não o Gradle. Correção: a ci-java assa a distribuição do wrapper (versão escalar gradle na matriz-baseline.json, fonte única como java/flutter) no caminho-hash canônico sob GRADLE_USER_HOME=/opt/gradle — o hash de wrapper/dists/<hash>/ deriva da distributionUrl, então o ./gradlew do runner acha a dist assada e não baixa. O egress do runner foi medido aberto (2026-07-27: Maven Central, plugins e a própria dist alcançam) → não há parede de rede nem 2º furo de deps; assar é blindagem + alinhamento, não conserto de parede.

Pareamento obrigatório Gradle-assado ↔ gradle-wrapper.properties: o app só se beneficia se a distributionUrl casar a assada byte-a-byte — mesma versão E mesma variante (-bin, não -all) — e usar o distributionBase default (GRADLE_USER_HOME; distributionBase=PROJECT põe a dist fora e volta a baixar). Divergir não quebra — degrada (volta a baixar).

Ordem de bump (anti-deadlock, igual ao Java): (1) PR na matriz-baseline.json com o gradle novo → a fábrica rebuilda a ci-java com a dist nova → (2) só então o app troca a versão na gradle-wrapper.properties. Invertido, o wrapper do app cai no download externo até a imagem existir. O cache do ci.yml passa a guardar só deps (/opt/gradle/caches); a dist vem da imagem.