Conventional Commits na prática: do git log bagunçado ao CHANGELOG automático
← Voltar para Codeshort

Conventional Commits na prática: do git log bagunçado ao CHANGELOG automático

Aprenda a escrever commits que seu time (e você daqui a seis meses) consegue entender — e automatize CHANGELOG e versionamento de quebra.

DC
Dev Code Software
13 de maio de 2026·9 min de leitura

Conventional Commits na prática: do git log bagunçado ao CHANGELOG automático

O histórico que ninguém consegue ler

Abre o git log --oneline de um projeto seu de seis meses atrás. Existe uma chance considerável de você ver algo assim:

a3f91c2 fix
b12dd4e ajustes
c889012 wip
d004abc testando
e771f3a agora vai
f3390ba mais um fix

Parabéns: você construiu um cemitério de contexto. Ninguém sabe o que mudou, por que mudou, nem o que quebrou junto. Você precisaria abrir cada diff individualmente para entender qualquer coisa.

Agora imagina o mesmo projeto com Conventional Commits:

a3f91c2 fix(auth): corrige token expirado não sendo renovado
b12dd4e feat(checkout): adiciona validação de CPF no formulário
c889012 perf(db): adiciona índice em user_id na tabela orders
d004abc refactor(api): extrai camada de serviço do módulo de pagamento
e771f3a fix(cart): impede quantidade negativa no carrinho
f3390ba feat(profile)!: novo endpoint de upload de avatar

Você consegue fazer git bisect, gerar CHANGELOG automaticamente e calcular a próxima versão semântica sem reunião. Isso não é frescura de processo — é a diferença entre "aquela query pesada" ser rastreável ou não quando quebra às 22h em produção.


O que é Conventional Commits

É uma especificação para mensagens de commit que segue uma estrutura legível por humanos e por máquinas. Nasceu dentro do projeto Angular, onde o volume de commits tornava inviável manter um CHANGELOG manual. Hoje está documentada em conventionalcommits.org e é usada por projetos como Vue.js, Electron, ESLint e praticamente qualquer pacote npm relevante.

O ponto central: quando o commit tem estrutura previsível, ferramentas conseguem ler o histórico e fazer coisas úteis por você. Gerar CHANGELOGs. Calcular se a próxima versão é patch, minor ou major. Disparar pipelines específicos com base no tipo de mudança. Tudo isso sem nenhuma configuração extra além do formato da mensagem.

💡 Dica: Você não precisa adotar 100% da spec de uma vez. Comece só com feat e fix no próximo commit. O hábito se consolida antes da perfeição chegar.


A anatomia completa de uma mensagem

O formato é:

<tipo>[escopo opcional]: <descrição curta>

[corpo opcional]

[rodapé opcional]

Cada parte tem uma função:

  • tipo — o que a mudança faz (feat, fix, docs, etc.)
  • escopo — onde no código ela vive (opcional, mas recomendado)
  • descrição — imperativo, presente, sem ponto final, máximo 72 caracteres
  • corpo — contexto adicional, o por quê da mudança, não o o quê
  • rodapé — referências a issues, breaking changes

Na prática, os quatro padrões que você vai usar 90% do tempo:

feat: adiciona autenticação por magic link

fix(auth): corrige token expirado não sendo renovado

feat(api)!: remove suporte ao endpoint /v1/users

BREAKING CHANGE: o endpoint /v1/users foi removido. Use /v2/users com paginação.

fix(checkout): previne duplo clique no botão de pagamento

O debounce de 300ms não estava sendo aplicado em dispositivos mobile.
Usuários conseguiam disparar dois POST seguidos antes do redirect acontecer.

Closes #412

A ! após o tipo sinaliza breaking change na linha de assunto. O rodapé BREAKING CHANGE: adiciona descrição detalhada. Você pode usar os dois juntos ou só o rodapé — ambos fazem ferramentas como release-please calcularem um bump major.


Tipos de commit: guia de referência rápida

TipoQuando usarBump de versão
featNova funcionalidade visível ao usuário ou consumidor da APIminor
fixCorrige um bugpatch
docsSó documentação — README, JSDoc, comentários inlinenenhum
styleFormatação, ponto-e-vírgula, espaçamento — zero mudança de lógicanenhum
refactorMudança interna que não é feat nem fixnenhum
perfMelhoria de performance mensurávelpatch
testAdiciona ou corrige testesnenhum
choreManutenção: bump de dependência, configs de CI, scripts de buildnenhum
ciMudanças em arquivos de pipeline (GitHub Actions, Dockerfile)nenhum
revertReverte um commit anterior — referencie o hashpatch
buildMudanças no sistema de build ou dependências externasnenhum

