Pular para conteúdo

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/&lt;app&gt;/)]
    S -- "sync a cada ~90s" --> C[docs.xadm.biz<br/>/aplicacoes/&lt;app&gt;/]

O contrato

  1. O app publica um site estático completo no bucket docs-sites, no prefixo com o nome do app: docs-sites/<app>/ (precisa ter index.html na raiz do prefixo).
  2. 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.
  3. Referência de API é gerada pelo CI de docs (constituição §5), não pelo build/deploy do app. O docs.yml roda o gerador da stack antes do mkdocs build, para dentro de docs/ — então o link é Markdown normal e o --strict valida (link quebrado = falha; sem HTML cru, sem cópia pós-build, sem guard de grep). Split de visibilidade:
  4. Javadoc/dartdoc (árvore de classes, interna) → docs/dev/api/, linkada de uma página dev/ com [API Reference](api/index.html). Inventário de classes (opcional): preencher o marcador <!-- GERADO: inventario --> no dev/guia-do-codigo.md (1ª frase do doc de cada tipo) por uma de duas vias:

    • (a) passo no docs.yml que extrai o inventário e substitui o marcador no arquivo antes do mkdocs build;
    • (b) hook do mkdocs do próprio app que preenche o marcador em on_page_markdownnão muta o source nem exige passo de CI.

    ⚠️ Na via (b), liste o hook do app ANTES do visao-tecnica.py em hooks:. 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 por docs/public/api/index.md via mkdocs-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-ui ao vivo (explorador de runtime), mas a fonte publicada é o portal. Quem gera OpenAPI descomenta também o checa-rotas.py no docs.yml: publicar o contrato ao lado de uma prosa que o contradiz é pior que não publicar nenhum dos dois — o ci.yml pega 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.

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/ com manual/ + img/ + api/, anexos/ + anexos/privado/ com seus READMEs, glossario.md com 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.yml próprio — copie o template canônico templates/mkdocs.yml do mirror público e troque <app> pelo slug. Não copie de outro app: os clones divergem (já aconteceu — def_list faltando quebrou a renderização de glossário).
  • .forgejo/workflows/docs.yml — idem, copie o template canônico templates/docs.yml e 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.json fica na raiz (não entra nas versões — senão quebra a fábrica de imagens). Quem escreve a raiz (contrato explícito): a app.json vai à raiz em TODO push — o docs.yml faz rclone copyto site/app.json …/<slug>/app.json (aditivo); já o versions.json + index.html são release-only (publica-versao.py, só em tag). Sem o copyto, app que só publicou dev/ cai em "Outros (sem app.json)" no portal. Nunca se faz rclone sync da raiz.
  • App sem app.json continua 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.py valida o app.json no 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:

  1. mkdocs build --strict — links quebrados, nav inconsistente.
  2. 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/`.
  3. valida-frontmatter.py — campos obrigatórios, enums de tipo/status/modulo, formato de responsavel: (Nome <email>) e de atualizado: (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 o app.json (app de cliente exige o campo igual em todo doc; app da X-Adm não leva), massa de dados em docs/public/ bloqueada (superfície exposta; em pasta privada é livre, confidencial em privado/ — §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). O valida-frontmatter.py isenta docs/projeto/modelagem.md e docs/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-docs confere 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 o sha256 do manifesto.json (que é o sha do arquivo cru do repo, igual aos templates em raw/). É 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.txt do repo central e os docs.yml dos 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 xadm no 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.