Semantic Versioning e Tags de Release

[91] Semantic Versioning e Tags de Release

Semantic Versioning e tags de release: o que MAJOR, MINOR e PATCH comunicam, sufixos de pré-release, tags leves e anotadas no Git, Conventional Commits como base para automação e geração automática de versão e changelog com Release Please.
DevOps

10 min de leitura

Imagine receber uma mensagem de um colega dizendo: "o bug está na versão nova". Qual versão nova? Nova em relação a quê? Lançada quando?

Sem uma convenção de versionamento, equipes acabam usando nomes arbitrários — "versão de dezembro", "build final", "build final v2", "build final v2 revisado". Em produção, esse caos tem consequências reais: dificuldade em identificar qual código está rodando, impossibilidade de comparar comportamentos entre versões e incapacidade de comunicar claramente o impacto de uma atualização para clientes e parceiros.

O Semantic Versioning — ou SemVer — é a convenção que resolve esse problema. Adotada pela maioria dos projetos de software modernos, ela transforma um número de versão em uma comunicação precisa sobre o que mudou.

A Lógica do SemVer

Um número de versão SemVer tem o formato MAJOR.MINOR.PATCH — por exemplo, 2.4.1.

Cada parte tem um significado definido:

PATCH — incrementado quando são feitas correções de bugs compatíveis com versões anteriores. Quem usa 2.4.0 pode atualizar para 2.4.1 sem preocupação. Nada quebrou, apenas foi corrigido.

MINOR — incrementado quando novas funcionalidades são adicionadas de forma compatível com versões anteriores. Quem usa 2.4.1 pode atualizar para 2.5.0 com segurança — o que já funcionava continua funcionando.

MAJOR — incrementado quando mudanças incompatíveis são introduzidas. Quem usa 2.x precisa ler o changelog antes de atualizar para 3.0.0 — algo pode ter quebrado intencionalmente.

Regras adicionais importantes:

  • Quando MINOR é incrementado, PATCH volta a zero: 2.4.12.5.0
  • Quando MAJOR é incrementado, MINOR e PATCH voltam a zero: 2.5.33.0.0
  • Versões com MAJOR igual a zero (0.x.y) indicam desenvolvimento inicial — a API pública não é considerada estável

Sufixos de Pré-release

Para versões que ainda não estão prontas para produção, o SemVer permite sufixos:

1.0.0-alpha        # primeira fase de testes internos
1.0.0-alpha.2      # segunda iteração do alpha
1.0.0-beta.1       # fase de testes mais ampla
1.0.0-rc.1         # release candidate — candidato a versão final
1.0.0              # versão estável final

Em pipelines de CI/CD, é comum gerar versões de pré-release automaticamente para branches de desenvolvimento, enquanto versões estáveis são geradas apenas a partir de merges na main.

Tags no Git

No Git, uma tag é um ponteiro nomeado para um commit específico. Diferentemente das branches, tags não se movem — elas marcam um ponto fixo no histórico, ideal para representar releases.

# Cria uma tag leve (apenas um ponteiro)
git tag v1.2.0

# Cria uma tag anotada (recomendada — inclui mensagem, autor e data)
git tag -a v1.2.0 -m "Release 1.2.0: adiciona suporte a autenticação OAuth"

# Lista todas as tags
git tag

# Lista tags com filtro
git tag -l "v1.*"

# Mostra detalhes de uma tag anotada
git show v1.2.0

# Cria tag em um commit específico (não necessariamente o mais recente)
git tag -a v1.1.5 abc1234 -m "Hotfix: corrige falha de segurança no login"

Tags precisam ser enviadas explicitamente para o repositório remoto — o git push padrão não as inclui:

# Envia uma tag específica
git push origin v1.2.0

# Envia todas as tags locais
git push origin --tags

Conventional Commits: A Base para Automação

O Semantic Versioning define como nomear versões. O Conventional Commits define como escrever mensagens de commit de forma que ferramentas possam determinar automaticamente qual parte da versão deve ser incrementada.

O formato é simples:

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

[corpo opcional]

[rodapé opcional]

Os tipos mais comuns e sua relação com o SemVer:

# Incrementa PATCH — correção de bug
git commit -m "fix: corrige cálculo incorreto de frete para regiões remotas"

# Incrementa MINOR — nova funcionalidade
git commit -m "feat: adiciona suporte a pagamento via PIX"

