Versionamento Semântico (SemVer): o que é e como usar corretamente

Versionamento Semântico (SemVer): o que é e como usar corretamente

Se você já publicou uma biblioteca, mantém um pacote no npm ou simplesmente atualizou uma dependência e viu o projeto quebrar do nada, você já sentiu na pele por que o versionamento de software importa. Um número de versão parece um detalhe bobo até o dia em que ele te salva (ou te ferra). E a diferença entre esses dois cenários costuma ser uma coisa só: se todo mundo naquela cadeia de dependências combinou de seguir as mesmas regras.

O Versionamento Semântico, ou SemVer, é justamente esse acordo. Ele pega aquele monte de números separados por ponto que a gente vê em tudo (1.4.2, 2.0.0, 0.9.1) e dá um significado preciso pra cada pedaço. Na prática, quando um projeto segue SemVer direito, você consegue olhar pra dois números de versão e saber, antes de atualizar, se aquilo vai só corrigir um bug, adicionar uma função nova ou explodir seu código inteiro. Neste post eu vou destrinchar como o SemVer funciona de verdade, quando incrementar cada número, os casos chatos (pre-release, build metadata, versão de API) e os erros que eu mais vejo por aí.

O que é versionamento semântico (SemVer)?

Versionamento semântico é uma convenção para numerar versões de software no formato MAJOR.MINOR.PATCH (por exemplo, 2.5.3), onde cada número comunica o tipo de mudança que aconteceu no código. Você incrementa o MAJOR quando faz alterações incompatíveis com versões anteriores, o MINOR quando adiciona funcionalidade de forma retrocompatível, e o PATCH quando corrige bugs sem quebrar nada. A especificação oficial vive em semver.org e foi criada por Tom Preston-Werner, um dos fundadores do GitHub.

A sacada central é que o número deixa de ser decorativo e vira um contrato. Se um projeto diz que segue SemVer, ele está prometendo uma coisa: nenhuma mudança que quebre seu código vai entrar sem que o primeiro número suba. Isso é o que permite ferramentas como npm, Composer, Cargo e pip resolverem dependências automaticamente sem você precisar auditar cada atualização na mão.

Versionamento Semântico (SemVer): o que é e como usar corretamente ilustracao 1

O formato MAJOR.MINOR.PATCH por dentro

Todo número de versão SemVer tem esses três componentes obrigatórios, sempre separados por ponto e sempre inteiros não negativos. A leitura é da esquerda pra direita em ordem de impacto: quanto mais à esquerda o número que mudou, maior o estrago potencial na sua aplicação.

MAJOR: a quebra de compatibilidade

O primeiro número é o mais dramático. Ele sobe quando você faz algo que vai obrigar quem usa seu código a mexer no código dele pra continuar funcionando. Removeu uma função pública, mudou a assinatura de um método, renomeou um parâmetro obrigatório, alterou o formato de retorno de uma API: tudo isso é breaking change e pede um MAJOR novo. Quando o MAJOR sobe, o MINOR e o PATCH voltam pra zero. Então de 1.8.4 você vai pra 2.0.0, nunca pra 2.8.4.

MINOR: a funcionalidade nova que não quebra

O número do meio sobe quando você adiciona capacidade nova mas mantém tudo que já existia funcionando igual. Uma função nova, um parâmetro opcional, um endpoint a mais na API: quem estava na versão anterior consegue atualizar sem tocar em nada. Aqui o PATCH também zera. De 1.8.4, adicionando um recurso novo, você vai pra 1.9.0. Marcar algo como deprecated (avisar que vai sumir no futuro, sem remover ainda) também é MINOR, porque o código continua rodando.

PATCH: só conserto

O último número é o mais tranquilo. Ele sobe quando você corrige um bug sem adicionar nada novo e sem quebrar nada. A correção tem que ser de comportamento interno: o usuário atualiza e a única diferença é que aquele bug sumiu. De 1.8.4 pra 1.8.5 e pronto. Se a sua “correção” muda a forma como alguém interage com o código, aí já não é mais PATCH, é MINOR ou MAJOR.

