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.*ou2.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
- Troque versões flutuantes por versões concretas nas referências diretas da aplicação.
- Ligue o arquivo de lock com
RestorePackagesWithLockFile. O restore passa a gerar umpackages.lock.jsoncom a versão resolvida de cada pacote, inclusive transitivos. - Faça commit do
packages.lock.json. Ele é o grafo que o CI deve reproduzir. - No CI, restaure com
--locked-mode. Se o grafo resolvido diferir do arquivo, o restore falha em vez de seguir com outra versão. - Declare os feeds num
nuget.configversionado e diga de qual feed vem cada pacote. - 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
.csprojdo 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.jsoncom 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. RestorePackagesWithLockFileestá ligado e opackages.lock.jsonestá no Git.- O CI roda
dotnet restore --locked-mode. - O
nuget.configestá 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?
Referência: https://alexalvess.medium.com/criando-pacotes-nuget-ec04dc6425a3