A dúvida mais frequente: quando é refactor e quando é feat? A regra é simples — se o comportamento externo mudou, é feat. Se só o código interno mudou e o resultado é o mesmo para quem consome, é refactor.

Outro ponto que pega bastante: chore não é lixeiro. Não jogue tudo que você não sabe nomear como chore. Atualizar uma dependência de segurança é chore. Mudar a lógica de cálculo de desconto "rapidinho" não é.


Os 5 erros mais comuns

1. Misturar tipos na mesma mudança

feat: adiciona página de perfil, corrige bug no login, atualiza README

Isso não é um commit, é um episódio de série. Separa:

feat(profile): adiciona página de perfil do usuário
fix(auth): corrige redirect após login com OAuth
docs: atualiza instruções de setup no README

A regra prática: se a descrição usa vírgula ou "e", você deveria ter feito mais de um commit.

2. Usar o passado na descrição

A convenção usa imperativo. Pense assim: o commit descreve o que ele faz no codebase quando aplicado — como se fosse uma instrução para o repositório.

fix(cart): corrigiu problema com quantidade negativa

fix(cart): impede quantidade negativa no carrinho

3. Scope genérico demais

feat(app): adiciona validação de CPF

app não diz nada. Isso apareceu num PR revisado em 2023 e o comentário foi só: "qual parte do app?". Não havia resposta imediata. Bons scopes mapeiam módulos reais do projeto: checkout, auth, api, dashboard, notifications.

feat(checkout): adiciona validação de CPF no formulário de pagamento

4. Breaking change sem sinalização

Remover um parâmetro de uma função exportada, renomear um campo de resposta de API, mudar o comportamento de um hook — tudo isso é breaking change. Se você não usou ! ou BREAKING CHANGE:, o time inteiro vai descobrir da pior forma.

⚠️ Atenção: Em monorepos com múltiplos pacotes, uma breaking change sem ! passa despercebida no CHANGELOG automatizado e pode quebrar consumidores sem aviso. Se o repositório tem pacotes publicados separadamente, o escopo vira crítico: feat(ui-kit)!: remove componente Button legado é completamente diferente de feat(api)!: altera contrato do endpoint.

5. Commitar sem corpo quando o por quê não é óbvio

A descrição curta diz o quê. O corpo diz por quê. Quando a mudança tem contexto que não aparece no diff — uma decisão de negócio, um bug que só ocorre em condição específica, uma restrição de browser — escreva o corpo.

fix(payments): desativa split de pagamento para pedidos com cupom

O provider não suporta split quando há desconto percentual aplicado.
Pedidos nesse estado estavam falhando silenciosamente no webhook.
Temporário até resolver com o suporte do Pagar.me — issue #891.

Daqui a seis meses você agradece.


Configurando commitlint + Husky do zero

Você não precisa confiar na memória do time. Duas ferramentas resolvem isso: o Commitizen guia o commit de forma interativa, e o Commitlint + Husky rejeita commits fora do padrão antes de entrarem no repositório.

Setup completo em um bloco

npm install --save-dev @commitlint/cli @commitlint/config-conventional husky commitizen cz-conventional-changelog
export default {
  extends: ['@commitlint/config-conventional'],
};
{
  "scripts": {
    "prepare": "husky",
    "commit": "cz"
  },
  "config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
    }
  }
}
npx husky init
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg

A partir daqui, npm run commit abre o Commitizen interativo. Se alguém tentar git commit -m "ajustes finais" direto, o hook do Husky rejeita antes mesmo de criar o commit.

Por que isso importa para o CI

O ambiente local não reproduzia alguns problemas de padrão de commit porque desenvolvedores novos no projeto simplesmente não sabiam a convenção. Com o hook, o erro acontece localmente, com mensagem clara, antes de qualquer push. O CI não quebra por causa disso — e o revisor não precisa comentar no PR.

💡 Dica: Documente os tipos aceitos pelo projeto no CONTRIBUTING.md com exemplos reais. Não deixe o dev descobrir pela mensagem de erro do commitlint.


De commits para CHANGELOG automático

Esse é o ponto onde a convenção paga o investimento de verdade.

release-please (recomendado)

Mantido pelo Google, lê o histórico de commits, calcula a próxima versão semântica e abre um PR automaticamente com o CHANGELOG.md atualizado.

on:
  push:
    branches:
      - main

jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - uses: googleapis/release-please-action@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          release-type: node

A lógica de versionamento é determinística: fix → patch, feat → minor, qualquer ! ou BREAKING CHANGE → major. Sem reunião, sem discussão, sem esquecimento de atualizar o CHANGELOG na sexta-feira antes do deploy.

semantic-release (alternativa mais flexível)

Para projetos que precisam de mais controle sobre o pipeline de publicação — npm publish, Docker tag, GitHub Release — o semantic-release é mais poderoso:

npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git

Ele roda no CI, analisa os commits desde a última tag, decide a versão, atualiza o CHANGELOG, faz o publish e cria a release no GitHub. Tudo em uma etapa.

⚠️ Atenção: O standard-version está em maintenance mode desde 2022 e não recebe atualizações. Migre para release-please ou semantic-release em projetos novos.

Squash merge e títulos de PR

Se o seu time usa squash merge (comum em GitHub Flow), o título do PR vira o único commit que vai para main. Nesse caso, o título do PR precisa seguir Conventional Commits — não o commit individual. Configure proteção de branch exigindo título de PR padronizado ou use o GitHub Action amannn/action-semantic-pull-request para validar automaticamente.


FAQ

Preciso adotar desde o primeiro commit ou posso migrar um projeto existente?

Não precisa começar do zero. Em projetos existentes, o caminho mais limpo é definir um ponto de corte: a partir de determinada tag ou data, todos os commits novos seguem a convenção. Reescrever o histórico antigo com git rebase -i é tecnicamente possível mas cria problemas sérios em repositórios compartilhados — os hashes mudam, branches divergem, e o time fica com dor de cabeça por dias. Não vale. Deixa o histórico antigo como está e começa limpo daqui pra frente.

Faz sentido em projetos solo ou só tem valor em time?

Faz mais sentido do que parece em projetos solo. O benefício principal não é a comunicação com outras pessoas — é a comunicação com você mesmo daqui a seis meses. Um git log --oneline com Conventional Commits é um resumo executivo do que aconteceu no projeto. Sem isso, você vai precisar abrir cada diff para entender o contexto de qualquer mudança. Em projetos com publicação de pacotes npm, a automação de versão semântica sozinha já justifica a adoção.

Como fica com squash merge? Os commits individuais da branch somem?

Com squash merge, sim — os commits da branch viram um único commit na main, e o título do PR é o que fica no histórico. Por isso, com squash merge, a disciplina precisa estar no título do PR, não nos commits locais. Isso é vantagem: você pode fazer commits bagunçados durante o desenvolvimento (wip, testa isso, revert) e limpar na hora de abrir o PR com um título bem escrito. A maioria dos times que usa release-please adota exatamente essa estratégia.

E commits de WIP durante o desenvolvimento? Tenho que me preocupar?

Não durante o desenvolvimento ativo. O histórico que vai para main é o que importa. Use git rebase -i antes de abrir o PR para organizar, combinar (squash, fixup) ou reescrever os commits que precisam ficar individuais. A regra prática: um commit por unidade lógica de mudança — algo que poderia ser revertido sem afetar o resto.

O commitlint bloqueia devs novos que ainda não conhecem a convenção?

Vai bloquear, e isso é intencional — mas só funciona bem se o erro for explicativo. A mensagem padrão do commitlint é clara o suficiente, mas vale configurar um link para o CONTRIBUTING.md do projeto na mensagem de erro. Commitizen junto ao commitlint resolve 90% do problema: em vez de digitar a mensagem livre, o dev passa por um prompt guiado que ensina o formato na prática. Na primeira vez que alguém usa, o próprio processo já é o onboarding.


Próximos passos

Não tente implementar tudo de uma vez — o hábito não se forma assim.

  1. Agora: escreva o próximo commit com feat: ou fix: e a descrição em imperativo. Só isso.
  2. Esta semana: instale commitlint + husky no projeto principal e teste o hook localmente.
  3. Este mês: configure release-please no CI e gere o primeiro CHANGELOG automático. Veja a versão subir sozinha depois de um feat.
  4. Documente: crie um CONTRIBUTING.md com os tipos aceitos, exemplos do próprio projeto e um link para a spec.

Histórico de commits é documentação — a única documentação que fica junto com o código, versionada automaticamente, e que ninguém precisa lembrar de atualizar. O custo é escrever uma linha melhor por commit. O retorno aparece na primeira vez que você rastreia um bug no git bisect sem precisar adivinhar o que cada commit fez.