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!

Validate NuGet packages before publishing from GitHub Actions

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.

For the rest of this post, I'll use a small package that I made as both a joke as well as a way to play with various features with NuGet, ASP.NET Core, and GitHub Actions. Say hello to RickrollingMiddleware. The package that lets you Rick Roll people and bots requesting invalid URLs on your website.

When downloading the package from nuget.org and opening it with NuGet Package Explorer we get a view like this:

NuGet Package Explorer

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:

Validate NuGet package on GitHub

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:

Enable Debug symbols

Building the code now produces the following output:

Healthy checks on GitHub

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:

Build green on GitHub

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.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website