Automatizando o versionamento semântico com Git Hooks

Alexis Lopes - Jul 6 - - Dev Community

Cenário

Venho trabalhando na criação de uma biblioteca para componentizar um design system e durante este processo, senti a necessidade de automatizar a atualização de versão para evitar precisar fazer isso manualmente toda vez.

Pensei em criar algo que pudesse pelo menos semi-automatizar os incrementos de cada seção da versão, assim, toda vez que houvesse um merge na branch master, a versão seria incrementada. Já que normalmente a master é a linha do tempo estável, para produção.

Contexto

No GitFlow a master pode ser atualizada de duas maneiras: a primeira é pelo fechamento de uma release. A outra, pelo fechamento de uma hotfix.

master sendo atualizada pelo fechamento de uma release master sendo atualizada pelo fechamento de uma hotfix

Normalmente, o que é tratado em uma hotfix tende a ser ajustes pontuais ou correções de bugs, sendo assim, atualizar apenas o patch está de bom tamanho.

Já para o que vem de uma release é um conteúdo mais substancial, então eu queria a possibilidade de escolher se eu atualizo a major, minor ou patch da nova versão.

Abordagem

Pensei em criar um script para rodar na linha de comando para lidar com estes casos, e aliar isso aos hooks do git acaba sendo bastante pertinente.

O git hook

Para o hook, dado o contexto, faz sentido usar o de post-merge, que promete ser invocado sempre que um merge é realizado com sucesso. Contei com a ajuda da lib husky para lidar melhor com os hooks.

#!/bin/bash

current_branch=$(git rev-parse --abbrev-ref HEAD)
commit_message=$(git log -1 --pretty=%B)

if [ "$current_branch" == "master" ]; then
  npm test
  if [[ "$commit_message" == *"release/"* ]]; then
    start node bump.js ; exit
  elif [[ "$commit_message" == *"hotfix/"* ]]; then
    npm version patch
  fi
fi
Enter fullscreen mode Exit fullscreen mode

O comando git rev-parse --abbrev-ref HEAD nos diz qual é a branch atual e o git log -1 --pretty=%B recupera qual foi a última mensagem de commit.

No GitFlow, sempre quando mergeamos uma branch, uma mensagem de commit é aplicada no modelo Merge branch release/nome_da_release, o mesmo acontece com a hotfix. Por isso recuperar a última mensagem de commit é importante, é nela que terá nosso fator condicional.

Um dos requisitos é que funcionasse apenas na branch master o que explica a primeira condicional, uma vez que estamos na master, é verificado se o ultimo commit vem de uma release ou hotflix, como está no trecho acima.

O script de escolha

Essa é a parte do texto onde confesso que nunca tinha feito um CLI antes. Não sabia por onde começar. Foi então que lembrei que o CLI do Vite é bem legalzinho e fui ver o que eles usam por lá.

Para minha felicidade, é feito em JavaScript. Usando um conjuntos dos pacotes cross-spawn, prompts e kolorist

  • cross-spawn: para rodar comandos de linha de comando.
  • prompts: para criar prompts interativos e inquerir informações do usuário
  • kolorist: adiciona cores aos prompts
//./bump.js

import spawn from 'cross-spawn';
import prompts from "prompts";
import pkg from "./package.json" assert { type: 'json' };

import {
  cyan,
  green,
  magenta,
  yellow
} from 'kolorist';

(async () => {
  const current_major = Number(pkg.version[0])
  const current_minor = Number(pkg.version[2])
  const current_patch = Number(pkg.version[4])

  const choices = [
    {
      title: green(`Major`),
      description: `${pkg.version} ➡️  ${`${current_major + 1}.0.0`}`,
      value: 'npm version major',
    },
    {
      title: cyan(`Minor`),
      description: `${pkg.version} ➡️  ${current_major}.${current_minor + 1}.0`,
      value: 'npm version minor',
    },
    {
      title: yellow("Patch"),
      description: `${pkg.version} ➡️  ${current_major}.${current_minor}.${current_patch + 1}`,
      value: 'npm version patch',
    }
  ]

  const response = await prompts({
    type: 'select',
    name: 'value',
    message: `Você acabou de realizar o fechamento de uma release. 

  Para que o fluxo possa continuar, atualize a versão da lib ${magenta(pkg.name)}.

  Qual das opções abaixo mais faz sentido para as alterações presentes nesta release? 

  Lembrando:

  ${green('MAJOR')}: é a versão que contém mudanças incopatíveis, breaking changes.
  ${cyan('MINOR')}: é a versão que adiciona funcionalidades, com campatibilidade.
  ${yellow('PATCH')}: é a versão que adiciona ajustes gerais ou de bugs, mantendo compatibilidade.

  Escolha 👇

  `,
    initial: 2,
    choices
  });

  const { status } = spawn.sync(response.value, [], {
    stdio: 'inherit',
  })

  process.exit(status ?? 0)
})();
Enter fullscreen mode Exit fullscreen mode

No código, a ideia foi pegar a versão atual importando o package.json e em cima disso, calcular as possíveis novas versões para major, minor e patch e transformar isso em escolhas para o usuário. Dependendo da escolha, o comando npm é rodado para atualizar a versão.

Com isso, o objetivo foi atingido. agora, sempre que a master for alimentada pela hotfix ou release, a versão será incrementada para patch ou abrirá o terminal para escolha dependendo da qualidade do conteúdo da release, a escolha do usuário.

script rodando com cursor na opção patchscript rodando com cursor na opção minorscript rodando com cursor na opção major

.