0026 — Apps compose (build-from-git) no control-plane, ancorados no slug do git repo¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-05 · Decidido em: 2026-09-05
Contexto¶
A 0021 (ADR central
0026) ancorou o control-plane de deploy
na imagem de registry: o reconcile só olha recursos Coolify com build_pack=dockerimage, deriva
registry_slug do docker_registry_image_name e target do prefixo da tag (<target>-amd64), e o
POST /api/ci/deploy exige image como âncora obrigatória.
Isso deixa de fora toda uma classe de app da casa: os recursos Coolify com
build_pack=dockercompose, que o Coolify builda a partir do git (os stacks PowerSync —
onpetro-powersync, maxsul-powersync, … — mongo + powersync num compose só). Eles não têm
imagem de registry: docker_registry_image_name e _tag vêm nulos, então o reconcile os pula e
nenhuma linha entra no app_deploy_targets. Sem linha, o /api/ci/deploy devolve 404
no_deploy_target — e o GitHub Actions desses repos fica sem caminho de deploy: ou faz auto-pull no
Coolify (sem gate, sem auditoria), ou volta pra ponte SSH que a 0021 matou.
O deploy em si é o mesmo: POST /deploy?uuid=<coolify_uuid> na API do Coolify, que rebuilda o
compose a partir do git. Só falta a âncora — o que identifica o recurso sem depender do
app_id do catálogo (que a 0021 recusa de propósito, por ser rebrandável).
Decisão¶
Recurso compose entra no control-plane ancorado no SLUG DO GIT REPO, com target='compose';
o endpoint ganha um segundo modo de âncora (app_id) para quem não tem imagem.
- Reconcile (
DeployReconcileService): além dedockerimage, tratadockercompose—registry_slug= último segmento dogit_repositorysem.gite sem credencial embutida (https://user:pass@fonte.xadm.biz/xadm/onpetro-powersync.git→onpetro-powersync),targetfixo'compose',instancedo FQDN como sempre. Compose sem git parseável (database ou serviço criado na UI) é pulado — não inventa linha. Build packs fora de{dockerimage, dockercompose}(nixpacks, static) seguem ignorados, agora explicitamente. - Endpoint (
POST /api/ci/deploy):imagedeixa de ser obrigatório. Comimage→ tudo como antes (a âncora é a imagem;app_idsegue rótulo de auditoria). Semimage→app_idé a âncora (registry_slug) etargetpassa a ser obrigatório. Novos códigos de erro:missing_anchor(nemimagenemapp_id) emissing_target(semimagee semtarget), ambos400. - Schema (V15): o CHECK de
app_deploy_targets.targetpassa a{jar, native, web, compose}. - Contrato pro CI do app compose:
POST /api/ci/deploy {"app_id":"<slug-do-repo>","target":"compose"}com oCENTRAL_DEPLOY_TOKEN. Oapp_idaqui é o nome do repo git, não oapp_iddo catálogo — eles coincidem por convenção, mas quem manda é o repo.
Alternativas descartadas¶
- Continuar exigindo
imagee publicar uma imagem falsa pros stacks compose só pra caber na âncora existente — build inútil, registry poluído, e a imagem nunca seria puxada (o Coolify builda do git). - Auto-pull do Coolify no push (webhook nativo) — é justo o que a 0021 recusou pra frota: sem
gate do CI, sem auditoria (
deploy_audit), sem controle de quando. Aceitá-lo só pros compose criaria dois modelos de deploy na casa — o preço da exceção é maior que o do segundo modo de âncora. - Ancorar compose no
app_iddo catálogoapps— reintroduz a dependência rebrandável que a 0021 removeu de propósito. O slug do git é tão estável quanto o slug da imagem, e igualmente observável no próprio recurso Coolify. - Endpoint separado (
/api/ci/deploy-compose) — duplicaria resolução, auditoria e on-miss por uma diferença que cabe num campo. Preterido.
Consequências¶
- O mapa
app_deploy_targetspassa a ter duas famílias de linha com a mesma chave lógica(registry_slug, target, instance): por imagem (jar|native|web) e por git (compose). Otargeté que as separa — nada mais muda na resolução, no on-miss, na auditoria nem noGETda frota. - Assunção que vale registrar: um repo git = um stack compose. Dois recursos compose do mesmo
repo, ambos com FQDN fora do padrão
int.<cli>.xadm.biz(→instanceNULL nos dois), colidem na UNIQUE e o segundo upsert sobrescreve ocoolify_uuiddo primeiro — um sumiria do mapa em silêncio. Hoje a convenção da casa é um repo por cliente (onpetro-powersync,maxsul-powersync, …), então o caso não existe; se um dia existir, o desempate é dar FQDNint.<cli>ao recurso (aíinstancesepara as linhas). target='compose'também é aceito no caminho por imagem (a validação detargeté única). Uma chamada{image: "…:jar-amd64", target: "compose"}passa a validação e cai em404na resolução — frouxo, mas inócuo; não vale um segundo conjunto deTARGETS.- Handoff §6 pra ADR central 0026, que ainda descreve a âncora como sendo a imagem e só ela: a
casa precisa da emenda "control-plane resolve compose pelo slug do git repo, com o segundo modo de
âncora
app_idno endpoint". - Sem endpoint novo, sem token novo, sem mecanismo novo — uma migration de CHECK, um ramo no
reconcile e um
ifna resolução.