DEV Community

Cover image for Criando e automatizando o versionamento semântico de projetos NodeJS
William Queiroz
William Queiroz

Posted on • Updated on

Criando e automatizando o versionamento semântico de projetos NodeJS

Photo by Johnson Wang on Unsplash

Introdução

E ae dev, tudo bem com você?

Desde que eu comecei a trabalhar com JavaScript do lado do servidor, e a utilizar as mais diversas bibliotecas lá do NPM, sempre me questionei como elas eram mantidas... Desde as suas versões publicadas, padrões dos projetos que podem ser seguidos por um ou mais desenvolvedores, ou até mesmo por uma equipe dedicada apenas nisso.

A questão que mais me intrigava era: como saber quais versões DEVEM ser publicadas, quando uma alteração é feita?

Bom, com base nisso, nos últimos dias dediquei os estudos a explorar os mais variados repositórios no GitHub, e as bibliotecas mais populares no momento no NPM. Identifiquei alguns padrões que podem facilitar a manutenção dos projetos com automatização de processos!

Ao final deste artigo, você irá:

  • Entender a importância de padronizar um projeto, antes de desenvolver.
  • Entender como funciona um versionamento semântico.
  • Entender o que são commits semânticos.
  • Aprender a automatizar a publicação/distribuição do seu projeto com base no versionamento.

Bora pro post?

Sumário

O problema

Imagine que você esteja trabalhando com o cenário, onde as versões do seu projeto precisam ser coerentes com cada ajuste que você precise fazer, isto é, as versões devem indicar aquilo que foi feito. Seja uma implementação de uma nova funcionalidade, uma correção de um bug, ou até mesmo um breaking change por remover uma feature ou mudar completamente a integração do seu projeto, com os demais projetos que o utilizam.

A coerência com as versões do seu projeto irá auxiliar na manutenção do projeto como um todo!

O SemVer está aqui para nos ajudar!

Entendendo o versionamento semântico

Vamos entender melhor como funciona a especificação SemVer!

Ela aborda um conjunto simples de regras e requisitos que determinam como os números da versão são atribuídos e por sua vez, incrementados.

"Sob esse esquema, os números da versão e a maneira como eles mudam transmitem significado sobre o código subjacente e o que foi modificado de uma versão para a seguinte".

Em resumo, dado o número de versão MAJOR.MINOR.PATCH, deve-se incrementá-las seguindo as seguintes regras:

  1. MAJOR: quando você realizar alterações incompatíveis da API;

  2. MINOR: quando você adicionar funcionalidades compatíveis com versões anteriores;

  3. PATCH: quando você corrigir erros compatíveis com versões anteriores.

Para a nossa configuração, o essencial está nesse resumo. Você pode ler mais sobre a especificação em: https://semver.org/

Recomendo também a leitura da seção FAQ disponível no site, lá você encontrará respostas para dúvidas do tipo: "Como sei quando lançar a 1.0.0?".

A especificação SemVer foi originalmente criada por Tom Preston-Werner, inventor do Gravatar e co-fundador do GitHub.

Entendendo o que são commits semânticos

Você em algum momento já se questionou como deveria escrever uma mensagem de commit (se deveria colocar muitos detalhes, descrever melhor o que fez no corpo do commit, usar algum prefixo, e etc.).

Seria mágico termos algum padrão para usarmos no nosso projeto, que siga uma maneira consistente e coesa de escrever os commits, e que informe exatamente o que foi feito ali, não é mesmo?

Pois é, e TEM!

Especificação Conventional Commits

A especificação Conventional Commits é inspirada e baseada fortemente no guideline de commits do Angular. É uma convenção bem simples para seguir ao escrever commits e que oferece um conjunto fácil de regras para manter um histórico de commits mais explícito e facilmente compreendido.

Em resumo, para seguir a especificação, um commit deve ser estruturado da seguinte maneira:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]
Enter fullscreen mode Exit fullscreen mode

O commit pode conter alguns elementos estruturais, que comunicam a intenção aos "consumidores" do seu projeto:

  1. fix: um commit "do tipo" fix indica que aquela alteração corrige algum bug no projeto (isso se correlaciona ao PATCH do SemVer);

  2. feat: um commit "do tipo" feat indica que aquela alteração adiciona alguma nova funcionalidade ao projeto (isso se correlaciona ao MINOR do SemVer);

  3. BREAKING CHANGE: um commit que possui um rodapé com BREAKING CHANGE ou está diretamente na mensagem com ! depois do type ou scope, indica que aquela alteração, muda a compatibilidade da sua API com os "consumidores" (isso se correlaciona com o MAJOR do SemVer). Um BREAKING CHANGE pode fazer parte de commits de qualquer type;

  4. Outros tipos além de feat e fix também são permitidos.

Um scope pode ser fornecido ao type do commit, para fornecer informações contextuais adicionais e pode ser encontrado entre parênteses na mensagem, e.g.:

feat(parser): add ability to parse arrays.
Enter fullscreen mode Exit fullscreen mode

