Contribuindo¶
Tudo o que você precisa para desenvolver, testar e lançar esta biblioteca.
Veja também: Uso · Referência da API · o
CONTRIBUTING.mdna raiz do repositório contém a política autoritativa de branch/PR e de mensagem de commit.
Preparando o ambiente de desenvolvimento¶
O projeto traz um Makefile e um tasks.sh equivalente, então use o que couber na sua máquina —
make init ou bash tasks.sh init quando make não estiver disponível (por exemplo, num
shell padrão do Windows).
make init # cria o virtualenv do Poetry + instala deps + instala hooks de pre-commit
# ou, sem make:
bash tasks.sh init
init compõe venv (cria o virtualenv do Poetry e instala todas as dependências, inclusive
dev + docs) e precommit (instala os git hooks). O Poetry é instalado automaticamente se estiver
faltando.
Testes e lint¶
make unit_tests # poetry run pytest tests/unit/
make integration_tests # poetry run pytest tests/integration/
make lint # ruff + mypy + codespell + pydocstyle + gates de shell/sql/yaml/actions
A CI roda os mesmos gates em cada pull request; mantenha-os verdes localmente antes de dar push.
⚠️ O gate de workflows é o
actionlint, não oyamllint. Os dois respondem perguntas diferentes: oyamllintdiz se o arquivo é YAML bem-formado; oactionlintdiz se ele é um workflow que o GitHub roda. Umon:com evento inexistente passa no primeiro e faz o GitHub rejeitar o arquivo inteiro — o workflow não roda de forma alguma, e o sintoma (PR sem rótulo, sem comentário do gate) parece lentidão, não morte. Oactionlinté opcional localmente:make lintavisa e segue se ele não estiver instalado, mas na CI a ausência falha o job, de propósito. Para tê-lo aqui, baixe o binário da página de releases (https://github.com/rhysd/actionlint/releases) — ele não entra como dev-dep do Poetry: oactionlint-pyé sdist-only e baixa o binário em tempo de build, o que colocaria um download dentro de todopoetry install, nas três matrizes de SO.
Servindo a documentação localmente¶
Documentação versionada (mike + GitHub Pages)¶
O site é versionado com mike: cada release publica a sua
própria versão navegável na branch gh-pages, e o leitor alterna entre versões por um dropdown
no cabeçalho. O alias latest sempre aponta para a release mais recente, então a URL raiz do site
cai na versão mais nova.
Como adicionar uma página: crie o .md em docs/ e registre-o no nav: do mkdocs.yml no
mesmo commit. A referência da API é um diretório (docs/api/): a entrada de um novo leitor ou
serializador vai em docs/api/reference.md, nunca em um docs/api.md. As páginas obrigatórias
estão em docs/.docs-skeleton.yaml e são verificadas por bin/check_docs_sections.py.
Como cada peça se encaixa:
- Build-check (CI): o workflow
Docs - Build Checkrodamkdocs build --stricta cada push e PR — sem publicar. É só a garantia de que a doc sempre compila; um release nunca descobre uma árvore de docs quebrada na hora de publicar. - Deploy (CI): o job
Deploy Versioned Docs (mike)doRelease to PyPIroda depois do publish no PyPI (só documenta versão que efetivamente subiu):mike deploy --push X.Y latest+mike set-default --push latest, empurrando paragh-pagescom oGITHUB_TOKENembutido (contents: write) — sem token externo. A granularidade é minor (X.Y): um patch herda o slot da sua minor em vez de multiplicar entradas. Prereleases (rc1,b1, …) não movemlatest(o job é pulado para elas).
Configuração do mantenedor — uma única vez, com admin no repositório: apontar o GitHub Pages
para a branch gh-pages (o modelo mike), em vez da fonte GitHub Actions. O workflow não faz
isso: o GITHUB_TOKEN é um token de GitHub App que não altera as configurações do Pages.
Já roda dentro de make init / bash tasks.sh init. É idempotente e não-bloqueante, e — para
nunca deixar o site em 404 — só troca a fonte para gh-pages depois que essa branch existir
(o primeiro mike deploy do release a cria). Se rodar antes disso, ele avisa e não mexe no Pages;
basta rodar make enable_pages de novo após o primeiro release. Alternativa manual:
Settings → Pages → Build and deployment → Source: Deploy from a branch → gh-pages / (root).
Semear a versão atual imediatamente (opcional): se quiser publicar a versão já lançada sem esperar o próximo release, rode uma vez, com a branch limpa:
git config user.name "<você>" && git config user.email "<seu-email>"
poetry run mike deploy --push --update-aliases 0.18 latest
poetry run mike set-default --push latest
make enable_pages # agora que gh-pages existe, aponta o Pages para ela
Verificando o pacote construído¶
Antes de abrir um PR de release, confirme que a wheel realmente constrói e importa — isso pega
erros de empacotamento (um __init__ faltando, um subpacote _internal/ não incluído) que os
testes na árvore de fontes nunca revelam:
Pull requests¶
- Crie a branch a partir da branch padrão seguindo a política de prefixo (
feat/…,fix/…, …). - Preencha o template de PR por completo.
- Garanta que os checks de CI (testes, lint, build da documentação) passem — eles são o gate de merge.
O ruleset pr-quality-gate (revisão automática e proteção da branch)¶
A branch padrão é protegida por um branch ruleset chamado pr-quality-gate, provisionado
inteiramente por código — nenhuma caixinha marcada à mão:
Já roda dentro de make init / bash tasks.sh init. É idempotente (um ruleset existente é
atualizado no lugar, não duplicado) e não-bloqueante (sem gh, sem auth ou sem permissão de
admin, ele avisa e sai com 0 — o init completa). Exige gh autenticado com direitos de admin no
repositório: o GITHUB_TOKEN do CI não consegue escrever rulesets, por isso isto é um passo do
mantenedor, não um job de workflow.
O que o ruleset aplica:
| Regra | Efeito |
|---|---|
pull_request |
Toda mudança passa por PR. 0 aprovações exigidas — o GitHub não deixa o autor aprovar o próprio PR, então exigir ≥1 travaria um mantenedor solo. As conversas precisam estar resolvidas (required_review_thread_resolution), o que torna vinculante todo thread de review inline — na prática, hoje, os do CodeRabbit (medido: foi essa regra que bloqueou os PRs #207 e #210 até as threads serem resolvidas). ⚠️ Não os do Copilot: a regra copilot_code_review está inerte neste repo (ver abaixo — é limitação de plano de conta), então ele não abre thread nenhuma. |
required_status_checks |
Os testes (Run Automated Tests, nos 3 SOs) e o build da documentação bloqueiam o merge de fato. |
code_scanning |
CodeQL: alertas de segurança high_or_higher e alertas de erro bloqueiam o merge. |
copilot_code_review |
Revisão automática do Copilot em todo PR, revisando cada novo push (review_on_push). Grátis em repositórios públicos. |
non_fast_forward + deletion |
Sem force-push e sem deletar a branch padrão. |
Automático × manual — a fronteira é repositório × conta. Tudo que é configuração do
repositório é gravável pela REST API e já vem no script — inclusive a revisão do Copilot, que é um
rule type próprio (copilot_code_review), e não um parâmetro de pull_request (essa grafia
devolve HTTP 422, o que faz a regra parecer exclusiva da UI). Nenhuma caixinha precisa ser
marcada à mão. O outro pré-requisito de repositório — habilitar o CodeQL (default setup), sem o
qual a regra code_scanning não tem ferramenta para checar — também é API:
O que não dá para automatizar daqui é o direito de uso na conta, não no repositório: a regra
copilot_code_review só dispara "se o autor tiver acesso ao Copilot code review" — e o code
review não está incluído no Copilot Free (a própria tela de planos lista "AI reviews" como
recurso de upgrade). Sem um plano que o inclua (Pro / Pro+ / Business), a regra fica configurada e
correta, porém inerte: nenhuma revisão aparece e nada dá erro — o silêncio é a armadilha.
Como obter revisão automática de fato:
- Copilot Pro gratuito — é gratuito para estudantes, professores e mantenedores de projetos open-source populares (verificação em github.com/settings/billing).
- Sem plano nenhum — um workflow de
pull_requestchamando um LLM de tier gratuito (ex.: Gemini Flash) para comentar o PR. Independe do Copilot.
Regra geral: configuração de repositório → script (é o que este helper faz, 100%). Direito de uso de conta → depende do plano, e não se resolve do repositório. As demais regras do gate (PR obrigatório, CI verde, CodeQL limpo) funcionam independentemente do Copilot.
O painel de Quality Gate e o auto-merge¶
Todo PR recebe, automaticamente (workflow pr-gate.yaml → bin/pr_gate.py):
- Rótulos:
risk:*(classe de risco pelos caminhos alterados),size:*(XS…XL, pelo volume do diff) egate:passing/gate:pending/gate:failing—pendingnão éfailing: um check ainda rodando não diz nada sobre o resultado. - Um comentário fixo (atualizado no lugar, nunca empilhado) com a tabela por eixo — testes nos 3 SOs, build da documentação, CodeQL.
Idioma. O bot escreve em inglês, como todo o resto voltado a quem contribui (código, mensagens de commit, saída de CI). Só a documentação publicada segue o idioma do site (
mkdocs.yml→theme.language, pt-BR). A fronteira é o público, não o repositório.
Auto-merge — por caminho, nunca por tamanho. Um diff pequeno não é um diff seguro aqui: a
falha real desta biblioteca é semântica (uma coluna de FileContract aterrada errado, um date_ref
pegando a partição errada). Uma mudança de um caractere em _internal/config/contracts/ é
minúscula e catastrófica — e todos os testes passam, porque os testes afirmam o contrato que foi
escrito. Por isso a classificação é por caminho:
| Classe | Caminhos | Auto-merge? |
|---|---|---|
docs |
docs/**, *.md, mkdocs.yml |
✅ |
ci |
.github/**, bin/**, Makefile, tasks.sh, configs de lint |
✅ |
deps |
poetry.lock, pyproject.toml |
✅ (os testes são o gate) |
tests |
tests/** |
❌ — definem o que "passar" significa |
src |
src/opendata_cvm/** |
❌ nunca, em nenhum tamanho |
other |
qualquer outro | ❌ (desconhecido = inseguro) |
Numa classe auto-fundível, o auto-merge é opt-out: a classificação é o consentimento, então
nenhum rótulo é preciso para armá-lo — as bumps semanais do Dependabot fundem sozinhas. O rótulo
do-not-merge é a válvula de escape que segura um PR específico.
Um diff XL também não funde — exceto quando o único arquivo alterado é o poetry.lock. O veto
por tamanho pergunta "esse diff é grande o bastante para um humano olhar?"; num lockfile regenerado
a pergunta não significa nada, porque o tamanho acompanha quantos hashes de dependência se
moveram, não quanto risco chegou — uma bump de 3 ferramentas de dev deu 579 linhas (XL, vetada)
enquanto uma de 2 pacotes deu menos de 500 (L, fundida). O pyproject.toml não entra na
exceção: é lá, num range editado à mão, que o risco de dependência de fato mora.
E ele não burla nada: usa o auto-merge nativo do GitHub, que segura o merge até todos os checks obrigatórios do ruleset ficarem verdes. O script decide apenas elegibilidade; quem decide se passou continua sendo o ruleset. (Auto-aprovação seria inútil aqui: o ruleset exige 0 aprovações, então uma aprovação de bot não destravaria nada.)
Quando o merge é entregue — no fim do run, nunca no começo. O gate não arma o auto-merge ao
abrir o PR. O GitHub recusa enablePullRequestAutoMerge num PR que já poderia ser fundido
("Pull request is in clean status"), e anuncia a recusa com HTTP 200 + errors — não com um
status de erro. Armar no opened cai justamente no pior instante: nenhum check registrou ainda e
nenhuma thread de review existe, então o PR parece fundível, a mutação é recusada, e o silêncio
passa por sucesso. Foi esse o defeito do #214, e ele ficou invisível porque o log dizia
auto_merge=True.
Então a entrega acontece depois do poll: o gate tenta fundir direto (PUT /pulls/:n/merge) e cai
no auto-merge se a tentativa não tiver sucesso — o caso esperado é a recusa do GitHub enquanto
algo ainda bloqueia (405), mas a queda vale para qualquer falha da chamada, inclusive um erro
de rede que sobreviva às retentativas. Nesse ponto armar o auto-merge é exatamente o
comportamento certo: se algo bloqueia, ele é aceito e o GitHub assume a espera; se a falha foi de
rede, ele é a rede de segurança para um PR que talvez já pudesse fundir. E se o próprio armar for
recusado, a razão vai para o log — a recusa do GraphQL chega com HTTP 200 + errors, então o
gate lê o corpo, nunca o status. Nada disso burla o ruleset: os dois caminhos são validados do lado
do servidor.
⚠️ Não existe gatilho para "resolveram a thread".
pull_request_review_threadnão é aceito noon:— existe só como evento de webhook, e colocá-lo lá faz o GitHub rejeitar o arquivo inteiro ("failed because of a workflow file issue"): o gate não roda de forma alguma, e o PR fica sem rótulo e sem comentário, o que parece um gate apenas lento.pull_request_reviewepull_request_review_comment, por outro lado, são gatilhos válidos — só que nenhum deles dispara ao resolver uma thread, então não servem de substituto. E não precisa de nenhum: quem espera o último bloqueio cair — check obrigatório ou conversa não resolvida — é o próprio auto-merge nativo, sem nenhum run de workflow envolvido.
Duas regras da UI ficam deliberadamente desligadas, para não criar uma segunda fonte de verdade:
Require code quality results (severidade subjetiva de IA no caminho do merge — ruff, mypy e os
gates de bin/check_*.py já cobrem qualidade de forma determinística) e Restrict code coverage (o
piso já é single-source em .coveragerc, aplicado por pre-commit + CI).
Auto-merge e commit empilhado — o push que chega depois do merge é perdido¶
Numa classe auto-fundível (docs/ci/deps, sem arquivo de teste), o PR funde no instante em que
os checks ficam verdes, em segundos. Acrescentar um commit a um PR já aberto vira uma corrida — e
quando ela é perdida, o commit fica órfão. Medido em 3 ocorrências numa sessão (#221, #223, #225):
em dois casos o push teve sucesso e origin/<branch> avançou, mas o PR já estava mergeado — o
trabalho ficou na branch e em lugar nenhum além dela. Nada falha: o PR está verde e fundido, e o
commit simplesmente não está no main.
⚠️ É consequência de o auto-merge funcionar, não um defeito dele. Quanto melhor ele funciona, mais estreita a janela. E a
required_review_thread_resolutionnão protege: ela bloqueia thread existente e não resolvida, não espera um revisor chegar (o #223 fundiu com o CodeRabbit ainda "processando novas mudanças"). A política de auto-merge não muda — o que muda é o seu hábito.
Como evitar:
- Conclua o trabalho antes de abrir o PR numa classe auto-fundível.
- Se precisar continuar depois de aberto, aplique o rótulo
do-not-mergeantes do push seguinte — não depois. - Ou aceite abrir um segundo PR para o que vier depois.
Como detectar. Depois de todo push num PR já aberto, compare o PR com o HEAD local:
state=MERGED com headRefOid diferente do HEAD local significa commit órfão.
Como recuperar. Crie uma branch nova a partir do main atualizado, traga o commit com
git cherry-pick <sha> e abra um PR novo:
Higiene de branches — apagar depois do merge, arquivar as de incidente¶
Depois do merge, a branch vai embora. Só o main e a gh-pages vivem no remoto.
⚠️ Ancestralidade não diz nada aqui — o repo faz squash-merge. Um squash cria um commit novo, então os commits originais de uma branch nunca são ancestrais do
main, por mais que o conteúdo tenha entrado por completo. Portantogit branch -dsempre recusa,git log main..branchsempre mostra "N commits à frente" e ogit cherrysempre marca+(o squash muda o patch-id). Nenhum dos três é evidência de perda. Os testes honestos são: a PR dela mergeou? (gh pr list --head <branch> --state all) e a comparação de conteúdo (o blob de cada arquivo tocado está nomain?).
Branch de incidente vira tag de arquivo. Se a branch tem história que vale preservar — um merge parcial, commits órfãos, uma recuperação — não é preciso escolher entre guardar o registro e limpar a lista:
git tag archive/<issue>-<slug> <sha-do-tip>e publique a tag;- só então apague a branch.
A tag deixa os commits alcançáveis para sempre (imunes ao GC) e a lista de branches limpa. Depois,
git log archive/<issue>-<slug> devolve tudo. Precedente: archive/49-retry-strategy (o incidente do
49 — mergeou com 1 de 3 commits; o resto foi recuperado pelo #50). Custa ~10 segundos e transforma¶
"posso apagar essa branch?" de decisão corajosa em decisão trivial.
⚠️ Uma PR auto-fundida não apaga a própria branch nem fecha a issue linkada — o merge é feito pelo bot, e ações do
GITHUB_TOKENnão cascateiam para as automações a jusante. Até isso ser resolvido, depois de toda PR auto-fundida: feche a issue (gh issue close <N>) e apague a ref (gh api -X DELETE repos/:owner/:repo/git/refs/heads/<branch>). Sem o fechamento da issue, o card nunca chega a "Done" — o workflow nativo do Projects depende dele.ℹ️
git push origin --delete <branch>é bloqueado pelo hook local de proteção de branch quando omainestá em checkout (falso positivo — apagar a ref de outra branch não escreve nomain). Use a API dogh, como acima.
Lançando¶
Os releases são dirigidos por tag quando o projeto está conectado a um remoto no GitHub:
- A versão é a tag git (via
poetry-dynamic-versioning); opyproject.tomlguarda um placeholder0.0.0. Não edite à mão. Dispare um release pela aba Actions (Release to PyPI/Release to Test PyPI,workflow_dispatchcom a versão) ou empurrando uma tagvX.Y.Z. - O workflow de release roda a suíte completa de testes como gate rígido, constrói com
python -m builde publica via trusted publishing (OIDC) (pypa/gh-action-pypi-publish) — semPYPI_TOKENarmazenado. - O changelog é regenerado a partir das tags no momento do build (
make changeloglocalmente); a CI nunca faz commit deCHANGELOG.mdde volta na branch padrão protegida.
Configuração do mantenedor — trusted publisher (uma vez, antes do primeiro release)¶
Registre um trusted publisher tanto em pypi.org quanto em
test.pypi.org. Cada claim precisa bater exatamente com o workflow, ou o
upload falha com um invalid-publisher opaco:
| Claim | Valor |
|---|---|
| Owner / repository | seu <owner> / <repo> no GitHub |
| Workflow filename | release-pypi.yaml (PyPI) / release-test-pypi.yaml (Test PyPI) |
| Environment | release-pypi / release-test-pypi |
| PyPI Project Name | deve ser igual ao nome de distribuição (name no pyproject.toml) |
Para o primeiro upload o projeto ainda não existe — registre um pending publisher no nível da conta (não dentro das configurações de um projeto existente). Publicar de um laptop em vez da CI é o único caso que ainda precisa de um API token; OIDC só funciona a partir do GitHub Actions.