Programação Por Equipe DevBras

Capa do artigo: Pacote NuGet: trave a versão para o CI compilar o mesmo que a sua máquina

Pacote NuGet: trave a versão para o CI compilar o mesmo que a sua máquina

Entenda como o NuGet resolve versões, quando o restore muda sozinho, como usar packages.lock.json, gestão central de versões e mapeamento de feeds, e como publicar o seu pacote.

Quinta, 17h: o pipeline do Faturamento.Api falha com erro de compilação num arquivo que ninguém tocou. Na sua máquina, compila. A diferença está no restore: o projeto referenciava o pacote interno Empresa.Fiscal com Version="2.*", alguém publicou a 2.4.0 de manhã, e o CI baixou a versão nova. A resposta curta: evite versão flutuante em aplicação, ligue o arquivo de lock (packages.lock.json) e rode o restore do CI com --locked-mode. Atualizar dependência vira um commit, não um efeito colateral.

Este guia explica como o NuGet escolhe versões, monta o Faturamento.Api com versão travada, centraliza versões numa solução com vários projetos e mostra como publicar o Empresa.Fiscal sem quebrar quem depende dele.

Como o NuGet escolhe a versão

Há uma confusão comum aqui. <PackageReference Include="Pacote" Version="13.0.3" /> não significa "exatamente 13.0.3". Significa "13.0.3 ou superior". Só que, diante de um intervalo, o NuGet escolhe a menor versão que atende, não a maior. Então, se a 13.0.3 existe no feed, é ela que vem, hoje e daqui a um ano.

O build muda sozinho em três situações:

  • Versão flutuante. 2.* ou 2.3.* pedem a maior versão que combina. Cada publicação nova muda o resultado do restore.
  • Dependência transitiva. O pacote A depende de B com um mínimo. Se você sobe A, ou se dois pacotes pedem mínimos diferentes de B, a versão de B muda sem aparecer no seu .csproj.
  • Feed diferente. O CI enxerga um feed que a sua máquina não enxerga, ou vice-versa, e um pacote com o mesmo nome vem de outro lugar.

Para fixar uma versão exata, a notação é [13.0.3], com colchetes. Use com parcimônia: exatidão em toda parte gera conflito quando duas bibliotecas pedem versões diferentes da mesma dependência.

Como travar o build do jeito certo

  1. Troque versões flutuantes por versões concretas nas referências diretas da aplicação.
  2. Ligue o arquivo de lock com RestorePackagesWithLockFile. O restore passa a gerar um packages.lock.json com a versão resolvida de cada pacote, inclusive transitivos.
  3. Faça commit do packages.lock.json. Ele é o grafo que o CI deve reproduzir.
  4. No CI, restaure com --locked-mode. Se o grafo resolvido diferir do arquivo, o restore falha em vez de seguir com outra versão.
  5. Declare os feeds num nuget.config versionado e diga de qual feed vem cada pacote.
  6. Atualize de propósito: mude a versão, rode o restore, revise o diff do lock, rode os testes e faça commit.

O projeto com lock ligado

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Empresa.Fiscal" Version="2.3.1" />
    <PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
  </ItemGroup>
</Project>

O passo do CI

- name: Restore
  run: dotnet restore --locked-mode
- name: Build
  run: dotnet build --no-restore -c Release
- name: Test
  run: dotnet test --no-build -c Release

Com isso, a publicação da 2.4.0 não muda nada no pipeline. Ela só entra quando alguém trocar 2.3.1 por 2.4.0 e o lock for atualizado no mesmo commit.

Vários projetos, uma versão: gestão central

Numa solução com Faturamento.Api, Faturamento.Worker e Faturamento.Testes, é comum cada projeto pedir uma versão diferente do mesmo pacote. A gestão central de pacotes resolve isso com um arquivo Directory.Packages.props na raiz:

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Empresa.Fiscal" Version="2.3.1" />
    <PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" />
  </ItemGroup>
</Project>

Nos projetos, a referência fica sem versão: <PackageReference Include="Empresa.Fiscal" />. Atualizar passa a ser uma linha num arquivo só, e o lock de cada projeto acompanha.

De qual feed vem cada pacote

Com feed público e feed interno juntos, um pacote com o mesmo nome do seu, publicado no nuget.org por outra pessoa, pode ser escolhido pelo restore. O mapeamento de origem de pacotes fecha essa porta:

<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
    <add key="interno" value="https://pkgs.exemplo.com.br/nuget/v3/index.json" />
  </packageSources>
  <packageSourceMapping>
    <packageSource key="nuget.org">
      <package pattern="*" />
    </packageSource>
    <packageSource key="interno">
      <package pattern="Empresa.*" />
    </packageSource>
  </packageSourceMapping>
</configuration>

O padrão mais específico vence: tudo que começa com Empresa. só vem do feed interno. O <clear /> garante que feeds configurados na máquina de alguém não entrem na conta.

Publicando o seu pacote sem quebrar ninguém

Do outro lado, o time que mantém o Empresa.Fiscal precisa tratar a versão como contrato. Siga versionamento semântico: correção sobe o último número, recurso novo compatível sobe o do meio, mudança que quebra quem chama sobe o primeiro.

dotnet pack src/Empresa.Fiscal -c Release -p:Version=2.4.0 -o ./artefatos
dotnet nuget push ./artefatos/Empresa.Fiscal.2.4.0.nupkg --source interno --api-key "$NUGET_API_KEY"

Algumas regras que evitam dor:

  • Biblioteca declara mínimo, aplicação trava. No .csproj do pacote, dependências com versão mínima simples dão espaço para quem consome resolver conflitos.
  • Versão publicada é imutável. O nuget.org não aceita enviar de novo a mesma versão e não permite apagar, só ocultar da busca. Feeds internos costumam seguir a mesma política, e deveriam.
  • Nada de segredo dentro do pacote. Um appsettings.json com senha empacotado por engano fica no feed para sempre; troque a credencial, como em segredo no Git se troca antes de limpar.
  • Notas do que mudou acompanham cada versão, para quem atualiza saber o que testar.

Erros comuns

  • Versão flutuante em aplicação. O build de amanhã compila outra coisa e quebra sem commit.
  • Lock gerado e fora do Git. O arquivo existe na sua máquina e o CI resolve do zero.
  • CI sem --locked-mode. O restore atualiza o lock em silêncio e o build segue com o grafo novo.
  • Feeds sem mapeamento. Um pacote homônimo de outro feed entra no build.
  • Quebrar a API numa versão de correção. Quem aceitava só correções recebe a quebra.
  • Publicar pacote interno no feed público "para testar". O código fica exposto e não dá para apagar.

Checklist

  • Nenhuma referência direta da aplicação usa * na versão.
  • RestorePackagesWithLockFile está ligado e o packages.lock.json está no Git.
  • O CI roda dotnet restore --locked-mode.
  • O nuget.config está no repositório, começa com <clear /> e mapeia cada prefixo ao seu feed.
  • Soluções com vários projetos usam Directory.Packages.props.
  • O pacote publicado segue versionamento semântico e tem notas da versão.
  • A chave de publicação vem de um segredo do CI, não do repositório.

Para ver o que está desatualizado sem mudar nada, rode dotnet list package --outdated --include-transitive; para falhas conhecidas, dotnet list package --vulnerable. A mesma lógica de "apelido que muda" aparece em imagens Docker, no post tag latest não é uma versão.

Apague a pasta obj do seu projeto, rode o restore e compare o resultado com o do CI. Qual dependência sua ainda aceita qualquer versão futura, e quem foi avisado quando ela mudou da última vez?

Avalie este artigo

Referência: https://alexalvess.medium.com/criando-pacotes-nuget-ec04dc6425a3