Pular para o conteúdo principal

ADR 0030 — Política de branches mecanizada

Contexto​

A política de branches do Brabo existia como apresentação e como convenção no CLAUDE.md: esteira dev → qa → main, taxonomia de branches, promoção só entre degraus vizinhos, versão calculada, hotfix voltando por retropropagação.

Convenção escrita não é mecanismo. Enquanto a regra mora só no documento, ela depende de todo mundo lembrar dela na hora errada — e a hora errada é sempre a de incidente, às 3h da manhã, quando lembrar é justamente o que não acontece. Pior: uma regra que ninguém verifica não tem como ser violada visivelmente. Ela é violada em silêncio, e o sintoma aparece meses depois, longe da causa.

A FASE 6 mecanizou a política inteira no próprio repositório do Brabo. Este ADR fecha a fase: mapeia regra → mecanismo, registra o que foi cortado com o custo de reintroduzir cada corte, e separa o que disso vira template para o bootstrap de Gitflow do produto — que é fase futura, não código agora.

Duas restrições moldaram tudo. A primeira: este é um repositório de usuário, não de organização. Times do GitHub não existem aqui, e o GITHUB_TOKEN não lê membership de time nem em organização. A segunda: o produto não pode ser tocado nesta fase. A FASE 6 é CI/CD do repositório; o que ela ensina ao produto vira este ADR, não código.

Decisão​

Regra → mecanismo​

regra da políticamecanismoonde a lógica vive
nome funcao/descritivo, regex ^.{0,15}/\S{0,32}$check pr-policescripts/ci/pr-police.ts
prefixo na lista fechada de 9 funçõespr-policeidem
trabalho nasce de dev, hotfix de mainpr-police, por contaminaçãoidem
destino coerente com a funçãopr-policeidem
promoção só entre degraus vizinhospr-police + promotepr-police.ts, version.ts
label de família em todo PRpr-policeidem
aprovação exigida por destinocheck approval-ladderscripts/ci/approval-ladder.ts
dois modos de aprovação por variávelapproval-ladderidem
pessoas distintas em mainemparelhamento por backtrackingidem
versão pelo maior impacto do ciclopromote + tag-releasescripts/ci/version.ts
N incrementa por reprovaçãocontagem das tags existentesidem
final ancorada na última -qa.Ntag-release, por árvore + paiidem
hotfix gera PATCH sem âncoratag-release, pelo segundo paiidem
push direto bloqueadorulesetsdocs/reference/rulesets.md
retropropagação obrigatória e ordenadacheck backmerge-gatescripts/ci/gate.ts
merge em permanente é sempre manualausência de mecanismo, por decisão—

Toda lógica é script testável; workflow é casca fina que lê ambiente e chama o script. São 149 testes cobrindo caminho felizes e casos de falha.

As sete lições que valeram mais que o código​

Elas estão aqui porque cada uma custou uma descoberta empírica, e nenhuma está óbvia em documentação de terceiros.

1. Cada família de gatilho lê o workflow de um lugar diferente. pull_request e push leem da branch do evento; pull_request_target e workflow_dispatch, da branch padrão. Com pull_request_target o pr-police teve zero execuções — a main estava 65 commits atrás. É ovo-e-galinha: o check que faz a esteira andar precisa já estar na main para poder rodar.

2. Tag e PR criados com o GITHUB_TOKEN não disparam workflow. Regra contra recursão. A Release da v0.2.0 nunca publicou por isso, e um PR de retropropagação sem check nunca ficaria verde — a cadeia travaria para sempre. Daí o BRABO_BOT_TOKEN.

3. "Não consegui verificar" nunca pode virar "está tudo bem". O promotion-check lia a configuração de merge com gh api --jq e comparava com 'true'. Quando o token não tem permissão, o comando tem sucesso devolvendo vazio — e vazio não é 'true', então a verificação passava a reprovar por motivo errado, ou pior, a aprovar. Hoje há três estados, e a impossibilidade vira aviso enquanto a garantia real olha o fato consumado: o commit tem dois pais.

4. Igualdade de árvore é mais forte que igualdade de commit. A âncora comparava shas — impossível de satisfazer, porque --no-ff cria um commit novo. Comparar árvore e exigir que a -qa.N seja pai é mais forte: se o outro lado do merge tivesse trazido um arquivo, a árvore mudaria.

5. Contaminação, não origem. Descobrir de onde uma branch nasceu é indecidível a posteriori: com P ⊆ Q ⊆ head, o argmin de distância escolhe a permanente mais avançada contida, não a de origem. A pergunta certa é outra — "esta branch carrega o topo de uma permanente mais avançada que a que ela declara?" — e essa tem resposta.

6. Isenção por autor, nunca por prefixo. Isentar branches que começam com dependabot/ seria uma brecha aberta: qualquer um nomeia uma branch assim.

7. Estado declarado tem que ser conferível. O .release/gate.json viaja nos PRs de retropropagação para qa e dev; uma promoção pode subir aquela cópia velha de volta e ressuscitar uma trava sem hotfix por trás — travando o repositório para sempre, porque não há retropropagação pendente que a resolva. O check pergunta ao git se o hotfix já desceu e deixa cair travas satisfeitas. locked é o registro da intenção; a contenção é a verdade.

O que foi cortado, e o custo de voltar​

cortadopor quêo que custa reintroduzir
degrau rc / UATsem ambiente e sem gente para exercê-lo, seria degrau cerimonialcriar a branch, uma linha em ESCADA, uma em ESTAGIO_POR_BRANCH, uma na escada de aprovação, e a ordem do gate ganha um degrau — a cadeia do hotfix passa a ser quatro PRs
taxonomia com rcfixmorreu com o degrau rcentrada em FUNCOES_DE_CORRECAO_ALTA mais os testes de origem e destino
times do GitHub para papéisrepo de usuário não tem times, e o GITHUB_TOKEN não lê membership nem em orgpapéis são listas de handles em variáveis; migrar para times exigiria a org e um token com read:org
deploypasso que nunca roda apodrece: ninguém o testa, e no dia de ligar estará erradoworkflow próprio disparado pela tag, mais os Environments — nada a religar aqui
GitHub Environmentsidemcriar quando houver ambiente
GOVERNANCE.mdcortado na FASE DOC; o critério mora no branching-policy.mdescrever o arquivo e mover a seção de papéis para lá

A escada de aprovação em modo community não foi cortada: está implementada, testada e desligada por configuração. A diferença é deliberada — código desligado por variável, com teste dos dois lados, é demonstrável hoje; passo de pipeline desligado por variável não é.

O que vira template do bootstrap do produto​

O produto provisiona repositórios com Gitflow (FASE 2). O que a FASE 6 aprendeu e que deve entrar nesse template, em fase futura:

entra no templatenão entra
a esteira e a taxonomia como dados, não código — o produto tem projetos com escadas diferentesos workflows do Brabo copiados literalmente
o gate de retropropagação: é a regra que mais custa quando faltao BRABO_BOT_TOKEN — cada repo precisa da sua credencial
versão calculada da tag, sem PR de bumpas listas de aprovadores deste repo
"não consegui verificar" como terceiro estado explícito nos gates—
isenção de bot por autor—

O GitProvider já expõe capabilities, e o ADR 0028 registra que proteção de branch divirge entre providers. O template terá de degradar: onde não houver ruleset, a regra vira check e a mensagem diz o que não pôde ser garantido — em vez de fingir garantia.

Consequências​

A política deixou de depender de memória. Nome errado de branch, promoção pulando degrau, hotfix sem retropropagação e tag final desancorada são agora falhas de check com mensagem que ensina, não descobertas tardias.

A esteira foi exercitada de ponta a ponta, não só testada em unidade. v0.1.0 → v0.2.0-dev.1..4 → -qa.1..3 → v0.2.0, com uma reprovação encenada entre os carimbos de qa para provar que o N conta sozinho. A final ancorou na -qa.3 com árvore idêntica.

Um check required que nunca roda trava o PR para sempre. É a consequência mais incômoda: a lista de checks obrigatórios é agora um acoplamento entre o ruleset (aplicado à mão) e os workflows (versionados). Job novo no CI ou entra na lista de rulesets.md, ou fica de fora de propósito e alguém escreve por quê. É por isso que o backmerge-gate não tem if: no job: veredito velho colado a um sha que não rodou liberaria merge durante trava.

O bypass do gate é do ator, não do caminho. Rulesets do GitHub não restringem bypass por path. Quem pode escrever .release/gate.json pode, tecnicamente, escrever qualquer coisa em main. O que limita de fato é o workflow e o histórico. Fica registrado como limitação da ferramenta.

Aplicação de ruleset continua manual. O repositório versiona a fonte (docs/reference/rulesets.md); aplicar é ato do owner. Automatizar exigiria um token com poder de mudar as próprias proteções — o que anularia o ponto delas.

A concentração de papéis no owner está escrita, não subentendida. Responsável de release e plantão de hotfix são o owner enquanto APPROVAL_MODE=solo. As duas atribuições reabrem na migração para community, onde o fallback de plantão terá de ser exceção documentada, nunca burla.

Nada do produto mudou. Nenhuma linha de apps/ foi tocada. O que a fase ensinou ao produto está na tabela de template acima, e virou plano, não código.

Referências: ADR 0028 (divergência de proteção entre providers), 0029 (o mecanismo de docs que esta fase usa), e a política completa em branching-policy.