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.jsondo bucket para descobrir as versões. Isso cria um deadlock de bootstrap: oapp.jsonsó chega ao bucket viadocs.yml, que roda emci-<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: amatriz-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 ocontainer:. 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>(FROMeclipse-temurin:<v>-jdk - node/git/python),
ci-flutter:<v>(FROMcirruslabs/flutter:<v>— SDK bootstrapado, mata ogit clone). - Java 25 é o LTS da casa (fixado na engenharia; antes não estava escrito).
- Declaração no
app.json: campotoolchain: { 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 ocontainer: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 daci-images/matriz-baseline.json(fonte única — ver Correção 2026-06-25 acima); otoolchaindo 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 (
.fvmrcp/ Flutter; manifesto p/ Java): ocontainer:referencia a tag igual à declaração; nada deFLUTTER_VERSIONno 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 dorespondingTimeoutselevado para o push grande não dar 499 (feedback item 7). Sem os dois, a fábrica não publica e ocontainer: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
./gradlewdos apps baixava a distribuição do Gradle deservices.gradle.org(redireciona p/ host de asset do GitHub) no 1º run — em cache frio, umUnknownHostExceptiontransiente ali pinta o gate de vermelho aleatório. Era a exceção acidental ao "zero download por run": aci-javaassava o JDK mas não o Gradle. Correção: aci-javaassa a distribuição do wrapper (versão escalargradlenamatriz-baseline.json, fonte única comojava/flutter) no caminho-hash canônico sobGRADLE_USER_HOME=/opt/gradle— o hash dewrapper/dists/<hash>/deriva dadistributionUrl, então o./gradlewdo 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 adistributionUrlcasar a assada byte-a-byte — mesma versão E mesma variante (-bin, não-all) — e usar odistributionBasedefault (GRADLE_USER_HOME;distributionBase=PROJECTpõ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.jsoncom ogradlenovo → a fábrica rebuilda aci-javacom a dist nova → (2) só então o app troca a versão nagradle-wrapper.properties. Invertido, o wrapper do app cai no download externo até a imagem existir. O cache doci.ymlpassa a guardar só deps (/opt/gradle/caches); a dist vem da imagem.