Você pode ler a especificação completa em: https://www.conventionalcommits.org/en/v1.0.0/#specification

Por que usar?

Ao adotar os padrões no seu projeto, você poderá:

  • Determinar automaticamente o bump das versões (de maneira semântica, com base nos tipos dos commits criados);
  • Comunicar com clareza a natureza das alterações (seja aos colegas de equipe ou ao público);
  • Automatizar o processo de build e publicação/distribuição do projeto.
  • Gerar CHANGELOGs automaticamente.

Outro ganho é que os desenvolvedores serão encorajados a granularizar os commits, uma vez que os padrões adotados visam contextualizar melhor a alteração feita (com tipos, escopos, e etc.). Isso é muito bom ao trabalhar com Code Review e Pull Requests.

Como isso se relaciona com o SemVer?

Como vimos, os tipos dos commits se relacionam com cada "acrônimo" da especificação SemVer:

  • fix: deve ser traduzido em liberações de PATCH;
  • feat: deve ser traduzido em liberações MINOR;
  • BREAKING CHANGE: deve ser traduzido, independentemente do tipo, em liberações MAJOR;

Hands-On

Bom, agora que já entendemos como o versionamento e os commits semânticos funcionam, vamos criar um projeto com as configurações ideais para:

  • Realizar o incremento automático de versões (coesas, através da análise dos commits);
  • Realizar a geração automática do arquivo CHANGELOG.md.
  • Realizar a distribuição/publicação da versão gerada (com a ajuda de CI/CD).

TL;DR: O arquivo CHANGELOG.md, na raiz do projeto, contém o descritivo de todas as alterações (pertintentes) feitas naquele específico projeto. A razão para criar e manter um changelog é simples: quando um colaborador ou usuário final deseja verificar se alguma alteração foi feita em um software, ele pode fazer isso com facilidade e precisão lendo o registro de alterações. Tudo o que eles precisam fazer é ir para o changelog e ele mostrará o que e quando as alterações foram feitas entre as diferentes versões ou lançamentos daquele software.

Iniciando o projeto

  1. Vamos criar um novo projeto NodeJS e criar o package.json, com o seguinte comando:
$ mkdir my-project && cd my-project && yarn init -y
Enter fullscreen mode Exit fullscreen mode
  1. Posteriormente, usaremos um hook do Git para que a cada vez que realizarmos um commit, seja feita uma análise do commit em questão para identificar se ele está ou não no padrão especificado pelo Conventional Commits. Então, vamos inicialiazar o git no projeto:
$ git init
Enter fullscreen mode Exit fullscreen mode

Habilitando a padronização de commits semânticos

Para realizar a análise dos commits criados, precisamos de uma ferramenta que fará esse trabalho e nos auxiliará a adotar os padrões que vimos anteriormente. Vamos então instalar e configurar o commitlint.

  1. Comece instalando os pacotes cli e config-conventional do commitlint nas dependências de desenvolvimento:
$ yarn add -D @commitlint/{config-conventional,cli}
Enter fullscreen mode Exit fullscreen mode
  1. Vamos criar o arquivo de configuração commitlint.config.js na raiz do projeto com o trecho abaixo:
module.exports = {
  extends: ["@commitlint/config-conventional"],
};
Enter fullscreen mode Exit fullscreen mode
  1. Valide as configurações com o comando:
echo 'foo: bar' | yarn commitlint
Enter fullscreen mode Exit fullscreen mode

Você deverá visualizar no terminal algo como:

⧗   input: foo: bar
✖   Please add rules to your `commitlint.config.js`
    - Getting started guide: https://git.io/fhHij
    - Example config: https://git.io/fhHip [empty-rules]

✖   found 1 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
Enter fullscreen mode Exit fullscreen mode

Instalando o husky e integrando-o ao commitlint

Bom, até aqui, apenas configuramos a ferramenta que realiza análise dos nossos commits. Para que ela seja usada, a cada novo commit, precisaremos instalar o husky e configurá-lo com o commitlint.

  1. Comece instalando o husky como dependência de desenvolvimento:
$ yarn add -D husky
Enter fullscreen mode Exit fullscreen mode
  1. Vamos agora habilitar o hook commit-msg criando um arquivo .huskyrc (na raiz do projeto) com o trecho abaixo:
{
  "hooks": {
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Valide a configuração criando um commit, no seguinte formato:
$ git commit -m "foo: bar" --allow-empty
Enter fullscreen mode Exit fullscreen mode

Você deverá visualizar no terminal algo como:

husky > commit-msg (node v12.16.1)
⧗   input: foo: bar
✖   Please add rules to your `commitlint.config.js`
    - Getting started guide: https://git.io/fhHij
    - Example config: https://git.io/fhHip [empty-rules]

✖   found 1 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

husky > commit-msg hook failed (add --no-verify to bypass)
Enter fullscreen mode Exit fullscreen mode

Perceba que o husky habilitou o hook commit-msg, o commitlint, por sua vez, foi executado e fez a análise do que escrevemos. Com isso os nossos commits serão analisados antes de serem criados!

Para que haja sucesso na criação do commit, ele deve estar padronizado seguindo a especificação.

Facilitando a criação de commits padronizados

Imagine que você esteja realizando um commit, e por ventura não se lembre de algum tipo que esteja na especificação, ou até mesmo não lembre do formato específico que comunique um breaking change, por exemplo. O Commitizen disponibiliza uma CLI que nos auxilia na criação de commits padronizados.

Essa configuração, é opcional, mas é bem interessante tê-la aqui para os futuros colaboradores terem mais facilidade e entendimento quanto aos padrões do projeto.

  1. Comece configurando a CLI no repositório com o comando:
$ npx commitizen init cz-conventional-changelog --yarn --dev
Enter fullscreen mode Exit fullscreen mode

Será adicionado ao package.json:

{
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-conventional-changelog"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. A seguir, vamos adicionar um script ao package.json para iniciar a CLI:
{
  "scripts": {
    "commit": "git-cz"
  }
}
Enter fullscreen mode Exit fullscreen mode

Execute o comando yarn commit --allow-empty. Você verá a ferramenta entrar em ação!

A flag --allow-empty, apesar de intuitiva vale a pena informar que ela possibilita a criação de commits sem ser necessário adicionar uma modificação em staged.


commitzen-git-cz.png

É possível definir o commitizen como padrão ao executar git commit no terminal, veja mais em: https://github.com/commitizen/cz-cli#optional-running-commitizen-on-git-commit

Extra: se o seu projeto for open-source, com essa configuração, é possível adicionar a badge "commitizen friendly" no README.md do repositório:

Commitizen friendly


[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
Enter fullscreen mode Exit fullscreen mode

Gerando versões semânticas e CHANGELOG

Até aqui, já podemos gerar os commits semânticos. Através deles, utilizaremos uma ferramenta que realiza a análise dos commits novos (adicionados desde a última versão do projeto) e determina qual será essa nova versão para a distribuição. Por padrão, ela também criará o CHANGELOG.md automaticamente de acordo com a análise feita.

Vamos configurar o projeto com o standard-version.

  1. Comece instale o standard-version como dependência de desenvolvimento:
$ yarn add -D standard-version
Enter fullscreen mode Exit fullscreen mode
  1. Em seguida, adicione o script abaixo no package.json:
{
  "scripts": {
    "release": "standard-version"
  }
}
Enter fullscreen mode Exit fullscreen mode

Ao executar o comando yarn release (ou npm rum release):

cmd-yarn-release.gif

  • Será realizada a análise dos commits feitos após o último release.
  • Será incrementada a versão do projeto no package.json, com base na análise dos commits.
  • Será gerado o CHANGELOG.md, incluindo os detalhes da nova versão.
  • Será criada uma tag com base na versão do package.json.

Para remover o prefixo "v" da tag, basta adicionar a flag -t sem nenhum valor no script criado, e.g.: standard-version -t \"\".

Depois de executar o comando, você pode publicar o projeto com o npm publish e enviar a tag gerada para o repositório remoto com git push --follow-tags origin master.

Workflow de desenvolvimento

Com ajuda de uma esteira de CI/CD, é possível automatizar o processo de publicação/distribuição de novas versões, para que a cada nova modificação na branch master, sejam executados os comandos:

  1. Gerando uma nova versão: yarn release (ou nom run release);

  2. Publicando a nova versão: npm publish

  3. Enviando a tag gerada para o repositório: git push --follow-tags origin master

Mas para que isso seja possível, deve-se seguir o seguinte fluxo de desenvolvimento:

development-workflow.png

  1. Criar uma nova feature branch a partir da branch principal (master);

  2. Realizar as alterações e "commitá-las" nos padrões estabelecidos pelas especificações;

  3. Mesclar as alterações na branch principal via Pull Request;

  4. A esteira de CI/CD deve ser acionada assim que houver uma nova alteração na branch principal, e (além de executar outros passos durante o processo, como testes, coleta de cobertura, lint, e etc.) incluir os comandos mencionados anteriormente.

Finalizando...

Bem, é isso, por hoje, é só!

Quero te agradecer por chegar até aqui, e queria lhe pedir também para me encaminhar as suas dúvidas, comentários, críticas, correções ou sugestões sobre a publicação.

Deixe seu ❤️ se gostou ou um 🦄 se esse post te ajudou de alguma maneira! Não se esqueça de ver os posts anteriores e me siga para maaaais conteúdos.

Até!

Top comments (2)

Collapse
 
lucianodiisouza profile image
O Primo Dev

Fala Willian, achei seu artigo simplesmente fantástico e atemporal, eu poderia usar ele como referência pra um vídeo meu no youtube?

Collapse
 
wnqueiroz profile image
William Queiroz

Opa Luciano! Mil perdões pela demora!

Mas é claro, compartilha o canal comigo por favor! 💙