Validate NuGet packages before publishing from GitHub Actions
A big part of elmah.io is our clients for various web and logging frameworks. All of them are open-source, hosted on GitHub, and available as NuGet packages on nuget.org. I have blogged about building on GitHub Actions in the past. It struck me that I have never actually shared anything about the various steps we take for validating NuGet packages before pushing them. Let's fix that!
So, why do we need to validate NuGet packages in the first place? NuGet packages support a range of features like bundling debug symbols, providing source link, and more. Some of the features are provided out of the box when building the package while others need some custom work. The easiest way to inspect NuGet packages is the NuGet Package Explorer tool. With that installed, you can open .nupkg
files and see both metadata and the actual content.
When downloading the package from nuget.org and opening it with NuGet Package Explorer we get a view like this:
In the left part, beneath the Health section, you will see a range of health checks. The NuGet Signature is valid but three health checks are failing validation: Source Link, Deterministic, and Compiler Flags.
Before we start fixing the failed checks, let's extend the GitHub Actions pipeline for this repository. Inspecting packages manually and on every commit quickly becomes tedious so we want to automatically run these health checks when building the code. Luckily, the nice folks behind NuGet Package Explorer released support for running these checks from the command line too. The tool is called dotnet-validate
and can be installed and run on GitHub Actions (or anywhere else really) by including the following steps to the build file:
- name: Install dotnet-validate
run: dotnet tool install --global dotnet-validate --version 0.0.1-preview.304
- name: Validate NuGet package
run: dotnet-validate package local out/*.nupkg
As suggested by the names, the first step installs the tool and the next step runs it against .nupkg
files. In the case of the RickrollingMiddleware repository, the NuGet package is built into the out
directory but the path may vary by the way the package is built in other repositories.
Let's see what a build including these build steps looks like:
It shouldn't be much of a surprise to see the same three health checks failing in the build now. Failing the validation step will cause the entire build to fail so let's go and fix the errors.
Both the Source Link and Compiler Flags check fail because we are missing debug symbols in the built NuGet package. Debug symbols are additional information added to a .pdb
file when building .NET code and allow you to inspect and debug the code in the package. Debug symbols can be easily embedded in NuGet packages by including the following property inside a <PropertyGroup>
in the .csproj
file:
<DebugType>embedded</DebugType>
The corresponding UI action in Visual Studio can be achieved by right-clicking the project, click Properties, go to the Build tab, and set Debug symbols to Embedded:
Building the code now produces the following output:
Source Link and Compiler Flags pass the health checks after embedding debug symbols. .NET 8 heavily improved on the Source Link feature but for anything older, we need to do a bit of manual work to make everything work as expected. First, add the following properties in the .csproj
file:
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
Secondly, install a NuGet package matching the server where the code is hosted. In this case GitHub:
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
</ItemGroup>
There are packages available for all major Git-hosting platforms like DevOps and GitLab.
Let's move on to the Deterministic failing check. Building packages as deterministic ensures that the binaries inside the package are built in a reproducible way. When building code from a build server (or even your own machine), various inputs can change from build to build. Timestamps, hostnames, randomness, to name a few.
A deterministic build is done by setting the ContinuousIntegrationBuild
property to true. We only want this when building on the build server which can be configured by including the following code in the .csproj
file:
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
After this change, the NuGet package parses all health checks:
Before I let you go, I want to touch upon another type of NuGet package validation. NuGet packages support a native set of validation rules that can be enabled by including the following property in the .csproj
file:
<EnablePackageValidation>true</EnablePackageValidation>
Enabling package validation will run a series of checks after running the pack
command. The rules are already documented by Microsoft so I won't spend a lot of time going through each one in this post. One interesting and highly usable rule for library authors is the ability to verify that breaking changes are not introduced in the library interface. By including a previous version number of the NuGet package, package validation will automatically diff the interface between the old and new versions. You provide the version number to compare with by including a second property:
<PackageValidationBaselineVersion>1.0.22</PackageValidationBaselineVersion>
The version number needs to match a publically available version of the package on nuget.org.