Pular para o conteúdo

Commits que geram changelog

Intermediário12 min de leituraversionamento

Mensagem de commit é a única documentação que sempre acompanha o código. “ajustes”, “fix”, “wip” custam caro no dia em que você faz git log procurando quando um comportamento mudou — e impedem qualquer automação em cima do histórico.

Conventional Commits é uma convenção mínima que resolve as duas coisas: o histórico fica legível para pessoas e processável por máquina.

<tipo>(<escopo>)!: <descrição no imperativo, minúscula, sem ponto final>
<corpo opcional: por que, não o quê — o diff já mostra o quê>
<rodapé opcional: BREAKING CHANGE, Refs: #482>
feat(checkout): aceitar pagamento em duas etapas
fix(api): tratar timeout do gateway sem derrubar a requisição
perf(consulta): usar índice parcial em pedidos por status
docs(readme): explicar variáveis de ambiente obrigatórias
refactor(auth)!: remover suporte a token v1
BREAKING CHANGE: clientes com token v1 precisam reautenticar.

Os tipos que cobrem quase tudo: feat, fix, docs, refactor, perf, test, build, ci, chore. O ! (ou o rodapé BREAKING CHANGE:) marca quebra de compatibilidade — é o que dispara o major.

Escreva a descrição como se completasse “Se aplicado, este commit vai…”. Isso resolve sozinho a dúvida entre imperativo e passado.

MAJOR.MINOR.PATCH é um contrato com quem consome:

  • PATCH (1.4.2 → 1.4.3): correção compatível. Atualizar deveria ser seguro.
  • MINOR (1.4.3 → 1.5.0): funcionalidade nova, compatível.
  • MAJOR (1.5.0 → 2.0.0): quebra. Quem consome precisa agir.

O mapeamento com a convenção é direto: fix → patch, feat → minor, ! → major.

SemVer faz sentido para biblioteca, API pública, módulo, chart e imagem base — coisas que outras pessoas consomem. Para uma aplicação implantada só por você, versionar por data ou pelo commit costuma ser mais honesto: ninguém precisa negociar compatibilidade com você mesmo.

Com a convenção no lugar, versão e changelog deixam de ser trabalho manual:

.github/workflows/release.yml
on:
push: { branches: [main] }
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # o histórico completo é o insumo
- uses: googleapis/release-please-action@v4
with: { release-type: node }

A ferramenta lê os commits desde a última tag, calcula a versão, gera o CHANGELOG.md, abre o PR de release e cria a tag no merge. release-please, semantic-release e changesets (bom para monorepo) resolvem o mesmo problema com estilos diferentes.

Para que isso não quebre, valide a mensagem antes do merge:

Janela do terminal
npx commitlint --from origin/main --to HEAD # no CI
npx commitlint --edit "$1" # em commit-msg, localmente

O ponto crítico é a estratégia de merge. Com squash merge, o que vira commit no tronco é o título do PR — então é o título que precisa seguir a convenção, e é ele que o CI deve validar. Com merge normal, valide os commits da branch.

Ela custa pouco, mas não é gratuita, e há casos em que só gera fricção:

  • Repositório de infraestrutura ou de conteúdo sem consumidores externos: o changelog automático não é lido por ninguém.
  • Time que ainda escreve “fix bug” com tipo na frente: fix: fix não é melhor que fix. A convenção organiza, não substitui o hábito de explicar.
  • Commits de bot (atualização de dependência) inflando o changelog: filtre por tipo, agrupe, ou marque como chore.

Se você adotar, adote com verificação automática. Convenção opcional produz metade do histórico organizado, que é pior que nenhum — porque a automação em cima dele passa a mentir.

Próximo passo: a mensagem entra pelo pull request, e o pull request passa por revisão. Continue em Revisão de código que agrega.