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. OmavenLocal()fica depois do registro e só parabr.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-releasedo app recusa dependência da casa fora do registro: no runner dopipeline.ymlnã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 --offlineresolve 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 -- "<release> = 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-releasedo app bumpa, migrando. - Abaixo do piso a
/xadm-releaserecusa a release; override só declarado no relatório, com o motivo. O CI só avisa (::warning::na guarda de pisos dopipeline.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-releasedoxadm-commonsfaz a leva nos adotantes (bump, migração, gate e commit local em cada um); o deploy de cada app sai pela/xadm-releasedele. - Transitiva: o app não confere dependência transitiva. A guarda de publish do
xadm-commonsrecusa 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:
- Remova o código local que a lib passou a prover (e o teste dele): dois donos do mesmo recurso
colidem no boot (dois
@Controllercom/login, doisErrorResponseProcessor). - Ajuste config, env e template ao contrato novo que a seção descreve.
- 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@ServerFilterou o contrato de um endpoint cede quando o app já tem dono, por@Requires(bean ausente ou property).@Replacestroca o bean mas não desfaz a rota: rota duplicada persiste e cada request responde 400. - Exceção sob native:
@Requiresde classe num@Controlleré avaliado em build-time pelo AOT e a rota é podada. Por isso os controllers flat de/healthe/infodaxadm-comum-websã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çãocom 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 nogrepdo app nem noapplication.yaml. - Handler de controller publicado é
publicpor contrato. Método executávelprivateou 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ó aserde-api: sem ela não háJsonMappere o@MicronautTestestoura.
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, elogin.jtena raiz da lib gera a mesma classe que a view do app, uma sombreando a outra sem erro. A lib servexadm/<view>.jtee o controller referencia o caminho inteiro (@View("xadm/login")). - Motor de view entra na lib como
compileOnlyquando a tela é opcional (o login daxadm-seguranca) e comoimplementationquando a tela é o contrato (/admin/mensageriadaxadm-mensageria); nos dois casos ojteGeneratenão vaza no POM. O plugin fazimplementation.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). OContentType.Htmlescapa 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.cssdo 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.