Componente Quando incrementar Exemplo Quebra código de quem usa?
MAJOR Mudança incompatível com a versão anterior (breaking change) 1.9.2 para 2.0.0 Sim, obriga a ajustar o código
MINOR Nova funcionalidade retrocompatível 1.9.2 para 1.10.0 Não, atualização é segura
PATCH Correção de bug retrocompatível 1.9.2 para 1.9.3 Não, atualização é segura

Repara numa coisa que confunde bastante gente: 1.10.0 é maior que 1.9.0. Esses números não são decimais, são inteiros independentes. Depois do 1.9.0 vem 1.10.0, depois 1.11.0 e assim vai. Comparar versão como se fosse número quebrado é um erro clássico de quem escreve script de comparação na mão.

Quando incrementar cada número: exemplos reais

A teoria é curta, o problema mora nos casos concretos. Vou dar situações que aparecem no dia a dia e o incremento correto de cada uma, porque é aqui que a maioria das dúvidas mora.

  • Você corrigiu um cálculo errado numa função sem mudar a assinatura dela. Isso é PATCH. O bug some, o contrato continua igual.
  • Você adicionou um parâmetro opcional a uma função existente. Isso é MINOR. Quem chamava sem o parâmetro continua funcionando, quem quiser usa o novo.
  • Você tornou obrigatório um parâmetro que antes era opcional. Isso é MAJOR. Todo mundo que chamava sem ele agora quebra.
  • Você melhorou a performance de um método sem mudar o resultado. PATCH, na maioria dos casos. Se ninguém percebe além de ficar mais rápido, é conserto.
  • Você renomeou uma função pública. MAJOR, mesmo que pareça pequeno. Qualquer um que importava o nome antigo vai quebrar.
  • Você adicionou um método novo numa classe. MINOR. A superfície cresceu, mas nada do que existia mudou.

Tem um caso que gera briga em code review: mudar o texto de uma mensagem de log ou de erro. Aqui depende. Se ninguém depende daquele texto pra tomar decisão, é PATCH tranquilo. Mas se você tem usuários que fazem parsing daquela string (e acredite, eles existem), tecnicamente você quebrou algo. A regra prática que eu uso é: se está documentado como parte da interface pública, mexer nele é breaking. Se é detalhe interno, é PATCH.

Versionamento Semântico (SemVer): o que é e como usar corretamente ilustracao 2

Pre-releases e build metadata

SemVer não para nos três números. Tem duas extensões opcionais que aparecem bastante e confundem quem nunca parou pra ler a spec: os identificadores de pre-release e o build metadata.

Pre-release: a versão que ainda não confia em si mesma

Quando você quer lançar uma versão pra teste antes de bater o martelo, você adiciona um hífen depois do PATCH seguido de identificadores. Coisas como 1.0.0-alpha, 1.0.0-beta.2 ou 2.3.0-rc.1. O ponto importante é a ordem de precedência: uma versão de pre-release tem prioridade menor que a versão normal associada. Ou seja, 1.0.0-alpha vem antes de 1.0.0. Faz sentido, porque o alpha é o rascunho e o 1.0.0 “limpo” é o lançamento definitivo.

A precedência entre pre-releases segue regras próprias: compara identificador por identificador, numérico contra numérico na ordem matemática, alfanumérico na ordem ASCII. Então a sequência natural fica assim:

1.0.0-alpha
1.0.0-alpha.1
1.0.0-alpha.beta
1.0.0-beta
1.0.0-beta.2
1.0.0-beta.11
1.0.0-rc.1
1.0.0

Repara que beta.11 vem depois de beta.2, porque quando os dois identificadores são numéricos a comparação é numérica, não alfabética. Isso é o oposto de como strings normais ordenam, e é uma pegadinha comum.

Build metadata: a etiqueta que ninguém compara

O build metadata vem depois de um sinal de mais, tipo 1.0.0+20130313144700 ou 1.0.0-beta+exp.sha.5114f85. Ele serve pra carregar informação de build (hash de commit, timestamp, número de build) mas tem uma característica crucial: ele é ignorado na hora de determinar precedência. Duas versões que só diferem no build metadata são consideradas a mesma versão pra efeito de ordenação. É metadado puro, informativo, nada mais.

