Dicas para automatizar releases com GitHub Actions e semantic versioning

1. Fundamentos do Semantic Versioning no Contexto de Automação

O versionamento semântico (SemVer) segue o formato MAJOR.MINOR.PATCH, onde cada incremento segue regras específicas:
- MAJOR: incrementado quando há mudanças incompatíveis com versões anteriores (breaking changes)
- MINOR: incrementado quando novas funcionalidades são adicionadas de forma compatível
- PATCH: incrementado para correções de bugs compatíveis com versões anteriores

No contexto de automação, o SemVer guia a lógica de releases ao ser combinado com convenções de commits. A especificação Conventional Commits estabelece um padrão que permite extrair automaticamente o tipo de incremento necessário:

feat: adiciona novo endpoint de usuários
fix: corrige validação de email duplicado
feat!: altera schema do banco de dados (breaking change)

Cada tipo de commit mapeia diretamente para um incremento SemVer, permitindo que ferramentas automatizadas determinem a próxima versão sem intervenção manual.

2. Estrutura Básica de um Workflow de Release no GitHub Actions

Um workflow de release no GitHub Actions requer três componentes essenciais: on (gatilhos), jobs (tarefas) e steps (passos). A configuração inicial deve considerar tanto gatilhos manuais quanto automáticos.

name: Automated Release

on:
  workflow_dispatch:    # Permite execução manual
  push:
    branches:
      - main           # Executa automaticamente ao fazer push na main

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Obtém todo o histórico para análise de tags

Este workflow mínimo já prepara o ambiente para versionamento automático, garantindo que todo o histórico de tags esteja disponível para análise.

3. Estratégias para Extrair e Incrementar a Versão Automaticamente

A automação de versão exige a leitura da última tag do repositório e o cálculo da próxima versão. A ação mathieudutour/github-tag-action simplifica esse processo:

- name: Bump version and push tag
  id: tag_version
  uses: mathieudutour/github-tag-action@v6.1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    tag_prefix: v
    release_branches: main

- name: Display new version
  run: echo "New version is ${{ steps.tag_version.outputs.new_tag }}"

Para cenários específicos, como versão inicial ou prefixos personalizados, é possível configurar parâmetros adicionais. O tratamento de versões pré-release permite testar antes de liberar para produção:

- name: Bump version with pre-release
  uses: mathieudutour/github-tag-action@v6.1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    pre_release: true
    pre_release_branches: develop

4. Geração Automática de Changelog e Notas de Release

A geração de changelog baseada em tipos de commit fornece transparência sobre as mudanças. Utilizando actions/github-script, é possível extrair commits entre tags e formatar notas de release:

- name: Generate release notes
  id: release_notes
  uses: actions/github-script@v7
  with:
    script: |
      const commits = await github.rest.repos.compareCommits({
        owner: context.repo.owner,
        repo: context.repo.repo,
        base: '${{ steps.tag_version.outputs.previous_tag }}',
        head: '${{ steps.tag_version.outputs.new_tag }}'
      });

      let notes = '## O que mudou\n\n';
      commits.data.commits.forEach(commit => {
        const msg = commit.commit.message.split('\n')[0];
        if (msg.startsWith('feat:')) {
          notes += `- 🚀 **Nova funcionalidade**: ${msg.replace('feat:', '').trim()}\n`;
        } else if (msg.startsWith('fix:')) {
          notes += `- 🐛 **Correção**: ${msg.replace('fix:', '').trim()}\n`;
        } else if (msg.includes('BREAKING CHANGE')) {
          notes += `- ⚠️ **Breaking change**: ${msg}\n`;
        }
      });

      return notes;

Este script categoriza automaticamente os commits e gera um changelog em markdown dinâmico, que pode ser utilizado na criação da release.

5. Criação e Publicação de Tags e Releases no GitHub

Após determinar a versão e gerar as notas, é necessário criar a tag e a release oficial. A ação softprops/action-gh-release simplifica esse processo:

- name: Create GitHub Release
  uses: softprops/action-gh-release@v2
  with:
    tag_name: ${{ steps.tag_version.outputs.new_tag }}
    name: Release ${{ steps.tag_version.outputs.new_tag }}
    body: ${{ steps.release_notes.outputs.result }}
    draft: false
    prerelease: false
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Para evitar tags duplicadas, o workflow deve validar se a tag já existe antes de tentar criá-la. A validação pode ser feita verificando o output da ação de bump:

- name: Check if tag already exists
  if: steps.tag_version.outputs.new_tag == ''
  run: |
    echo "Tag already exists or no changes detected"
    exit 1

6. Integração com Publicação em Registries (npm, NuGet, Docker Hub, etc.)

Após a criação da tag, é possível disparar automaticamente a publicação em registries. O workflow condicional abaixo publica no npm apenas quando uma nova tag é criada:

- name: Publish to npm
  if: steps.tag_version.outputs.new_tag != ''
  run: |
    npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
    npm publish --access public
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Para Docker Hub, a integração segue padrão similar:

- name: Build and push Docker image
  if: steps.tag_version.outputs.new_tag != ''
  run: |
    docker build -t myapp:${{ steps.tag_version.outputs.new_tag }} .
    docker tag myapp:${{ steps.tag_version.outputs.new_tag }} myapp:latest
    echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
    docker push myapp:${{ steps.tag_version.outputs.new_tag }}
    docker push myapp:latest

7. Boas Práticas e Tratamento de Erros em Workflows de Release

Proteção de branches e regras de merge são essenciais para evitar releases acidentais. Configure branch protection rules na interface do GitHub para exigir revisões e verificações de status antes do merge na main.

Para notificações de falha, integre com serviços externos:

- name: Notify Slack on failure
  if: failure()
  uses: slackapi/slack-github-action@v1.24.0
  with:
    payload: |
      {
        "text": "Falha no release automático: ${{ github.repository }} - ${{ github.run_id }}"
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Em caso de versão incorreta, utilize git revert para desfazer a tag:

- name: Rollback tag
  if: failure() && steps.tag_version.outputs.new_tag != ''
  run: |
    git tag -d ${{ steps.tag_version.outputs.new_tag }}
    git push origin :refs/tags/${{ steps.tag_version.outputs.new_tag }}

A automação de releases com GitHub Actions e semantic versioning transforma um processo manual e propenso a erros em um fluxo confiável e repetível. Ao combinar commits convencionais, workflows bem estruturados e ações especializadas, sua equipe pode focar no desenvolvimento enquanto o versionamento e a publicação ocorrem de forma transparente e consistente.

Referências