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 TB
A[Repo do app] -- "CI: mkdocs build<br/>+ javadoc/dartdoc" --> S[(Garage<br/>docs-sites/<app>/)]
S -- "sync no publish (event-driven)" --> 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 prefixo do app quando o publish dispara
(event-driven — o job
docsavisa o central, que notifica o container docs; decisão 0028) + um rede de segurança lento (reconcile no boot + poll de 60min); serve cada prefixo emhttps://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 (Níveis de documentação), não pelo
build/deploy do app. O job
docsdopipeline.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 job
docsdopipeline.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.pyno jobdocsdopipeline.yml. Publicar o contrato ao lado de uma prosa que o contradiz é pior que não publicar nenhum dos dois. O gate dopipeline.ymlpega o rename no PR de código; aqui se pega a prosa editada sem tocar código, inclusive pela web do Forgejo. Ver CI, gates e testes. 4. Contrato server-server tipado (receptor M2M). Um app que recebe chamada de outro app Java da casa publica, além doopenapi.yaml, um módulo de contrato<svc>-api. É um artefato Maven — groupbr.com.xadm, nome<slug>-apipelo slug de registry (0024) — com os DTOs@Serdeablee a interface@Clientdas operações. Sai do próprio repo para o registry Forgejo, porpublishToMavenRepositorycomFORGEJO_PACKAGES_*, o mesmo mecanismo da 0019. O caller declara a dependência por versão e injeta a interface — o contrato é checado no compilador (decisão 0020). O<svc>-apié artefato do receptor (não entra noxadm-commons). Onde o caller não é Java (skill/build-time/ externo), não há módulo — oopenapi.yamlacima É o contrato. - (a) passo no job
docs/ é privado por padrão — o público vive em docs/public/
Três classes de acesso (Convenções de doc):
- Toolchain do padrão (constituição, validadores, hooks, manifesto, templates crus em
/padroes/e/toolchain/) é pública por design: os CIs dos apps a baixam porcurl. - Doc interna do app (pré-projeto, projeto, decisões, dev, operação, anexos) é privada.
- O público do app é o que está em
docs/public/(public/index,public/manual/,public/api/) — uma pasta, sem marcação por página nem build separado.
Guarda: página em public/ não linka para fora de public/ (o validador falha); quando o
portal trancar tudo menos /public/, esse link quebraria.
Enquanto o portal não exige login, ele é um site só e serve docs/ aberto, exceto
privado/: esse segmento sai sempre do build (exclude_docs:, a guarda que reprova privado/
dentro do site/ e o --exclude do rclone). Dado real de cliente ou confidencial fica em
privado/ (ex. anexos/privado/); arquivo com extensão fora da lista do validador (um .json
com dado real) é publicado, e o risco é do autor. O que não é doc (ferramenta, insumo de build,
fixture) fica fora de docs/, em scripts/ ou no fonte.
O que o repo do app precisa ter¶
docs/no padrão de Repo e frontmatter (index,projeto/,decisoes/,operacao/,public/commanual/+img/+api/,anexos/+anexos/privado/com seus READMEs,glossario.mdcom os termos específicos do projeto). Repo com código tem tambémdev/guia-do-codigo.mdedev/como-rodar.md(Níveis de documentação); referência não-prosa publicável (schema, exemplo de requisição) pode ficar ao lado deles emdev/.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)..github/workflows/pipeline.yml— copie o template canônicotemplates/pipeline.ymle ajuste<slug>, a variante da stack e os passos de API reference (Javadoc/dartdoc/OpenAPI) conforme a stack. O branch émaster(já no template — Branch principal).
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 na página Aplicações 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": "vantroba-bi",
"perfil": "app",
"app_id": "vantroba-bi",
"nome": "BI",
"descricao": "Dashboards do BI de transporte, via PowerSync.",
"grupo": "cliente",
"cliente": "Vantroba",
"cliente_id": "vantroba",
"stack": "Dart/Flutter",
"constituicao": "2.0.0",
"kit": "2.0.1",
"toolchain": { "flutter": "3.44.0" },
"build": { "targets": ["web"] },
"smoke": { "bases": ["https://bi.vantroba.xadm.biz"], "routes": [ { "path": "/health", "class": "public" } ] },
"features": {
"glitchtip": { "enabled": true, "dsn": "https://<chave>@bug.xadm.biz/<n>", "project": "vantroba-bi" },
"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 uma vez publicado): pasta no bucket docs-sites/<slug>/, path do site_url, nome do .docx e nome da imagem no registry (fonte.xadm.biz/xadm/<slug>). Forma: <cliente_id>-<projeto> (cliente) / <projeto> (xadm) — decisão 0024. O job docs do pipeline.yml e o export_docx derivam dele; o validador confere que o site_url do mkdocs.yml bate. App que ainda não tem slug o ganha na migração oportunista. |
nome |
sim | nome de exibição, derivado do slug: o slug sem o prefixo <cliente_id>-, em CAIXA ALTA, com o hífen de família virando - — integrador-sascar → INTEGRADOR - SASCAR, central-ui → CENTRAL - UI, onpetro-bi → BI, sulplata-ho-scraper → HO SCRAPER. É o que a Central de Apps mostra no card, com o cliente logo abaixo — por isso o nome não repete o cliente, e apps de clientes diferentes com o mesmo projeto têm o mesmo nome de propósito (dois BI, dois XLS); a grade ordena globais primeiro, depois por cliente e por nome. O catálogo só recebe o valor novo na próxima /xadm-setup do repo. |
descricao |
sim | uma frase |
grupo |
sim | xadm (app de produto) ou cliente (sob medida) — ver Convenções de doc |
cliente |
se grupo: cliente |
rótulo de exibição do cliente, ex. Vantroba, Sulplata |
cliente_id |
se grupo: cliente |
chave canônica do tenant, minúscula kebab, ex. vantroba — a plataforma escopa por ela, nunca pelo rótulo (Central de Apps) |
production_url |
não | URL de produção para a Central de Apps linkar, ex. https://bi-transporte.xadm.biz |
stack |
não | rótulo curto no formato <linguagem ou runtime>/<framework> — Java/Micronaut, Dart/Flutter, Node.js/Patchright; sem framework, só a linguagem (Java 8 + Powersync SDK (Kotlin) é o limite do aceitável). Exibido no card da Central, ao lado do nome — descrição de cenário ((cliente on-premise), — Processador XLS) vai na descricao, não aqui. |
perfil |
não | app (padrão quando ausente), config (stack de serviço self-hosted) ou lib (o xadm-commons) — Perfis de repo. Perfil config e lib usam o app.json mínimo: slug, perfil, constituicao, kit. |
constituicao |
não (recomendado) | versão da norma que o repo segue, ex. 2.0.0 — a /xadm-docs e o hook de sessão comparam com a vigente (publicada em /toolchain/constituicao-versao.txt). É o único lugar do número: o CLAUDE.md aponta para o campo. |
kit |
não | versão do kit (templates, scripts, skills) que o repo sincronizou, ex. 2.0.0; quem grava é a /xadm-docs ao re-derivar, e ausente vale 0.0.0. Norma e kit têm números próprios — Três versões. |
toolchain |
sim p/ Java/Flutter | versão de CI por stack, ex. { "java": "25" } ou { "flutter": "3.44.0" } — fonte única: o job decide do pipeline.yml a exporta e o gate e os builds a usam (0027); o .fvmrc e o FROM do Dockerfile batem com ela (guarda). Obrigatório quando há build.targets; app só de docs não declara. |
build |
não | { "targets": [...] }, subconjunto de jar, native, web: quais imagens os jobs build_* do pipeline.yml buildam fora do host de produção. Server Micronaut: ["native"]; jar só com motivo declarado (0033). Flutter web: ["web"]. O app é native se e só se targets contém native — o validador, as guardas do pipeline.yml e o e2e leem este campo. Cada release sobrepõe pelo trailer Deploy: <alvos> que a /xadm-release crava na tag; build.targets é o fallback. Sem o campo, o repo não deploya imagem (lib, CLI on-prem). |
smoke |
sim com build.targets |
as rotas que não podem quebrar — o /health e uma de cada classe de credencial que o app serve: bases (uma por instância), routes (path, class = a credencial que a rota exige, method, status) e, para a classe que só tem rota com efeito colateral, sem_rota ({classe: motivo}). Lido pelo e2e native e pelo smoke pós-deploy — Smoke de produção. |
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. É igual ao slug, e igual também a features.glitchtip.project: uma identidade por app (0038, que substituiu a cláusula da 0024 sobre divergir). O validador barra a divergência; renomear é migração em duas fases, com a release do app no meio. |
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; central-backend = 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 — é metadado do app, lido pela raiz). Quem escreve a raiz (contrato explícito): aapp.jsonvai à raiz em TODO push — o jobdocsdopipeline.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. - A tabela
appsdodb_authé ESPELHO doapp.json, e só a/xadm-setupa escreve. O caminho único é oPOST /api/setup/provision, que faz upsert do app a partir do payload; o catálogo não lê oapp.jsonpublicado, nem sozinho, nem no deploy. Consequência prática: editar oapp.jsonnão muda a Central — o valor novo só chega na próxima/xadm-setupdaquele repo, e app que nunca rodou a skill não existe no catálogo (não aparece na grade, e nenhum SQL o cria com as features). - O payload do
/provisionleva o bloco de identidade INTEIRO doapp.json—app_id,nome,descricao,grupo,cliente_id,slug,production_url,stack,toolchain—, não só os campos que o broker exige. Campo omitido é preservado (o upsert é merge, não replace), mas quem omite deixa o catálogo defasado em silêncio: foi assim que três apps ficaram semstacke um entrou semslug. Obrigatórios de verdade, fail-closed:nome,grupoe, paragrupo: cliente, umcliente_idjá cadastrado emclientes(a FK; tenant novo se cria antes, pelo CRUD admin). - O
app.jsonnão tem campo de deploy: o mapa app → recurso Coolify tem um dono só, o central, e o reconcile casa pelo slug da imagem (no compose, pelo slug do repo git), nunca por nome nem por uuid — Deploy. - App sem
app.jsoncontinua sendo servido e aparece na listagem em "Outros", só com o slug — publique o arquivo para sair de lá. - A API gerada (Javadoc/dartdoc em
dev/api/, OpenAPI empublic/api/) é servida dentro do site do app; o portal central não mantém índice próprio dela. - 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. Às vezes o leitor precisa do fato
na mão, e o dono dele é outro app: o contrato de ingestão da Plataforma de Integração, o formato
de um artefato compartilhado. Nesse caso o consumidor importa no build em vez de copiar, e o fato
chega fresco do dono a cada build.
É a mesma disciplina do validador fresco (Versões), e o oposto da cópia, que nasce certa e envelhece calada. Onde um link basta, linka; onde o leitor precisa do fato na mão, importa.
São dois lados, e o primeiro é obrigação do dono (Duas fontes da verdade): consumidor que copia é
sintoma de dono que não publicou. O dono publica o OpenAPI gerado e/ou um arquivo-fato em raw/.
O dono é o repo onde o fato é verdade (o código, o schema, o spec gerado), não o projeto que dirige a evolução dele. Um projeto que puxa features de outro escreve o PR lá, cria o arquivo-fato lá e o importa aqui; do contrário um serviço compartilhado passaria a ter um dono por consumidor.
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
Os globs de exclude_docs: e not_in_nav: seguem o formato do .gitignore, não o do shell: *
não cruza /, vence a última linha que casa e ! reinclui.
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 job docs do pipeline.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. Como o app.json, o raw/ vai à raiz em todo push do master do dono, com
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 (Frontmatter — 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 — o contrato, item 3); 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 —, 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 e frágil de forma, porque não vê validação imperativa, default nem serialização
custom. O que garante que a forma escrita aqui bate com a
realidade é o teste de fio que exercita o endpoint real (definição de pronto: 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 se afastar 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
# use o slug ATUAL do dono (0024) — aqui o integrador é 'integrador-server', não 'integrador'
curl -fsSL https://docs.xadm.biz/aplicacoes/integrador-server/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:, nunca em
not_in_nav:. O motivo é o mesmo 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.
A doc do consumidor passa a mudar sem PR dele
É o objetivo (o livro 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. Omkdocs.ymlcanônico ligavalidation.nav.omitted_files: warn(um.mdfora do nav vira bloqueador, não o INFO silencioso do default — órfã não publica escondida) elinks.not_found: warn(link markdown pra alvo fora dedocs/falha). Autoria: não linke código-fonte por caminho relativo —[x](../../lib/foo.dart)aponta pra fora dedocs/, não resolve, e o--strictaborta (quebra recorrente pós-tag). Cite o fonte em code span (`lib/.../foo.dart`— sem link, imune a rename e ao strict). URL do source-browser do Forgejo (https://fonte.xadm.biz/xadm/<repo>/src/branch/master/lib/...) só em página deprivado/: o Forgejo exige login, e o link numa página aberta não serve a quem a lê. Regra: Convenções de doc.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); o jobdocsdopipeline.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 fora deprivado/bloqueada (o mkdocs publica o arquivo mesmo fora do nav; exceções: sobprivado/e.docxde pré-projeto — Convenções de doc). É a validação prometida na definição de pronto. 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 — Convenções de doc). Duas regras de conteúdo:- avisa (não falha) sobre identificador de código volátil em título ou heading
(
*Test,*Service,*Controller,Bulk*…) — em título o nome interno envelhece a página inteira; descreva pelo papel, e cite o nome só quando ele é a interface; - falha quando a tabela de decisões de um doc de projeto afirma um status que contradiz o frontmatter da decisão linkada — a decisão é a fonte do próprio status; tire a coluna.
- avisa (não falha) sobre identificador de código volátil em título ou heading
(
checa-admonition.py, depois do build —!!!/???que o Markdown não reconheceu sai como parágrafo cru, e o--strictnão vê. Dentro de item de lista o admonition pede 4 espaços de indentação: parágrafo de continuação aceita 2, bloco novo não.checa-nav.py, no jobgate(a cada PR de código, que não rodamkdocs), reprova duas coisas. A primeira é.mddedocs/fora donav:e não excluído — a mesma órfã que ovalidation.nav.omitted_filespega no--strict, antecipada para o PR que a criou. A segunda é chave YAML repetida nomkdocs.yml, que o YAML resolve apagando a primeira em silêncio: um segundoexclude_docs:apaga a exclusão deprivado/com todos os gates verdes.
O repo central roda também o checa-numeracao.py: página que numera as seções (## 1., ## 2.)
numera 1..n sem duplicata e sem salto, conferindo por nível de heading e ignorando bloco de código.
Duas ## 12. são dois headings válidos com slugs distintos, e nenhum outro gate as vê.
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: 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 (Convenções de doc). O checa-modelagem.py (publicado em
https://docs.xadm.biz/toolchain/checa-modelagem.py) é o reforço: no
job gate, 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,
o marcador legacy e o equivalente em português). Vem ativo no gate da variante Java do
pipeline.yml, que já
faz checkout com fetch-depth: 0; sem migrations no diff, fica quieto.
O script compara a base com a árvore de trabalho: vê o que ainda não foi commitado, inclusive
arquivo novo. Por isso o aviso chega antes do commit. A /x-documentar o roda contra origin/master
no fechamento, e a /xadm-release contra a última tag, cobrindo o intervalo inteiro do release. Na CI,
a base é a do PR ou o before do push.
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 ospipeline.ymldos apps no mesmo movimento.
O print-site é sempre o último plugin do mkdocs.yml: fora de ordem, ele quebra o
mkdocs build --strict.
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.)
Windows / Python da Microsoft Store: o diretório Scripts\ do Python
pode não entrar no PATH → mkdocs --version dá "command not found"
mesmo com o pacote instalado (só o entry-point bare não resolve). Invoque via
python -m mkdocs … (ou py -m mkdocs …) — mesmo binário, entry-point
garantido. Regra geral: CLI Python que não resolve bare, caia pra
python -m <mod> (ferramentas §allow-list).
Credenciais¶
O bucket docs-sites tem duas keys fixas — nada é provisionado por app
(ver Garage):
- Escrita — usada pelo CI de todos os apps, cadastrada como secret de cada repo
no GitHub (
DOCS_S3_ENDPOINT,DOCS_S3_ACCESS_KEY,DOCS_S3_SECRET_KEY— decisão 0027; o CI é GitHub Actions, a conta é user-wide, não org — a mesma key vai em todo repo). - 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? Notificação do GitHub¶
Job quebrado dispara a notificação nativa do GitHub — email ao autor do commit (com o link da run). Nada no YAML, sem secret. Detalhes em Forgejo e CI · 0027.