Versionamento de API e o problema do zero

Versionar API tem uma relação direta com SemVer, porque uma API pública é basicamente um contrato que outras pessoas consomem. Quando você muda a resposta de um endpoint, remove um campo do JSON ou torna um parâmetro obrigatório, você acabou de fazer um breaking change na sua API, e isso pede um MAJOR novo. É por isso que a gente vê APIs versionadas na URL como /api/v1/ e /api/v2/: o número na rota costuma acompanhar o MAJOR do SemVer, mantendo a v1 no ar enquanto o mundo migra pra v2.

Agora, o assunto que mais gera confusão: a fase 0.y.z. Enquanto o MAJOR é zero, o software é considerado em desenvolvimento inicial e qualquer coisa pode mudar a qualquer momento. A spec diz explicitamente que a API pública não deve ser considerada estável nessa fase. Na prática isso significa que num projeto 0.x um breaking change costuma ser sinalizado incrementando o MINOR, não o MAJOR. De 0.4.0 pra 0.5.0 pode conter quebra, e tá tudo dentro das regras.

O momento em que você lança o 1.0.0 é uma declaração pública: “a partir daqui eu me comprometo com estabilidade”. Muita gente segura o 1.0.0 por medo, deixando o projeto em 0.x por anos, mas isso tem um custo: seus usuários não têm nenhuma garantia de retrocompatibilidade enquanto você não bater o 1. Se seu software está em produção e gente séria depende dele, ele merece um 1.0.0.

No ecossistema JavaScript, o package.json combina SemVer com operadores de faixa que valem a pena conhecer:

{
  "name": "meu-pacote",
  "version": "2.4.1",
  "dependencies": {
    "express": "^4.18.2",
    "lodash": "~4.17.21",
    "chalk": "5.3.0"
  }
}

O acento circunflexo (^4.18.2) aceita qualquer atualização que não mexa no MAJOR, ou seja, pega da 4.18.2 até antes da 5.0.0. O til (~4.17.21) é mais conservador e trava no MINOR, aceitando só updates de PATCH até antes da 4.18.0. E a versão sem operador nenhum (5.3.0) é exata, pino cravado, nada além dela. Entender esses operadores é o que separa um npm install tranquilo de uma noite depurando por que o build quebrou sozinho.

Erros comuns de versionamento que eu vejo por aí

Depois de anos olhando changelog dos outros, alguns erros se repetem com uma frequência que impressiona. Listo os que mais causam dor.

Tratar breaking change como MINOR ou PATCH

O pecado capital. Alguém remove um método, acha que “é pequeno” e solta como PATCH. Aí meio ecossistema atualiza automaticamente por causa do ^ no package.json e tudo pega fogo. Se quebrou, é MAJOR, ponto. Não existe breaking change pequeno o suficiente pra virar PATCH.

Nunca sair do 0.x

Já falei disso acima, mas repito porque é comum: manter software maduro e em produção eternamente na casa do zero. Isso comunica pros usuários que nada é estável, mesmo quando na real você já não quebra nada há meses. Se você já se compromete com estabilidade na prática, assuma isso no número.

Esquecer de zerar os números à direita

Subiu o MAJOR? MINOR e PATCH vão a zero. Subiu o MINOR? PATCH vai a zero. Ver um 2.0.5 logo depois de um 1.9.5 é sinal de que alguém só incrementou o primeiro número e esqueceu o resto. Fica torto e confunde as ferramentas de resolução.

Não manter changelog

SemVer diz o tipo de mudança, mas não diz qual mudança. Um número de versão sem um changelog do lado é meio caminho. Se você subiu um MAJOR, quem vai atualizar precisa saber o que exatamente quebrou pra se planejar. Um arquivo CHANGELOG.md no formato Keep a Changelog resolve isso e custa pouco.

Versionar de forma sentimental

