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 TB
    A[Repo do app] -- "CI: mkdocs build<br/>+ javadoc/dartdoc" --> S[(Garage<br/>docs-sites/&lt;app&gt;/)]
    S -- "sync no publish (event-driven)" --> 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 prefixo do app quando o publish dispara (event-driven — o job docs avisa 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 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 (Níveis de documentação), não pelo build/deploy do app. O job docs do pipeline.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 job docs do pipeline.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_markdown — nã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 job docs do pipeline.yml. Publicar o contrato ao lado de uma prosa que o contradiz é pior que não publicar nenhum dos dois. O gate do pipeline.yml pega 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 do openapi.yaml, um módulo de contrato <svc>-api. É um artefato Maven — group br.com.xadm, nome <slug>-api pelo slug de registry (0024) — com os DTOs @Serdeable e a interface @Client das operações. Sai do próprio repo para o registry Forgejo, por publishToMavenRepository com FORGEJO_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 no xadm-commons). Onde o caller não é Java (skill/build-time/ externo), não há módulo — o openapi.yaml acima É o contrato.

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 por curl.
  • 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/ com manual/ + img/ + api/, anexos/ + anexos/privado/ com seus READMEs, glossario.md com os termos específicos do projeto). Repo com código tem também dev/guia-do-codigo.md e dev/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 em dev/.
  • 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).
  • .github/workflows/pipeline.yml — copie o template canônico templates/pipeline.yml e 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.json fica na raiz (não entra nas versões — é metadado do app, lido pela raiz). Quem escreve a raiz (contrato explícito): a app.json vai à raiz em TODO push — o job docs do pipeline.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.
  • A tabela apps do db_auth é ESPELHO do app.json, e só a /xadm-setup a escreve. O caminho único é o POST /api/setup/provision, que faz upsert do app a partir do payload; o catálogo não lê o app.json publicado, nem sozinho, nem no deploy. Consequência prática: editar o app.json não muda a Central — o valor novo só chega na próxima /xadm-setup daquele 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 /provision leva o bloco de identidade INTEIRO do app.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 sem stack e um entrou sem slug. Obrigatórios de verdade, fail-closed: nome, grupo e, para grupo: cliente, um cliente_id já cadastrado em clientes (a FK; tenant novo se cria antes, pelo CRUD admin).
  • O app.json nã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.json continua 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 em public/api/) é servida dentro do site do app; o portal central não mantém índice próprio dela.
  • 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. À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:

  1. mkdocs build --strict — links quebrados, nav inconsistente. O mkdocs.yml canônico liga validation.nav.omitted_files: warn (um .md fora do nav vira bloqueador, não o INFO silencioso do default — órfã não publica escondida) e links.not_found: warn (link markdown pra alvo fora de docs/ falha). Autoria: não linke código-fonte por caminho relativo — [x](../../lib/foo.dart) aponta pra fora de docs/, não resolve, e o --strict aborta (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 de privado/: o Forgejo exige login, e o link numa página aberta não serve a quem a lê. Regra: Convenções de doc.
  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); o jobdocsdopipeline.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 fora de privado/ bloqueada (o mkdocs publica o arquivo mesmo fora do nav; exceções: sob privado/ e .docx de 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). O valida-frontmatter.py isenta docs/projeto/modelagem.md e docs/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.
  4. checa-admonition.py, depois do build — !!!/??? que o Markdown não reconheceu sai como parágrafo cru, e o --strict nã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.
  5. checa-nav.py, no job gate (a cada PR de código, que não roda mkdocs), reprova duas coisas. A primeira é .md de docs/ fora do nav: e não excluído — a mesma órfã que o validation.nav.omitted_files pega no --strict, antecipada para o PR que a criou. A segunda é chave YAML repetida no mkdocs.yml, que o YAML resolve apagando a primeira em silêncio: um segundo exclude_docs: apaga a exclusão de privado/ 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-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: 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.txt do repo central e os pipeline.yml dos 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.