# Incrementa MAJOR — mudança incompatível (indicada no rodapé)
git commit -m "feat!: remove suporte à API v1

BREAKING CHANGE: os endpoints /api/v1/* foram removidos.
Migre para /api/v2/* conforme documentação."

# Não incrementa versão — mudanças que não afetam o produto
git commit -m "docs: atualiza exemplos de autenticação no README"
git commit -m "chore: atualiza dependências de desenvolvimento"
git commit -m "ci: adiciona cache de dependências no pipeline"
git commit -m "refactor: extrai lógica de validação para módulo separado"
git commit -m "test: adiciona testes para o módulo de pagamento"

Automatizando Releases com GitHub Actions

Com Conventional Commits no histórico, é possível automatizar completamente o processo de versionamento e geração de changelog. A ferramenta Release Please, mantida pelo Google, faz exatamente isso:

# .github/workflows/release.yml
name: Release

on:
  push:
    branches: [ main ]

permissions:
  contents: write
  pull-requests: write

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

O que esse workflow faz: a cada push na main, o Release Please analisa os commits desde a última release, determina qual parte do SemVer deve ser incrementada, cria automaticamente um Pull Request com a atualização de versão e o changelog gerado. Quando esse PR é mergeado, uma tag Git e uma GitHub Release são criadas automaticamente.

Criando uma Release no GitHub Manualmente

Para projetos que preferem controle manual sobre o processo de release:

# Garante que a main está atualizada
git checkout main
git pull

# Cria a tag anotada
git tag -a v2.1.0 -m "Release 2.1.0

Novidades:
- Autenticação via OAuth com Google e GitHub
- Dashboard de métricas em tempo real
- Correção de falha em sessões simultâneas"

# Envia a tag
git push origin v2.1.0

No GitHub, navega-se até Releases → Create a release, seleciona-se a tag criada e adiciona-se as notas de release. O GitHub renderiza o markdown e disponibiliza a release publicamente, com download do código-fonte em zip e tar.gz automaticamente.

Um Fluxo Completo de Versionamento

Reunindo tudo em um cenário real:

# Desenvolvimento da funcionalidade
git checkout -b feature/dashboard-metricas
git commit -m "feat: adiciona componente de gráfico de requisições por minuto"
git commit -m "feat: adiciona filtro por intervalo de datas no dashboard"
git commit -m "test: adiciona testes do componente de gráfico"
git push origin feature/dashboard-metricas

# PR criado, revisado e mergeado na main
# O Release Please detecta dois commits 'feat' e propõe incremento MINOR
# Versão atual: 1.4.2 → proposta: 1.5.0

# PR de release mergeado → tag v1.5.0 criada automaticamente
# Changelog gerado automaticamente:

# ## [1.5.0] - 2025-03-10
# ### Features
# - adiciona componente de gráfico de requisições por minuto
# - adiciona filtro por intervalo de datas no dashboard

Referências para Aprofundamento

Especificações

Ferramentas

  • Release Please — GitHub — Ferramenta do Google para automação de releases baseada em Conventional Commits. Documentação completa no repositório.
  • semantic-release — Alternativa ao Release Please, mais configurável e com ecossistema de plugins. Popular em projetos Node.js.
  • git-cliff — Gerador de changelog altamente configurável, escrito em Rust. Suporta templates customizados para o formato de changelog.

Leitura complementar

  • Keep a Changelog — Guia e convenção para escrita manual de changelogs, em português. Explica os princípios por trás de um bom changelog mesmo quando o processo não é automatizado.

Exercícios

Exercício 1

Partindo da versão 2.4.1, qual é a próxima versão em cada caso abaixo? Por que os campos à direita voltam a zero?

a) correção de um bug, sem quebrar nada
b) nova funcionalidade compatível com o que já existia
c) remoção de endpoints que os clientes usavam
Ver resposta

✓ Resposta: (a) 2.4.2 — só o PATCH avança. (b) 2.5.0 — o MINOR avança e o PATCH zera. (c) 3.0.0 — o MAJOR avança e MINOR e PATCH zeram. O zeramento não é estética: o número precisa ser lido como uma afirmação sobre o histórico. Se a versão nova fosse 2.5.1, ela afirmaria que já houve uma correção aplicada sobre uma 2.5.0 — uma versão que nunca existiu. Zerar mantém o número honesto sobre o que de fato aconteceu.

Exercício 2

O que uma versão 0.x.y comunica? Que cuidado isso exige de quem consome essa dependência?

Ver resposta

✓ Resposta: MAJOR igual a zero sinaliza desenvolvimento inicial: a API pública ainda não é considerada estável. Na prática, isso suspende a garantia central do SemVer — a de que subir o MINOR é seguro. Entre 0.3.0 e 0.4.0 a compatibilidade pode ter sido quebrada legitimamente, porque a promessa só passa a valer a partir do 1.0.0. Para quem consome, o cuidado é concreto: fixar a versão exata em vez de usar faixas como ^0.3.0, e ler o changelog a cada atualização em lugar de confiar no número.

Exercício 3

Qual a diferença entre uma tag leve e uma tag anotada? E por que tags não são enviadas pelo git push comum?

Ver resposta

✓ Resposta: A tag leve é apenas um ponteiro para um commit. A anotada, criada com -a, é um objeto próprio no Git, que carrega autor, data e mensagem — por isso é a recomendada para release: ela registra quem publicou, quando e o quê, informação que a tag leve simplesmente não guarda. Quanto ao push, tags não são branches: são referências independentes, e o Git não presume que toda marcação feita localmente deva ir para o remoto. Daí a necessidade de git push origin v1.2.0 ou --tags. Esquecer esse passo é um tropeço frequente — a release existe na máquina de quem a criou e não existe para mais ninguém, nem para o pipeline configurado para disparar quando surge uma tag.

Exercício 4

Relacione cada tipo de Conventional Commit à parte do SemVer que ele incrementa. Por que docs, chore, ci, refactor e test não incrementam nada?

Ver resposta

✓ Resposta: fix: incrementa PATCH. feat: incrementa MINOR. feat!: ou o rodapé BREAKING CHANGE: incrementam MAJOR. Os demais não incrementam porque não alteram o que a pessoa que usa o software recebe: refatorar, documentar, ajustar o pipeline ou acrescentar testes muda o projeto por dentro, não o contrato externo — e é o contrato externo que o número de versão descreve. O ganho de fundo é outro: a convenção transforma a mensagem de commit em dado processável, e a decisão de versão deixa de ser um julgamento humano no fim do ciclo para virar consequência do que já foi registrado ao longo dele.

Exercício 5

O que o Release Please faz a cada push na main? Por que ele abre um Pull Request em vez de já criar a tag diretamente?

Ver resposta

✓ Resposta: Ele analisa os commits acumulados desde a última release, determina qual parte do SemVer deve ser incrementada e abre um PR contendo o bump de versão e o changelog já gerado. A tag e a GitHub Release só nascem quando esse PR é mergeado. O PR intermediário existe por dois motivos. O primeiro é conferência: você enxerga qual versão será publicada e exatamente o que entrará no changelog antes que isso se torne público e imutável. O segundo é agrupamento: vários merges podem se acumular no mesmo PR de release, evitando que cada commit na main vire uma versão publicada.

Exercício 6

No cenário do dashboard, dois commits feat produziram a versão 1.5.0 — e não 1.6.0. Por quê? E o que aconteceria se, no mesmo intervalo, houvesse também um fix e um BREAKING CHANGE?

Ver resposta

✓ Resposta: Porque o SemVer descreve a natureza da mudança, não a quantidade dela. Dois, dez ou cinquenta commits feat acumulados desde a 1.4.2 geram um único incremento de MINOR, resultando em 1.5.0. Com um fix junto, prevalece o de maior impacto: a versão continua 1.5.0 e a correção apenas aparece no changelog, sem elevar mais nada. Já um BREAKING CHANGE no intervalo vence todos os outros e leva a versão a 2.0.0 — porque o único aviso que realmente importa para quem consome é o de que algo deixou de funcionar como antes.

Comentários

Mais em DevOps

Processos, Serviços e o Comando `systemctl`
Processos, Serviços e o Comando `systemctl`

Processos e serviços no Linux: leitura de ps aux e top, busca com pgrep, a…

Ansible: Configurando Servidores de Forma Declarativa
Ansible: Configurando Servidores de Forma Declarativa

Configuração declarativa de servidores por SSH, sem agente instalado: o…

SSH: Conectando e Gerenciando Servidores Remotos
SSH: Conectando e Gerenciando Servidores Remotos

SSH na prática: verificação de host com known_hosts, autenticação por par de…