Publicar a documentação de uma aplicação¶
Cada aplicação builda e publica a própria documentação; o site central
(docs.xadm.biz) só agrega e serve. O contrato entre as partes é um bucket
no Garage:
flowchart LR
A[Repo do app] -- "CI: mkdocs build<br/>+ javadoc/dartdoc" --> S[(Garage<br/>docs-sites/<app>/)]
S -- "sync a cada ~90s" --> C[docs.xadm.biz<br/>/aplicacoes/<app>/]
O contrato¶
- O app publica um site estático completo no bucket
docs-sites, no prefixo com o nome do app:docs-sites/<app>/(precisa terindex.htmlna raiz do prefixo). - O site central sincroniza o bucket a cada ~90s e serve cada prefixo em
https://docs.xadm.biz/aplicacoes/<app>/. App novo = publicar no bucket — nenhuma configuração no repo central. - Referência de API é gerada pelo CI de docs (constituição §5), não pelo
build/deploy do app. O
docs.ymlroda o gerador da stack antes domkdocs build, para dentro dedocs/— então o link é Markdown normal e o--strictvalida (link quebrado = falha; sem HTML cru, sem cópia pós-build, sem guard de grep). Split de visibilidade: -
Javadoc/dartdoc (árvore de classes, interna) →
docs/dev/api/, linkada de uma páginadev/com[API Reference](api/index.html). Inventário de classes (opcional): preencher o marcador<!-- GERADO: inventario -->nodev/guia-do-codigo.md(1ª frase do doc de cada tipo) por uma de duas vias:- (a) passo no
docs.ymlque extrai o inventário e substitui o marcador no arquivo antes domkdocs build; - (b) hook do mkdocs do próprio app que preenche o marcador em
on_page_markdown— não muta o source nem exige passo de CI.
⚠️ Na via (b), liste o hook do app ANTES do
visao-tecnica.pyemhooks:. O hook central remove o marcador cru não preenchido; um hook que rode depois nunca o encontra → o inventário some sempre. Em qualquer via, sem preenchimento o marcador é removido e a seção fica só com o link para a API gerada — nunca embarca cru; - OpenAPI (contrato, público) →docs/public/api/openapi.yaml, embutido pordocs/public/api/index.mdviamkdocs-swagger-ui-tag(spec gerado do código, ex../gradlew classes+ micronaut-openapi — não do app deployado). Os artefatos gerados são.gitignore. O app pode seguir servindo/swagger-uiao vivo (explorador de runtime), mas a fonte publicada é o portal. Quem gera OpenAPI descomenta também ocheca-rotas.pynodocs.yml: publicar o contrato ao lado de uma prosa que o contradiz é pior que não publicar nenhum dos dois — oci.ymlpega o rename no PR de código, e aqui se pega a prosa editada sem tocar código (inclusive pela web do Forgejo). Ver CI, gates e testes. - (a) passo no
docs/ é privado por padrão — o público vive em docs/public/
O que é público é o que está em docs/public/ (public/index,
public/manual/, public/api/) — uma pasta, sem marcação por página nem
build separado. O resto de docs/ é privado (o gating de tudo-menos-/public/
é spec futura — por ora há um site só, servido como hoje, sem mexer em
infra). Guarda: uma página em public/ não pode linkar para fora de
public/ (o validador falha) — senão quebraria quando o portal trancar.
Sensível de verdade (dado de cliente) vai em anexos/privado/, que segue
excluído do build enquanto não há login. Toolchain do padrão
(/padroes/…) é pública por design. Regra: constituição §5.
O que o repo do app precisa ter¶
docs/no padrão da constituição §3 (index,projeto/,decisoes/,operacao/,public/commanual/+img/+api/,anexos/+anexos/privado/com seus READMEs,glossario.mdcom os termos específicos do projeto).docs/app.json— os metadados do app, que fazem o app aparecer automaticamente nas páginas Aplicações e API Reference do site central.mkdocs.ymlpróprio — copie o template canônicotemplates/mkdocs.ymldo mirror público e troque<app>pelo slug. Não copie de outro app: os clones divergem (já aconteceu —def_listfaltando quebrou a renderização de glossário)..forgejo/workflows/docs.yml— idem, copie o template canônicotemplates/docs.ymle ajuste<app>e os passos de API reference (Javadoc/dartdoc) conforme a stack. O branch émaster(já no template — constituição §5).
Consumo é via mirror público, não pelo Forgejo
Todo template/script para copiar vem de docs.xadm.biz/toolchain/
(a lista canônica e as versões estão no
manifesto.json). O
fonte.xadm.biz é o Forgejo privado — serve edição e hosting dos
repos, não consumo: dá 404 sem auth num bootstrap. Quem migra usa a
/xadm-docs, que já lê o manifesto e baixa do mirror.
Montando um app novo do zero? Siga o checklist de app novo.
Metadados do app (app.json)¶
A listagem nas páginas Aplicações e
API Reference do site central é automática: o central
descobre os apps pelo bucket e lê o app.json publicado na raiz do site de
cada um. Basta criar docs/app.json no repo do app (o MkDocs copia para o
site no build — nenhum passo extra no workflow):
{
"slug": "bi-transporte",
"app_id": "bi-vantroba",
"nome": "BI Transporte",
"descricao": "Dashboards do BI de transporte, via PowerSync.",
"grupo": "cliente",
"cliente": "Vantroba",
"stack": "Flutter/Dart",
"constituicao": "0.5.0",
"toolchain": { "flutter": "3.44.0" },
"features": {
"glitchtip": { "enabled": true, "dsn": "https://<chave>@bug.xadm.biz/<n>" },
"analytics": { "enabled": true, "aptabase_key": "A-SH-...", "aptabase_host": "https://aptabase.xadm.biz" }
}
}
| Campo | Obrigatório | Valores |
|---|---|---|
slug |
sim (recomendado) | fonte ÚNICA do identificador público (kebab-case, para sempre): pasta no bucket docs-sites/<slug>/, path do site_url, nome do .docx. O docs.yml e o export_docx derivam dele; o validador confere que o site_url do mkdocs.yml bate. App legado sem slug segue na migração oportunista. |
nome |
sim | nome de exibição |
descricao |
sim | uma frase |
grupo |
sim | xadm (app de produto) ou cliente (sob medida) — ver constituição §5 |
cliente |
se grupo: cliente |
nome do cliente, ex. Vantroba, Sulplata |
stack |
não | ex. Java/Micronaut — exibido na API Reference |
constituicao |
não (recomendado) | versão da constituição que a doc segue, ex. 0.5.0 — a skill /xadm-docs e o hook de sessão comparam com a vigente (publicada em /toolchain/constituicao-versao.txt) |
toolchain |
sim p/ Java/Flutter | versão de CI por stack, ex. { "java": "25" } ou { "flutter": "3.44.0" } — a fábrica de imagens builda a imagem-versão a partir daqui e o container: do CI a referencia. Deve bater com o .fvmrc (Flutter) / manifesto (Java). App docs-only não declara. |
app_id |
sim p/ Central de Apps | identidade canônica (kebab-case), mesmo namespace dos oauth_grants — ver Central de apps. App só-docs pode omitir. |
features |
não | objeto por funcionalidade (vocabulário fechado: glitchtip, garage, analytics, docs); cada bloco tem enabled + os refs client-grade que o /xadm-setup grava (build-time). O validador rejeita chave desconhecida. Ver Central de apps. |
flags |
não | feature flags do app — protocolo próprio (estado no DB do app; auth = painel). Ver Central de apps. |
- Layout versionado (decisão 0004):
com a doc versionada, o site de cada release vive em
docs-sites/<app>/<X.Y.Z>/(master →dev/), e a raiz<app>/guarda sóapp.json+versions.json+index.html(redirect → maior SemVer).app.jsonfica na raiz (não entra nas versões — senão quebra a fábrica de imagens). Quem escreve a raiz (contrato explícito): aapp.jsonvai à raiz em TODO push — odocs.ymlfazrclone copyto site/app.json …/<slug>/app.json(aditivo); já oversions.json+index.htmlsão release-only (publica-versao.py, só em tag). Sem o copyto, app que só publicoudev/cai em "Outros (sem app.json)" no portal. Nunca se fazrclone syncda raiz. - App sem
app.jsoncontinua sendo servido e aparece na listagem em "Outros", só com o slug — publique o arquivo para sair de lá. - A presença de API reference é detectada sozinha (existência de
api/no site publicado) — não há campo para isso. - O
valida-frontmatter.pyvalida oapp.jsonno CI junto com o frontmatter.
Importar um fato de outro repo¶
O --8<-- não atravessa repositório — mas o build atravessa. Quando o leitor precisa,
na mão, de um fato cujo dono é outro app (o contrato de ingestão de um hub, o formato de
um artefato compartilhado), o consumidor importa no build em vez de copiar: o fato chega
fresco do dono a cada build. É a mesma disciplina do validador fresco (constituição §6)
— e o oposto da cópia, que nasce certa e envelhece calada.
São dois lados, e o primeiro é obrigação do dono (§1.9): consumidor que copia é sintoma de dono que não publicou.
Lado do dono — publicar o fato em raw/¶
O fato vive em docs/public/contratos/ (é público: outros o consomem) e não é uma
página — é um fragmento, como o modelagem.md do livro. Logo, precisa de duas coisas
que andam juntas: uma hospedeira que o mostre e ficar fora do build.
A hospedeira é docs/public/contratos/index.md: uma página de verdade, no nav:, com
frontmatter, que apresenta o contrato e embute cada fato. É o mesmo padrão do
projeto/index.md embutindo o projeto/modelagem.md — quem lê tem uma página; quem
importa tem o cru; e existe um só dono do texto.
## Envelope, autenticação e resposta
--8<-- "public/contratos/xadm-ingest.md"
(sem o ; — ver a armadilha)
Fora do build com exclude_docs:, reincluindo a hospedeira:
exclude_docs: |
/public/contratos/*.md
!/public/contratos/index.md
not_in_nav: não serve aqui — ele não esconde, só cala o aviso
O fato continua sendo buildado: vira página pública com um H1 inventado do nome
do arquivo (Xadm ingest), alcançável por URL, e entra na busca — procurar
"Envelope" devolve a hospedeira e a órfã, com o mesmo texto. exclude_docs: tira do
build; o --8<-- continua transcluindo, porque lê do disco, e o raw/ também (o cp
abaixo copia do fonte). Use not_in_nav: para o que deve sair no site sem estar no
índice — o Javadoc gerado em dev/api/.
Para que o fato seja consumível por máquina, o docs.yml publica também o .md cru:
- name: Publicar arquivos-fato (consumidos por outros repos)
run: |
mkdir -p site/raw
cp docs/public/contratos/*.md site/raw/
E o rclone o manda para a raiz do prefixo do app — não para a subpasta da versão:
- name: Publicar raw na raiz (aditivo, todo push)
run: rclone copy site/raw "garage:docs-sites/${SLUG}/raw" --quiet
Por que a raiz, e não a versão: o raw/ é contrato entre apps, não conteúdo
versionado — exatamente o critério que já põe o app.json na raiz. O consumidor importa o
atual: o dono mudou o contrato, a doc do consumidor muda no próximo build, sem ninguém
lembrar de nada. Casa com o auto-deploy (o master do dono é o que está no ar). Como o
app.json: copy/copyto (aditivo) — nunca rclone sync da raiz.
A URL fica estável e determinística — https://docs.xadm.biz/aplicacoes/<slug>/raw/<fato>.md.
Não use .../aplicacoes/<slug>/ para máquina: a raiz serve um index.html de redirect
para a maior versão.
Convenção do arquivo-fato: a mesma dos snippets do livro — sem frontmatter, headings em
### (encaixam sob o ## da hospedeira), e exclude_docs:. A hospedeira, sim, é página:
leva frontmatter como qualquer outra (§6 — quem consome um contrato quer saber a data, o
status e o dono). O que é derivável do código não se escreve à mão aqui: o contrato REST é
o OpenAPI gerado (publique-o, §Referência de API); o raw/ serve o que a geração não
cobre — a prosa do contrato, listas que vivem no código (origens aceitas), o formato de um
artefato.
Schema em prosa não tem gate — atesta-se por teste de fio. Quando o fato descreve a
forma do corpo (campo obrigatório, enum, nullable, resultado: SUCESSO|ERRO), saiba
que nenhum check estático confere isso: o checa-rotas.py cobre rota, não corpo, e de
propósito — o OpenAPI gerado é oráculo confiável de rota, frágil de forma (não vê validação
imperativa, default, serialização custom). O que garante que a forma escrita aqui bate com a
realidade é o teste de fio que exercita o endpoint real (§6: E2E na fronteira de
contrato). Escrever a forma "a partir do código" lendo o código não basta se o teste não
a trava: prosa e código podem driftar juntos até um cliente quebrar. Detalhe:
ci-testes §Contrato REST.
Lado do consumidor — importar e embutir¶
- name: Importar fatos de outros repos (frescos)
run: |
mkdir -p docs/_importado
curl -fsSL https://docs.xadm.biz/aplicacoes/integrador/raw/xadm-ingest.md \
-o docs/_importado/xadm-ingest.md
curl -f falha o build se o dono despublicar ou renomear o fato — ruidoso de propósito:
o alerta é o build vermelho, não um leitor descobrindo meses depois. Na página, embute:
--8<-- "_importado/xadm-ingest.md"
(sem o ; — ver a armadilha). O
docs/_importado/ é efêmero: entra no .gitignore e em exclude_docs: (não em
not_in_nav: — pelo mesmo motivo do lado do dono, e aqui o estrago é maior: o fato
importado viraria página no site do consumidor, servindo o contrato do dono como se
fosse dele — a cópia que este mecanismo existe para matar, de volta pela porta dos fundos).
Versionar o importado o transformaria de volta numa cópia — que é o que se está evitando.
A doc do consumidor passa a mudar sem PR dele
É o objetivo (o §2 quer estado atual, e um pin envelhece calado), mas é a consequência
a aceitar conscientemente: mudou o contrato no dono, o próximo build do consumidor
publica o contrato novo. Quem precisa de contrato estável não resolve na doc —
resolve com versão de API no dono (/api/v1/), que é o mecanismo certo para isso.
Validação no CI¶
O workflow valida antes de publicar:
mkdocs build --strict— links quebrados, nav inconsistente.lint-mermaid.mjs— cada bloco``mermaid passa pormermaid.parse(Node + jsdom, **sem Chromium**); diagrama com erro de sintaxe renderiza **cru** no site e escapa do--strict, então aqui **falha o build**. Publicado no site (https://docs.xadm.biz/toolchain/lint-mermaid.mjs); odocs.ymlfaznpm install mermaid jsdome roda contradocs/`.valida-frontmatter.py— campos obrigatórios, enums detipo/status/modulo, formato deresponsavel:(Nome <email>) e deatualizado:(AAAA-MM-DD),superado-por:em decisão obsoleta (e que a cadeia de supersessão chega a uma decisão viva),cliente:cruzado com oapp.json(app de cliente exige o campo igual em todo doc; app da X-Adm não leva), massa de dados emdocs/public/bloqueada (superfície exposta; em pasta privada é livre, confidencial emprivado/— §5). É a validação prometida na constituição §6. O script é mantido no repo central e publicado neste site (https://docs.xadm.biz/toolchain/valida-frontmatter.py) — é de lá que o workflow dos apps baixa (o repo central é privado; o site é público). Ovalida-frontmatter.pyisentadocs/projeto/modelagem.mdedocs/projeto/mapeamento.md(fragmentos sem frontmatter, embutidos no livro — constituição §5).
Duas formas de cada script publicado (carimbo × sha)
Todo script publicado existe em duas formas, com propósitos distintos:
- Executável (carimbada):
https://docs.xadm.biz/padroes/<script>.py— tem a versão da constituição na 1ª linha (substituída no publish). É o que o CI baixa e roda, e o que a/xadm-docsconfere por versão (1ª linha == vigente). O sha desta forma muda a cada bump (a versão muda) — por isso não é usada para integridade por sha. - Crua (sha-verificável):
https://docs.xadm.biz/toolchain/raw/scripts/<script>— sem carimbo (placeholder intacto), casa com osha256domanifesto.json(que é o sha do arquivo cru do repo, igual aos templates emraw/). É a forma de verificar integridade, estável entre bumps (só muda se a lógica mudar).
Confundir as duas formas dá "sha não bate" (o carimbado nunca bate com o
manifesto, por design) — verifique sha sempre contra raw/, versão sempre contra
a forma carimbada.
Aviso opcional: modelo de dados × migrations (CI de código)¶
Projeto com banco mantém docs/projeto/modelagem.md e o evolui no mesmo PR que
evolui o schema (constituição §5). O checa-modelagem.py (publicado em
https://docs.xadm.biz/toolchain/checa-modelagem.py) é o reforço opcional: no
ci.yml, avisa (nunca falha) se o diff mexe numa migration sem tocar o
modelagem.md — e aponta sinais de transição inacabada no SQL (DROP COLUMN,
"legado"). É opt-in (precisa de python3 e de fetch-depth: 0 no checkout) —
descomente o passo no ci.yml.
Só faz sentido em projeto com banco; sem migrations no diff, fica quieto.
Toolchain — fonte da verdade das versões¶
Os workflows dos apps pinam estas versões. Para mudar de versão, mude aqui primeiro e replique nos workflows — não deixe um repo subir de versão sozinho:
| Pacote | Versão |
|---|---|
| mkdocs | 1.6.1 |
| mkdocs-material | 9.7.6 |
| pymdown-extensions | 10.21.3 |
| mkdocs-print-site-plugin | 2.8 |
| mkdocs-swagger-ui-tag | 0.8.0 |
Aviso conhecido: o Material já anuncia mudanças incompatíveis no futuro MkDocs 2.0. Quando o bump for inevitável, atualize esta tabela, o
requirements.txtdo repo central e osdocs.ymldos apps no mesmo movimento.
Ambiente local (Linux/Ubuntu)¶
Para rodar mkdocs build --strict e mkdocs serve na sua máquina sem criar
venv a cada uso: o Ubuntu bloqueia pip install fora de venv
(PEP 668, "externally-managed-environment"). O caminho recomendado é o
pipx, que instala CLIs Python isoladas mas disponíveis direto no PATH —
você chama mkdocs como qualquer comando, sem ativar nada:
sudo apt install pipx
pipx ensurepath # uma vez; garante ~/.local/bin no PATH (reabra o terminal)
pipx install mkdocs==1.6.1
pipx inject mkdocs mkdocs-material==9.7.6 pymdown-extensions==10.21.3 mkdocs-print-site-plugin==2.8 mkdocs-swagger-ui-tag==0.8.0
Use as versões da tabela acima (o exemplo já as reflete). Quando a tabela
mudar, atualize com pipx install --force mkdocs==X.Y.Z e repita o
pipx inject com as versões novas.
Evite pip install --break-system-packages na máquina de trabalho: ele
desliga a proteção do PEP 668 e pode conflitar com pacotes Python do apt.
(No CI/Docker, dentro de container, tudo bem — o Dockerfile do site
central faz isso.)
Credenciais¶
O bucket docs-sites tem duas keys fixas — nada é provisionado por app
(ver Garage):
- Escrita — usada pelo CI de todos os apps, configurada uma vez como
secrets da organização
xadmno Forgejo (DOCS_S3_ENDPOINT,DOCS_S3_ACCESS_KEY,DOCS_S3_SECRET_KEY). Repo na organização herda os secrets automaticamente. - Leitura — usada pelo site central para sincronizar o bucket
(envs
DOCS_S3_*no Coolify).
São as únicas credenciais do fluxo: não há token de Git nem chamadas entre CIs — quem quer publicar doc só precisa escrever no bucket. (Key por app existe só para bucket de dados do próprio app, ex. as planilhas de um processador — outra credencial, fora deste fluxo.)
Falhou o CI? Aviso no Telegram¶
Job quebrado manda mensagem no chat de CI da X-Adm (com o link da run). É um webhook nativo da organização no Forgejo — nada no YAML do app, vale para todos os workflows (docs, build, testes) automaticamente. Detalhes em Forgejo e CI.