Pular para conteúdo

Bibliotecas da casa (xadm-commons)

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-12

Aplica-se a: perfil app que consome as libs (server Micronaut) e perfil lib (o xadm-commons).

A infra idêntica entre apps vive no xadm-commons: um repo multi-módulo, cada módulo com versão própria, publicado como artefato Maven no registro público fonte.xadm.biz. O app consome por versão, nunca por cópia — o porquê está na decisão 0019.

Módulos

Módulo Capacidade
xadm-seguranca autenticação, login Google server-side das views com a tela servida pela lib, enriquecedor XadmViewModel das views JTE
xadm-comum-web corpo de erro RFC 7807, /health e /info flat, versão em runtime, observabilidade (Sentry)
xadm-mensageria entrega M2M at-least-once (outbox + relay) — 0021
xadm-comum-util Formata e os helpers null-safe de exibição das views JTE
xadm-comum-storage storage S3 (Garage) com o ponto de injeção ObjetoStorage
xadm-comum-teste infra de teste (Testcontainers, Garage, ArchUnit)
xadm-comum-powersync administração do lado servidor do PowerSync
xadm-ingestion-core recepção e envelope de ingestão de arquivos

Elegível = server Micronaut. O integrador-client (Java 8, sem Micronaut) e os apps Flutter ficam fora por stack: é fronteira, não dívida.

Consumir

repositories {
    mavenCentral()
    // Registro público — leitura anônima, sem credentials{}.
    maven { url = uri("https://fonte.xadm.biz/api/packages/xadm/maven") }
    // Depois do registro: só a versão da casa ainda não publicada sai do ~/.m2.
    mavenLocal { content { includeGroup("br.com.xadm") } }
}
dependencies {
    implementation("br.com.xadm:xadm-seguranca:<versão corrente>")
}

Ler não pede credencial; publicar sim, e só o CI do xadm-commons publica (Segurança §Segredos). O app compila contra o artefato publicado, que é o que a produção embarca; includeBuild apontando o checkout da lib é recurso de desenvolvimento local, nunca norma.

  • Versão ainda não publicada vem do mavenLocal, para testar o app contra a lib antes do deploy. O mavenLocal() fica depois do registro e só para br.com.xadm: a versão publicada sempre sai do registro, e o local só preenche o número que ainda não saiu. Por isso a lib sobe o número antes de publicar local (Versionamento) — republicar local um número que já está no registro não chega ao app.
  • A /xadm-release do app recusa dependência da casa fora do registro: no runner do pipeline.yml não há ~/.m2, e ela daria 404.
  • Registro fora do ar: o Gradle não passa para o mavenLocal — falha no timeout do registro (Could not HEAD … Connection timed out). ./gradlew --offline resolve pelo cache e pelo ~/.m2.

Versão: na corrente, nunca abaixo do piso

flowchart TB
    L["xadm-commons<br/>módulo com SemVer próprio"] -- "/xadm-release publica" --> R["registry Maven do Forgejo<br/>maven-metadata.xml"]
    R -- "&lt;release&gt; = a corrente" --> A["app declara a versão<br/>no build.gradle.kts"]
    P["pisos-libs.json do central<br/>(/toolchain/pisos-libs.json)"] --> Q{"onde o app está?"}
    A --> Q
    Q -- "na corrente" --> OK["release segue"]
    Q -- "atrás da corrente" --> PEND["pendência: a próxima<br/>/xadm-release bumpa e migra"]
    Q -- "abaixo do piso" --> STOP["/xadm-release RECUSA<br/>(CI só avisa)"]
    PEND --> MIG["migrar no mesmo PR<br/>pela seção ### Migração do CHANGELOG"]
    STOP --> MIG

App elegível declara a última versão publicada de cada módulo que consome. A fonte é o maven-metadata.xml do registro — público, anônimo, sempre verdadeiro:

curl -fsSL https://fonte.xadm.biz/api/packages/xadm/maven/br/com/xadm/<modulo>/maven-metadata.xml
# <release> é a corrente
  • Atrás da corrente é pendência: a próxima /xadm-release do app bumpa, migrando.
  • Abaixo do piso a /xadm-release recusa a release; override só declarado no relatório, com o motivo. O CI só avisa (::warning:: na guarda de pisos do pipeline.yml).
  • A versão que vale é a do registro, não a da seção do CHANGELOG: número anunciado na nota de release que não chegou ao registro não existe para o app.

Pisos — a versão abaixo da qual estar é defeito conhecido rodando em produção:

módulo piso abaixo do piso
xadm-seguranca 0.7.2 abaixo, o app.api-token declarado e vazio não recusa o boot: o app sobe healthy com /api/** em 401 a tudo
xadm-comum-web 0.9.1 abaixo, ou todo 404 de negócio autenticado vai ao GlitchTip como ERROR e reprova a camada 3 do smoke num deploy sadio, ou a rota podada pelo AOT não chega ao GlitchTip
xadm-mensageria 0.3.4 abaixo, o relay não elege instância: duas instâncias do app entregam em dobro
xadm-comum-teste 0.3.0 abaixo, o ArchUnit exportado não conhece o class major do Java 25 e a suíte de arquitetura roda vácua, verde testando nada

A fonte única é o pisos-libs.json do central, publicado em /toolchain/pisos-libs.json; esta tabela é gerada dele no build. Leem o mesmo arquivo a guarda do pipeline.yml, a /xadm-docs, a /xadm-release e a guarda de publish do xadm-commons. Lista curta por construção: só entra piso que cobra migração, não todo bump.

  • Quando um piso sobe, a /xadm-release do xadm-commons faz a leva nos adotantes (bump, migração, gate e commit local em cada um); o deploy de cada app sai pela /xadm-release dele.
  • Transitiva: o app não confere dependência transitiva. A guarda de publish do xadm-commons recusa POM com módulo irmão abaixo do piso ou não publicado.

Adotar uma versão é migrar, no mesmo PR

Toda release de módulo traz a seção ### Migração no CHANGELOG do módulo (xadm-commons/<modulo>/CHANGELOG.md), nem que seja "nada a fazer". A /xadm-release do app aplica todas as seções entre a versão atual e a corrente, em ordem:

  1. Remova o código local que a lib passou a prover (e o teste dele): dois donos do mesmo recurso colidem no boot (dois @Controller com /login, dois ErrorResponseProcessor).
  2. Ajuste config, env e template ao contrato novo que a seção descreve.
  3. Rode o e2e-local e ajuste no mesmo passo: é ele que pega o desvio de contrato (rota, corpo de erro, model key, env) antes da produção.

Migração mecânica ("nada a fazer", apagar a cópia que a lib promoveu) entra sem perguntar; a que mexe em config, DDL ou comportamento pergunta (REGRA Nº 1). Rename de env ou de prefixo de config é dual-read: a lib lê o nome novo e o antigo até todo deploy estar no novo.

Site da lib

O xadm-commons publica um site mínimo em docs-sites/xadm-commons/dev/, sem versões: um índice, uma página por módulo que embute o CHANGELOG dele (--8<--) e o Javadoc em dev/api/<modulo>/. É por ele que o agente de um app lê a seção Migração — o raw do Forgejo é privado e responde 404 sem login. A /xadm-docs e a 0019 linkam para lá.

O pipeline, a /xadm-release e o índice de CHANGELOGs da lib são customização local declarada: consumidor único, sem template no kit.

Autoria de lib

O defeito numa lib alcança todo adotante, muitas vezes sem contorno do lado dele — a régua de teste de uma lib é mais alta que a de um app (definição de pronto).

Extrair sem regredir

  • Extração é conservadora e caracterizada. Sobe para a lib o que é idêntico entre apps; classe que diverge exige decisão explícita de reconciliação antes de subir. Cada extração começa pela matriz idêntico/divergente das cópias.
  • Reconcilie para o baseline endurecido, não para o mais simples. Idêntico é gatilho de revisão, não meta: copiar fielmente também copia os defeitos da origem. Se uma cópia tinha endurecimento (comparação constant-time, guarda de vazio, default de config que não quebra o boot), a lib o porta; se as cópias divergiam, canoniza-se a mais forte. Classe que toca segredo ou auth passa pelo checklist do revisor, com diff semântico contra a origem.

Bean de lib é bom-cidadão do contexto do adotante

  • Recurso global é condicional e sobreponível: bean de lib que ocupa uma rota, o ErrorResponseProcessor, um @ServerFilter ou o contrato de um endpoint cede quando o app já tem dono, por @Requires (bean ausente ou property). @Replaces troca o bean mas não desfaz a rota: rota duplicada persiste e cada request responde 400.
  • Exceção sob native: @Requires de classe num @Controller é avaliado em build-time pelo AOT e a rota é podada. Por isso os controllers flat de /health e /info da xadm-comum-web são incondicionais, e é o app que desliga o endpoint de management que disputaria a rota (endpoints.health.enabled: false, endpoints.info.enabled: false).
  • Recurso global tem de ser nomeável. Recurso que não colide em runtime mas é compartilhado (advisory lock, fila, tabela, prefixo de rota, chave de cache) aparece no ### Migração com o nome, a regra de derivação e a relação com o que o app já tem — nome derivado de property não aparece no grep do app nem no application.yaml.
  • Handler de controller publicado é public por contrato. Método executável private ou package-private não roteia e não pode ser estendido pelo adotante.
  • Módulo que sobe contexto no teste declara a impl de serde de runtime (micronaut-serde-jackson), não só a serde-api: sem ela não há JsonMapper e o @MicronautTest estoura.

Controller de lib fora do event loop

A lib não conhece o thread-selection do app que a adota: controller de lib com I/O bloqueante leva @ExecuteOn(TaskExecutors.BLOCKING). Controller sem I/O bloqueante (/health, /info) declara // event-loop-ok: <motivo>, que a guarda do pipeline.yml honra.

View empacotada em lib

  • Mora sob namespace, nunca na raiz do src/main/jte: o JTE transforma subdiretório em pacote, e login.jte na raiz da lib gera a mesma classe que a view do app, uma sombreando a outra sem erro. A lib serve xadm/<view>.jte e o controller referencia o caminho inteiro (@View("xadm/login")).
  • Motor de view entra na lib como compileOnly quando a tela é opcional (o login da xadm-seguranca) e como implementation quando a tela é o contrato (/admin/mensageria da xadm-mensageria); nos dois casos o jteGenerate não vaza no POM. O plugin faz implementation.extendsFrom(jteGenerate), então a extensão de build sairia como dependência de runtime de todo adotante. Cura, no módulo da lib — e confira no POM gerado, único lugar onde o defeito aparece:
configurations.named("implementation") {
    setExtendsFrom(extendsFrom.filterNot { it.name == "jteGenerate" }.toSet())
}
  • A reachability-metadata vai no jar: a lib aplica a extensão que gera o registro de reflexão das views e o publica em META-INF/native-image/, e o adotante o descobre sozinho no build native.
  • A view renderiza no teste da lib, pelo caminho do adotante: TemplateEngine.createPrecompiled(ContentType.Html) + engine.render("<namespace>/<view>.jte", model, out). O ContentType.Html escapa por contexto — dentro de <script>, / sai \/ —, então o teste afirma o valor escapado, não o literal.
  • O markup vem do jar e o CSS do kit: as classes da tela da lib moram no custom-theme.css do kit de UI, e nenhum gate amarra as duas versões. O CHANGELOG do módulo declara a versão mínima do kit que traz as classes; tela sem estilo = kit desatualizado, re-derive.