Aquele lance de “esse release é tão importante que merece um 2.0”. Não. O número não mede orgulho nem tamanho de esforço, ele mede compatibilidade. Um MAJOR pode ser uma mudança minúscula que só quebrou uma assinatura, e um MINOR pode ser uma feature gigante que não quebrou nada. Deixa o sentimento de fora.

Perguntas frequentes sobre versionamento semântico

SemVer e versionamento semântico são a mesma coisa?

Sim. SemVer é só a abreviação de “Semantic Versioning”, o nome em inglês. É exatamente a mesma especificação, aquela do formato MAJOR.MINOR.PATCH descrita em semver.org. Você vai ver os dois termos usados de forma intercambiável em documentação, changelogs e ferramentas.

Qual a diferença entre 1.0.0 e 0.1.0 na prática?

A diferença é a promessa de estabilidade. Uma versão 0.1.0 avisa que a API ainda é experimental e pode mudar a qualquer momento, inclusive de forma incompatível, sem subir o MAJOR. Já 1.0.0 é um compromisso público: dali em diante, qualquer quebra de compatibilidade obriga a subir pra 2.0.0. Na dúvida entre lançar 0.x ou 1.0.0, pergunte se você está disposto a se comprometer com retrocompatibilidade.

O que significa o hífen e o sinal de mais numa versão?

O hífen introduz um identificador de pre-release, como em 1.0.0-beta.1, indicando uma versão instável que vem antes do lançamento final. O sinal de mais introduz o build metadata, como em 1.0.0+build.42, que carrega informação de build e é totalmente ignorado quando as ferramentas comparam qual versão é mais recente.

Preciso subir o MAJOR se eu só corrigi um bug crítico?

Não, desde que a correção não mude o comportamento esperado da interface pública. Correção de bug é PATCH por definição, mesmo que o bug seja grave. A gravidade do problema não muda o tipo de incremento, o que muda é se a correção quebra ou não o código de quem já usava. Se pra corrigir você teve que mudar uma assinatura ou remover algo, aí sim virou breaking change e o MAJOR entra.

Como o SemVer lida com deprecação de funcionalidades?

Marcar algo como deprecated (avisar que vai ser removido no futuro) é uma mudança MINOR, porque o código continua funcionando normalmente, só ganha um aviso. A remoção efetiva daquilo, essa sim, é MAJOR, porque quebra quem ainda dependia da funcionalidade. O padrão saudável é depreciar num MINOR, deixar o aviso rodando por um tempo razoável e só remover no próximo MAJOR, dando fôlego pra galera migrar.

SemVer serve só pra bibliotecas ou dá pra usar em qualquer software?

Dá pra usar em qualquer coisa que tenha uma interface pública que outras pessoas consomem: bibliotecas, APIs REST, CLIs, plugins, até formatos de arquivo. Onde SemVer não encaixa tão bem é em aplicações de usuário final sem API pública, tipo um app web fechado, onde o conceito de “breaking change para quem consome” não existe do mesmo jeito. Nesses casos muita gente usa versionamento por data (CalVer) ou esquemas próprios, e tudo bem.

Conclusão

Versionamento semântico não é burocracia, é comunicação. Cada um daqueles três números conta uma história sobre o que mudou e, principalmente, sobre o que vai acontecer com o seu código se você atualizar. Quando um projeto respeita SemVer de verdade, você para de auditar cada release na mão e passa a confiar no número, e essa confiança é o que faz todo o ecossistema de dependências funcionar sem virar caos.

O resumo que eu levaria pra qualquer projeto é curto: quebrou compatibilidade, sobe o MAJOR; adicionou coisa sem quebrar, sobe o MINOR; consertou bug sem quebrar, sobe o PATCH. Zere os números à direita quando um da esquerda sobe, mantenha um changelog do lado explicando o que rolou, e não trave seu software maduro na casa do zero por medo de assumir estabilidade. Faça isso com disciplina e o seu 1.0.0 vai deixar de ser um número e virar uma promessa que você consegue cumprir.

Para quem constrói sistemas que precisam de dados oficiais, o Hub do Desenvolvedor mantém APIs de consulta de CNPJ e CPF na Receita Federal prontas para você integrar sem burocracia.

Compartilhe nas mídias: