Você atualiza uma dependência do seu projeto, o build quebra, e passa horas descobrindo que a nova versão removeu uma função que você usava. Ou o cenário oposto: você lança uma “atualização” que quebra a API dos seus consumidores, e só descobre quando os tickets de suporte começam a chegar.
Esses dois cenários têm uma causa comum: ausência de um contrato claro sobre o que uma mudança de versão significa.
O Versionamento Semântico — SemVer, do inglês Semantic Versioning — existe para resolver exatamente esse problema. Criado por Tom Preston-Werner, cofundador do GitHub, o SemVer define um conjunto de regras simples e precisas para a atribuição de números de versão: cada componente do número carrega uma informação específica sobre o impacto das mudanças. Não é apenas uma convenção de nomenclatura — é um protocolo de comunicação entre quem desenvolve e quem consome software.
Neste guia, você vai entender o que cada componente do número de versão significa e quando incrementar cada um, como aplicar SemVer na prática com exemplos reais, quais ferramentas suportam e simplificam o processo, como automatizar o versionamento em pipelines de CI/CD e como superar os desafios mais comuns na adoção. Se você mantém uma biblioteca, uma API, um pacote open source ou qualquer software consumido por outras pessoas ou sistemas, este artigo é para você.
Por que versionamento mal feito é um problema?
Antes de entrar nas regras, vale entender o custo concreto de um versionamento sem convenção.
Em projetos com dependências, cada biblioteca tem sua própria política de versão. Sem um padrão compartilhado, “versão 2.0” pode significar coisas completamente diferentes para projetos diferentes — em um pode ser uma atualização de segurança, em outro pode ser uma reescrita total da API. Para quem gerencia dependências, essa ambiguidade é paralisante.
O fenômeno conhecido como dependency hell — o inferno das dependências — surge quando múltiplos pacotes têm dependências incompatíveis entre si, e não existe forma previsível de saber se uma atualização vai funcionar ou quebrar alguma coisa. O npm, o pip, o Maven e praticamente todos os gerenciadores de pacote modernos foram construídos assumindo que os pacotes seguem SemVer. Quando não seguem, o sistema inteiro de resolução de dependências fica comprometido.
💡 Dica: O próprio criador do SemVer descreve o problema que motivou a especificação: “Sem um padrão formal, os números de versão são essencialmente inúteis para comunicar as intenções do desenvolvedor.” Um número de versão que não comunica nada sobre o impacto das mudanças força cada consumidor a ler os release notes completos antes de cada atualização — e muitos não fazem isso, o que resulta em quebras inesperadas.
A estrutura do SemVer: MAJOR.MINOR.PATCH
O Versionamento Semântico usa um formato de três números separados por pontos:
MAJOR.MINOR.PATCH
Cada posição carrega uma informação específica e não arbitrária. A leitura de um número de versão como 3.7.2 deve comunicar imediatamente: versão principal 3, sétima adição de funcionalidade, segunda correção de bug dentro desse ciclo.
MAJOR: quando o contrato muda
O número MAJOR é incrementado quando são introduzidas mudanças incompatíveis com versões anteriores — o que na terminologia técnica chamamos de breaking changes.
Uma breaking change é qualquer alteração que force os consumidores do seu software a modificar seu próprio código para continuar funcionando. Exemplos concretos:
- Remoção de um método ou função que existia na versão anterior
- Mudança na assinatura de uma função (parâmetros obrigatórios adicionados, tipos alterados)
- Mudança no comportamento de uma função existente de forma que o código anterior produza resultados errados
- Remoção de endpoints de uma API REST
- Alteração de contratos de serialização/deserialização
Quando você incrementa MAJOR, está sinalizando: “se você depende de mim, precisará revisar e possivelmente modificar seu código antes de atualizar.”
1.4.2 → 2.0.0 (breaking change: API incompatível com versão 1.x)
Após um incremento MAJOR, os números MINOR e PATCH são zerados. 2.0.0, não 2.4.2.
⚠️ Atenção: O incremento MAJOR não é uma punição ou um sinal negativo — é uma comunicação honesta. O erro mais comum na adoção de SemVer é evitar incrementar MAJOR para “não assustar os usuários”, entregando breaking changes disfarçadas de incrementos MINOR. Isso é exatamente o oposto do que o SemVer propõe, e destrói a confiança dos consumidores no seu esquema de versionamento.
MINOR: quando novas capacidades são adicionadas
O número MINOR é incrementado quando novas funcionalidades são adicionadas de forma retrocompatível — ou seja, código que funcionava com a versão anterior continua funcionando sem modificações.
Exemplos de incremento MINOR:
- Adição de novos métodos ou funções à API existente
- Novos endpoints em uma API REST que não alteram os existentes
- Novos módulos ou componentes opcionais
- Extensão de funcionalidades existentes de forma aditiva
- Deprecação de funcionalidades (avisar que algo será removido futuramente, sem remover ainda)
1.4.2 → 1.5.0 (nova funcionalidade retrocompatível)
Após um incremento MINOR, o número PATCH é zerado. 1.5.0, não 1.5.2.
Consumidores que não precisam das novas funcionalidades podem ignorar a atualização sem impacto. Consumidores que querem as novas funcionalidades podem atualizar com confiança de que nada vai quebrar.
PATCH: quando problemas são corrigidos
O número PATCH é incrementado quando são feitas correções de bugs e melhorias que não afetam a compatibilidade com versões anteriores.
Exemplos de incremento PATCH:
- Correção de um bug que causava comportamento incorreto
- Melhoria de performance sem alteração de interface
- Correção de documentação
- Ajustes de segurança que não alteram a API pública
- Refatorações internas sem impacto no comportamento externo
1.4.2 → 1.4.3 (correção de bug)
Incrementos PATCH são as atualizações mais seguras — consumidores podem (e devem) adotar com mínima avaliação de risco, pois o contrato permanece idêntico.
Versões especiais: pré-release e build metadata
O SemVer também define formatos para versões que ainda não são estáveis ou que carregam metadados de build.
Versões pré-release
Versões pré-release usam um identificador após o PATCH, separado por hífen:
1.0.0-alpha
1.0.0-alpha.1
1.0.0-beta.2
1.0.0-rc.1
Uma versão pré-release tem precedência menor que a versão de release correspondente: 1.0.0-alpha < 1.0.0. Isso comunica que o software ainda não é estável e pode mudar antes do release final.
Convenções comuns:
- alpha — versão inicial, instável, pode ter features incompletas
- beta — feature-complete, mas ainda em teste
- rc (release candidate) — candidata ao release final, apenas bugs críticos seriam motivo para não lançar
Build metadata
Metadados de build são adicionados após um +:
1.0.0+20240315
1.0.0-beta.1+exp.sha.5114f85
Build metadata é ignorado na comparação de versões — 1.0.0+build.1 e 1.0.0+build.2 são consideradas a mesma versão em termos de precedência.
A versão 0.x.x: o território de desenvolvimento inicial
Um caso especial importante do SemVer é o tratamento de versões com MAJOR igual a zero:
0.1.0
0.4.2
0.9.0
Versões 0.x.x sinalizam que o software está em desenvolvimento inicial e a API pública não deve ser considerada estável. Qualquer versão 0.y.z pode introduzir breaking changes no MINOR. Isso dá liberdade para iterar rapidamente sem o compromisso formal da compatibilidade retroativa.
O primeiro release estável e pronto para produção deve ser 1.0.0. Esse é o marco que estabelece o contrato público com os consumidores.
💡 Dica: Muitos projetos ficam em 0.x por muito mais tempo do que deveriam por insegurança em “comprometer” com 1.0.0. Se seu software está em produção sendo consumido por outros, está na hora de mudar a versão para 1.0.0. Essa versão não significa perfeição — significa que você estabelece um contrato público e vai seguir as regras do SemVer a partir dali.
Exemplos práticos: lendo e escrevendo versões no dia a dia
Sequência típica de um projeto
0.1.0 — primeira implementação funcional, API ainda instável
0.2.0 — novas funcionalidades adicionadas
0.2.1 — correção de bug encontrado em 0.2.0
0.9.0 — versão quase completa, em fase de estabilização
1.0.0 — primeiro release estável, API pública estabelecida
1.0.1 — hotfix de segurança crítico
1.1.0 — nova funcionalidade adicionada retrocompatívelmente
1.1.1 — correção de bug em funcionalidade de 1.1.0
1.2.0 — mais funcionalidades adicionadas
2.0.0 — redesign da API com breaking changes
2.0.1 — correção de bug encontrado logo após o release 2.0.0
2.1.0 — nova funcionalidade na versão 2.x
Interpretando versões de dependências
Ao declarar dependências em gerenciadores de pacote, o SemVer habilita formas expressivas de especificar compatibilidade:
npm (package.json):
{
"dependencies": {
"express": "^4.18.0", // Aceita qualquer 4.x.x >= 4.18.0 (não aceita 5.x.x)
"lodash": "~4.17.21", // Aceita apenas patches: 4.17.x >= 4.17.21
"axios": "1.6.0" // Versão exata — sem flexibilidade
}
}O operador ^ (caret) aceita atualizações MINOR e PATCH mas não MAJOR. O ~ (tilde) aceita apenas PATCH. A ausência de operador fixa a versão exata.
pip (requirements.txt):
requests>=2.28.0,<3.0.0 # Aceita qualquer 2.x.x >= 2.28.0
flask~=2.3.0 # Compatível com 2.3, aceita 2.3.x mas não 2.4.0
django==4.2.7 # Versão exata
Maven (pom.xml):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.0</version>
</dependency>Maven usa versões fixas por padrão, mas o Spring BOM (Bill of Materials) gerencia a compatibilidade entre dependências do ecossistema Spring automaticamente.
Ferramentas que suportam e simplificam o SemVer
Gerenciadores de pacote
Os principais gerenciadores de pacote de cada ecossistema foram construídos sobre os princípios do SemVer:
npm e yarn para JavaScript/Node.js são os que implementam SemVer de forma mais rica, com os operadores de range (^, ~, >=, <) e resolução automática de compatibilidade.
pip para Python tem suporte a SemVer com os operadores de compatibilidade (~=, >=, <), embora a comunidade Python seja menos rígida na adoção da especificação completa.
Composer para PHP implementa SemVer completamente, com resolução de dependências que entende os contratos MAJOR/MINOR/PATCH.
Cargo para Rust é um dos exemplos mais rigorosos de adoção do SemVer — o compilador Rust e o ecossistema Crates.io são construídos com SemVer como princípio fundamental.
semantic-release: automatização completa do versionamento
O semantic-release é uma ferramenta que automatiza todo o ciclo de versionamento com base nas mensagens de commit, seguindo o padrão Conventional Commits.
A ideia é simples e poderosa: se as mensagens de commit seguem uma convenção estruturada, é possível determinar automaticamente o tipo de incremento de versão necessário.
Conventional Commits define o formato:
tipo(escopo): descrição
feat: adiciona endpoint de exportação CSV
fix: corrige cálculo de desconto para pedidos negativos
feat!: redesenha API de autenticação — breaking change
docs: atualiza README com exemplos de uso
chore: atualiza dependências de desenvolvimento
Com semantic-release configurado, ao fazer push para a branch principal:
- Commits fix: → incremento PATCH automático
- Commits feat: → incremento MINOR automático
- Commits com ! ou footer BREAKING CHANGE: → incremento MAJOR automático
- Geração automática de CHANGELOG
- Criação de tag Git
- Publicação no registro de pacotes (npm, PyPI, etc.)
⚠️ Atenção: Para usar semantic-release de forma eficaz, toda a equipe precisa adotar o padrão de mensagens Conventional Commits consistentemente. Um commit fix(auth): corrige validação de token é informação; um commit atualizações é ruído. A qualidade da automação depende diretamente da disciplina nas mensagens de commit.
Automatizando SemVer com CI/CD: exemplos reais
GitHub Actions com semantic-release
# .github/workflows/release.yml
name: Release
on:
push:
branches:
- main
jobs:
release:
name: Semantic Release
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Necessário para análise do histórico de commits
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Semantic Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}O arquivo de configuração do semantic-release (.releaserc.json):
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
["@semantic-release/git", {
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}],
"@semantic-release/github"
]
}GitLab CI/CD
# .gitlab-ci.yml
stages:
- test
- release
test:
stage: test
image: node:lts
script:
- npm ci
- npm test
only:
- merge_requests
- main
release:
stage: release
image: node:lts
script:
- npm ci
- npx semantic-release
only:
- main
variables:
GL_TOKEN: $GITLAB_TOKENVersionamento manual com Commitizen
Para times que preferem controle manual mas querem padronizar as mensagens de commit, o Commitizen fornece um CLI interativo:
# Instalação
npm install -g commitizen cz-conventional-changelog
# Ao invés de git commit, use:
git cz
# O Commitizen guia o desenvolvedor por um prompt interativo:
? Select the type of change that you're committing:
❯ feat: A new feature
fix: A bug fix
docs: Documentation only changes
style: Changes that do not affect the meaning of the code
refactor: A code change that neither fixes a bug nor adds a feature
perf: A code change that improves performance
test: Adding missing tests or correcting existing tests
? What is the scope of this change? (press enter to skip)
auth
? Write a short, imperative tense description of the change:
add Google OAuth provider
? Is there any breaking change? No
? Does this change affect any open issues? NoO resultado: feat(auth): add Google OAuth provider — uma mensagem padronizada sem precisar memorizar a convenção.
Boas práticas e erros comuns
O Changelog: a documentação que acompanha as versões
Um número de versão comunica o impacto de uma mudança. O changelog comunica o conteúdo. Os dois são complementares e igualmente necessários.
Um bom CHANGELOG.md segue a estrutura do keepachangelog.com:
# Changelog
## [2.1.0] - 2024-03-15
### Added
- Endpoint `/api/v2/export` para exportação em CSV e XLSX
- Suporte a paginação cursor-based nas listagens
### Fixed
- Cálculo incorreto de desconto quando valor do pedido era negativo
- Timeout inconsistente em requisições com payload maior que 10MB
## [2.0.0] - 2024-02-01
### BREAKING CHANGES
- Endpoint `/api/users` renomeado para `/api/v2/users`
- Campo `user_id` substituído por `id` em todos os responses
- Autenticação via Basic Auth removida (use Bearer Token)
### Added
- Suporte a autenticação OAuth 2.0
- Rate limiting configurável por client
## [1.4.2] - 2024-01-10
### Fixed
- Vazamento de memória em operações de upload de arquivos grandesO que fazer e o que evitar?
Faça:
- Declare uma API pública clara antes de começar a versionar (1.0.0)
- Zere MINOR e PATCH após incremento MAJOR
- Documente breaking changes de forma explícita no changelog
- Use versões pré-release (1.0.0-beta.1) para comunicar instabilidade
- Marque funcionalidades como deprecated antes de removê-las em uma futura MAJOR
Evite:
- Incrementar PATCH para mudanças que alterem comportamento — use MAJOR se há incompatibilidade
- Lançar 1.0.0 sem ter uma API pública estável e testada
- Usar versões como 1.0 ou 2 — o formato é sempre três números
- Incrementar MAJOR para mudanças internas que não afetam a API pública
- Publicar patches que introduzem novas funcionalidades — isso deve ser MINOR
💡 Dica: Uma estratégia eficaz para evitar breaking changes frequentes é praticar expand-contract: primeiro adicione a nova API (expand), marque a antiga como deprecated, dê tempo para os consumidores migrarem, então remova a antiga em uma MAJOR futura (contract). Isso transforma uma quebra abrupta em uma migração gradual e planejada.
Desafios na adoção e como superá-los
O desafio da consistência em times
O maior obstáculo na adoção de SemVer em times não é técnico — é cultural. A decisão de “isso é MAJOR, MINOR ou PATCH?” exige julgamento, e pessoas diferentes fazem julgamentos diferentes.
A solução mais eficaz é documentar exemplos concretos para o seu projeto específico. Um guia de contribuição com exemplos reais (“se você remover um parâmetro obrigatório, é MAJOR; se você adicionar um parâmetro opcional, é MINOR; se você corrigir o comportamento de um parâmetro existente sem alterar a assinatura, é PATCH”) reduz drasticamente a ambiguidade.
O paradoxo da compatibilidade
Uma das questões mais difíceis do SemVer é: o que é uma breaking change? Nem sempre é óbvio.
Uma mudança de comportamento que corrige um bug pode quebrar usuários que dependiam do comportamento incorreto. Uma melhoria de performance pode alterar a ordem de operações de forma que afeta casos de uso específicos. A definição formal é: qualquer mudança que force modificações no código dos consumidores para manter o funcionamento correto.
⚠️ Atenção: Bugs em APIs públicas criam um dilema clássico: corrigir o bug é tecnicamente uma breaking change para quem dependia do comportamento incorreto, mas não corrigir perpetua um problema. A prática comum é lançar a correção como PATCH com documentação clara, marcando o comportamento anterior como bug documentado. O SemVer é uma convenção, não uma lei física — comunicação clara mitiga quase qualquer situação ambígua.
Dependency Hell mesmo com SemVer
O SemVer não elimina todos os conflitos de dependência — apenas os torna previsíveis. Se dois pacotes dependem de versões MAJOR diferentes de uma mesma biblioteca, o conflito é real independentemente de quanto SemVer você aplique.
A solução está em política de atualização regular (não deixar dependências acumular versões) e em usar lock files (package-lock.json, poetry.lock, Cargo.lock) que fixam versões exatas em produção enquanto permitem atualizações controladas em desenvolvimento.
Perguntas frequentes sobre Versionamento Semântico
SemVer é mais crítico para bibliotecas e APIs consumidas por outros times ou sistemas. Para aplicações internas com consumidores controlados, a rigidez formal é menor — mas os princípios de comunicar breaking changes claramente e manter changelogs são valiosos em qualquer contexto. Muitos times adotam uma versão simplificada: incremento MAJOR para breaking changes e deploys significativos, MINOR para features, PATCH para hotfixes.
Nada de errado — os números não têm limite máximo. 1.100.0 é uma versão completamente válida que indica 100 adições de funcionalidade desde a versão 1.0.0. Se os números parecem altos demais, pode ser um sinal de que features estão sendo acumuladas sem incremento MAJOR quando deveriam, mas isso é uma questão de processo, não de sintaxe.
APIs REST frequentemente versionam através da URL (/api/v1/, /api/v2/) em vez de ou em adição a SemVer de pacote. As duas abordagens são complementares: o número de versão do pacote/serviço segue SemVer, enquanto o versionamento da URL permite manter versões antigas da API em produção enquanto a nova é adotada gradualmente. Um incremento MAJOR no pacote geralmente corresponde a um novo prefixo de versão na URL.
Mostre o custo concreto da ausência de convenção: quantas horas foram gastas debugando problemas causados por atualizações inesperadas de dependências? Quantas vezes releases quebraram consumidores sem aviso? SemVer não adiciona burocracia — adiciona previsibilidade. Comece com uma proposta simples: “vamos só incrementar MAJOR quando quebrarmos a API pública” e construa a partir daí.
Sim. O semantic-release tem uma arquitetura de plugins que suporta publicação em múltiplos registros: PyPI (Python), Cargo (Rust), Maven Central (Java), imagens Docker, GitHub Releases, entre outros. A lógica de análise de commits e determinação de versão é independente da linguagem ou plataforma de publicação.
SemVer é um protocolo de comunicação, não uma burocracia
Ao longo deste guia, ficou claro que o Versionamento Semântico não é uma convenção arbitrária de nomenclatura — é um protocolo de comunicação que estabelece um contrato entre quem desenvolve e quem consome software.
Os três princípios que sustentam o SemVer são simples: MAJOR para breaking changes, MINOR para adições retrocompatíveis, PATCH para correções. Quando todos os participantes de um ecossistema seguem esses princípios, o gerenciamento de dependências se torna previsível, as atualizações ganham semântica clara e o inferno das dependências se torna um problema solucionável.
A adoção eficaz requer dois ingredientes além do conhecimento técnico: disciplina para classificar mudanças corretamente e honestidade para incrementar MAJOR quando necessário, mesmo que isso signifique uma conversa difícil com os consumidores. O custo de uma breaking change mal comunicada é sempre maior do que o custo de um incremento MAJOR bem documentado.
Comece pelo mais simples: documente sua API pública, defina claramente o que constitui uma breaking change para o seu projeto, e comunique o changelog em cada release. O restante — automação, conventional commits, semantic-release — é consequência natural de uma fundação sólida.
👉 Compartilhe este guia com o time antes da próxima discussão sobre política de releases — pode ser a base para um acordo que vai economizar horas de debugging e conflitos de dependência no futuro.
Uma resposta