Versionamento semântico simplificado com MinVer

MinVer é uma ferramenta extremamente útil para facilitar o versionamento semântico de assemblies .NET usando Git tags.

O versionamento semântico, de acordo com o SemVer 2.0.0, segue a seguinte estrutura: [Major].[Minor].[Patch]. Resumindo:

  • Quando a Major sobe, isso indica que ouve uma grande mudança, e possível quebra de compatibilidade com as versões anteriores
  • Quando a Minor sobe, isso indica adição de funcionalidade, sem perda de compatibilidade
  • Quando o Patch sobe, isso indica correções ou melhorias

Versionamento semântico mal feito

Tradicionalmente, a forma como controlamos a versão de um assembly é através da tag <PackageVersion>1.2.3</PackageVersion> no arquivo .csproj de um projeto (ou nos arquivos AssemblyInfo.cs em projetos mais antigos).

No exemplo <PackageVersion>1.2.3</PackageVersion>, estamos usando a versão fixa no código:

[Hard Coded].[Hard Coded].[Hard Coded]
1.2.3

Mas se esse processo for feito sempre manualmente, as chances são grandes de esquecermos de trocar a versão em alguns dos assemblies do projeto, e esses assemblies serão compilados com a versão errada, ou até mesmo, ficarem eternamente na versão 1.0.0:

Detalhes do arquivo ForeverFactory.dll, com a versão 1.0.0.0

Uma alternativa bastante comum e fácil de implementar, é ajustar a Major e Minor manualmente, e subir o Patch de acordo com o número do build do pipeline de integração contínua:

[Hard Coded].[Hard Coded].[CI Build Number]
1.2.64 (o pipeline já rodou 64x)

Esse processo pode funcionar no começo de um projeto, mas não é sustentável a longo prazo. O principal motivo é que não é possível identificar facilmente a versão do programa através da árvore de commits do Git.

E é aí que entram as tags do Git. Com elas, é fácil identificar em qual versão um software se encontra:

Árvore de commits do repositório Git do repositório ForeverFactory, com a tag versão v1.1.0 facilmente visível

Há ainda o risco de as tags do git não refletirem a versão dos assemblies nos commits que elas representam. Um exemplo simples dessa situação seria um commit com a tag 1.2.0 em que a versão do projeto no código continua com a versão 1.0.0. Se fizermos o build do código deste exato commit, a versão do assembly será 1.0.0, e não 1.2.0, como esperaríamos a partir da tag de versão.

Essa situação pode ser facilmente corrigida com o auxílio do MinVer.

Como o MinVer pode ajudar

Em primeiro lugar, o uso do MinVer é muito simples. Basta referenciá-lo no projeto, e sem necessidade de mais configurações, a versão dos assemblies passará a ser baseada na última tag de versão encontrada no histórico do Git.

Por padrão, o MinVer utiliza as seguintes configurações, satisfazendo o guia oficial de versionamento para bibliotecas open-source .NET:

Property Value
AssemblyVersion {MinVerMajor}.0.0.0
FileVersion {MinVerMajor}.{MinVerMinor}.{MinVerPatch}.0
PackageVersion {MinVerVersion}
Version {MinVerVersion}

Essas configurações podem ser editadas livremente no arquivo do projeto.

MinVer CLI

Também existe uma interface de linha de comando que pode nos auxiliar com algumas configurações. Para instalá-la, basta rodar o comando abaixo:

dotnet tool install --global minver-cli --version 2.5.0

Com a CLI instalada, podemos pedir para o MinVer calcular a próxima versão semântica informando a parte do que queremos incrementar, podendo ser major, minor ou patch (padrão).

Por exemplo, ao rodar o comando minver --auto-increment minor --tag-prefix=v na raiz do repositório da biblioteca ForeverFactory, que está atualmente na versão v1.1.0, temos essa resposta:

MinVer: Using { Commit: 3450a1e, Tag: 'v1.1.0', Version: 1.1.0, Height: 2 }.
MinVer: Calculated version 1.2.0-alpha.0.2.
1.2.0-alpha.0.2

Usei o parâmetro --tag-prefix=v apenas porque decidi prefixar todas as tags de versão com o caractere v, como em v1.1.0.

