- O histórico que ninguém consegue ler
- O que é Conventional Commits
- A anatomia completa de uma mensagem
- Tipos de commit: guia de referência rápida
- Os 5 erros mais comuns
- Configurando commitlint + Husky do zero
- De commits para CHANGELOG automático
- FAQ
- Próximos passos
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
featefixno 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
| Tipo | Quando usar | Bump de versão |
|---|---|---|
feat | Nova funcionalidade visível ao usuário ou consumidor da API | minor |
fix | Corrige um bug | patch |
docs | Só documentação — README, JSDoc, comentários inline | nenhum |
style | Formatação, ponto-e-vírgula, espaçamento — zero mudança de lógica | nenhum |
refactor | Mudança interna que não é feat nem fix | nenhum |
perf | Melhoria de performance mensurável | patch |
test | Adiciona ou corrige testes | nenhum |
chore | Manutenção: bump de dependência, configs de CI, scripts de build | nenhum |
ci | Mudanças em arquivos de pipeline (GitHub Actions, Dockerfile) | nenhum |
revert | Reverte um commit anterior — referencie o hash | patch |
build | Mudanças no sistema de build ou dependências externas | nenhum |
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 defeat(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.mdcom 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-versionestá em maintenance mode desde 2022 e não recebe atualizações. Migre pararelease-pleaseousemantic-releaseem 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.
- Agora: escreva o próximo commit com
feat:oufix:e a descrição em imperativo. Só isso. - Esta semana: instale
commitlint+huskyno projeto principal e teste o hook localmente. - Este mês: configure
release-pleaseno CI e gere o primeiro CHANGELOG automático. Veja a versão subir sozinha depois de umfeat. - Documente: crie um
CONTRIBUTING.mdcom 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.