Pular para conteúdo

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 de dockerimage, trata dockercompose — registry_slug = último segmento do git_repository sem .git e sem credencial embutida (https://user:pass@fonte.xadm.biz/xadm/onpetro-powersync.git → onpetro-powersync), target fixo 'compose', instance do 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): image deixa de ser obrigatório. Com image → tudo como antes (a âncora é a imagem; app_id segue rótulo de auditoria). Sem image → app_id é a âncora (registry_slug) e target passa a ser obrigatório. Novos códigos de erro: missing_anchor (nem image nem app_id) e missing_target (sem image e sem target), ambos 400.
  • Schema (V15): o CHECK de app_deploy_targets.target passa a {jar, native, web, compose}.
  • Contrato pro CI do app compose: POST /api/ci/deploy {"app_id":"<slug-do-repo>","target":"compose"} com o CENTRAL_DEPLOY_TOKEN. O app_id aqui é o nome do repo git, não o app_id do catálogo — eles coincidem por convenção, mas quem manda é o repo.

Alternativas descartadas

  • Continuar exigindo image e 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_id do catálogo apps — 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_targets passa 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). O target é que as separa — nada mais muda na resolução, no on-miss, na auditoria nem no GET da 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 (→ instance NULL nos dois), colidem na UNIQUE e o segundo upsert sobrescreve o coolify_uuid do 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 FQDN int.<cli> ao recurso (aí instance separa as linhas).
  • target='compose' também é aceito no caminho por imagem (a validação de target é única). Uma chamada {image: "…:jar-amd64", target: "compose"} passa a validação e cai em 404 na resolução — frouxo, mas inócuo; não vale um segundo conjunto de TARGETS.
  • 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_id no endpoint".
  • Sem endpoint novo, sem token novo, sem mecanismo novo — uma migration de CHECK, um ramo no reconcile e um if na resolução.