Olhando para o resultado, vale notar que além da versão que foi incrementada para 1.2.0, o MinVer também traz uma segunda parte alpha.0.2. Vamos decompô-la para entendermos o que significa:

  • alpha é valor padrão da fase de pré-release. Este valor pode ser trocado com o parâmetro -d|--default-pre-release-phase <PHASE>, onde fase pode ser algo como alpha, beta, preview, rc, etc;
  • O segundo valor, 0, indica o intervalo de versões de pré-releases lançadas até agora. Este valor vai subir só depois de publicarmos uma versão de pré-release;
  • O último valor, 2, é uma métrica chamada altura, calculada em número de commits desde o último release.

Uma premissa importante do MinVer, é que o tageamento de versão é feito antes da publicação. Logo, a expectativa é que a versão compilada será baseada na tag de versão semântica mais recente encontrada no histórico do Git.

A estratégia de versionamento da ForeverFactory

Logo após concluir a primeira versão da biblioteca ForeverFactory, publiquei essa versão no NuGet. O versionamento era 100% manual através da propriedade <PackageVersion>.

Mas logo depois, encontrei alguns problemas, e precisei lançar algumas versões corretivas. Depois de meia dúzia de versões corretivas, ficou claro que versionar dessa forma não seria prático, nem o processo seria natural.

Consultei então o repositório do MediatR, uma das minhas bibliotecas preferidas para .NET, a fim de verificar como é feito o versionamento. E foi aí que me deparei com o MinVer.

Usando o MinVer no projeto

Essa é a parte fácil. Basta adicionar uma referência NuGet para o MinVer com o comando dotnet add package MinVer.

Feito isso, para garantir que o MinVer vai funcionar corretamente, é importante conferir no arquivo .csproj se a referência inclui a propriedade PrivateAssets="All". Deve ficar assim:

<PackageReference Include="MinVer" Version="2.5.0">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

A partir desse momento, o MinVer vai se encarregar de executar um processo conhecido como assembly patching, e cada build utilizará a versão da última tag de versão. Simples assim.

Obs.: se não houver tag nenhuma, a versão provavelmente será 0.0.0-alpha.0.

No meu caso, como já estava tageando as versões com o prefixo v, adicionei também a propriedade <MinVerTagPrefix>v</MinVerTagPrefix> nas propriedades do projeto, para o MinVer considerar minhas tags de versão.

Como fazer releases baseado em tags

A abordagem de versionamento do repositório jbogard/MediatR é bem minimalista, e foi muito fácil de adotá-la.

Se conferirmos o workflow do GitHub abaixo, veremos que o processo de release é disparado a partir do push de novas tags de versão, que naturalmente seguem o padrão *.*.*.

name: Release
on:
  push:
    tags:
    - '*.*.*'
jobs:
  release:
    strategy:
      fail-fast: false
    runs-on: ubuntu-latest   
    steps:
      - uses: actions/checkout@v2
      - name: Setup dotnet 5.0
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: 5.0.x
      - name: Clean
        run: dotnet clean -c Release
      - name: Build
        run: dotnet build -c Release
      - name: Test
        run: dotnet test -c Release -r nuget-package --no-build -l trx --verbosity=normal
      - name: Pack
        run: dotnet pack src/ForeverFactory/ForeverFactory.csproj -c Release --no-build -o nuget-package
      - name: Publish to Nuget.org
        run: dotnet nuget push nuget-package/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json

Assim, quando chegar a hora de liberar uma nova versão, a única coisa que preciso fazer é adicionar uma tag de versão no commit escolhido, e a mágica acontece: a biblioteca é compilada, testada, empacotada e publicada no Nuget, com a versão da tag que disparou o pipeline.

Você pode conferir o resultado lá no Nuget neste link.

Pensamentos finais

O MinVer é perfeito para projetos pequenos e times reduzidos. Ele elimina toda burocracia que envolve o versionamento de um projeto, ao mesmo tempo que nos ajuda a seguir as melhores práticas de versionamento semântico.

Mas para workflows mais complexos, produtos maiores e equipes grandes, com múltiplos squads, é mais apropriado considerar ferramentas mais robustas, como o GitVersion.

Espero que este post se mostre útil para você. Para mim, ele já vem funcionando, e trouxe simplicidade e consistência ao versionamento da biblioteca. Certamente usarei esse mesmo fluxo em futuros projetos também.

Outras opções para compartilhar:
comments powered by Disqus