Caching NuGet packages in GitHub Actions

Caching of NuGet packages can significantly speed up .NET builds in GitHub Actions, especially if your projects has many dependencies. The generic cache GitHub action used to be the only way to do that and I described how to configure it a while back in my article for DotNetCurry Magazine. However, recent versions of setup-dotnet GitHub action now have this functionality built in which makes the configuration even simpler.

Any existing GitHub Actions workflow for .NET projects most likely already uses the setup-dotnet GitHub action to install the .NET SDK. When created with the template from TimHeuer.GitHubActions.Templates NuGet package , for example, this is its configuration:

- name: Setup .NET SDK
  uses: actions/setup-dotnet@v4
  with:
    dotnet-version: 8.0.302

To enable caching of NuGet packages, you only need to set its cache parameter to true:

- name: Setup .NET SDK
  uses: actions/setup-dotnet@v4
  with:
    dotnet-version: 8.0.302
    cache: true

However, for this to work you need to enable lock files for your NuGet dependencies. You do that by adding the following property to each of the project files:

<PropertyGroup>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

This will cause a lock file named packages.lock.json to be generated in the folder with the project file. You need to add this file to source control.

You should also add the --locked-mode argument to the dotnet restore command in the GitHub Actions workflow file:

- name: Restore
  run: dotnet restore --locked-mode

This will prevent the lock files from being updated during restore and make sure that the exact versions of dependencies listed in the lock files will be used.

However, if you try to run the workflow file with these changes in place, it's likely going to fail with the following error (the exact path will of course differ):

Dependencies lock file is not found in /home/runner/work/20240726-dotnet-github-actions-cache/20240726-dotnet-github-actions-cache. Supported file patterns: packages.lock.json

The reason being that the setup-dotnet GitHub action expects the single packages.lock.json lock file to be placed in the repository root folder which is rarely going to happen. In my experience, most .NET repositories consist of multiple projects, each in its own subfolder. At best, their solution file will be placed in the root folder of the repository. To account for that, you can set the path to the lock file(s) using the cache-dependency-path parameter:

- name: Setup .NET SDK
  uses: actions/setup-dotnet@v4
  with:
    dotnet-version: 8.0.302
    cache: true
    cache-dependency-path: "**/packages.lock.json"

If you set the path to **/packages.lock.json as I did in the snippet above, all packages.lock.json files in the repository will be found and used. It's a good choice if your workflow file builds all project files in the repository.

Now, the build should succeed again. You can also check the logs to make sure the caching is working. In the first successful build, the following log entries for the Post Setup .NET SDK step indicate that the NuGet packages have been cached:

/usr/bin/tar --posix -cf cache.tzst --exclude cache.tzst -P -C /home/runner/work/20240726-dotnet-github-actions-cache/20240726-dotnet-github-actions-cache --files-from manifest.txt --use-compress-program zstdmt
Cache Size: ~50 MB (52461352 B)
Cache saved successfully
Cache saved with the key: dotnet-cache-Linux-9287935246dcf0938da38f834ff13de067c8c0708863a007cc9e061b1bad0a22

In subsequent builds with no changes to the NuGet package lock files, the following log entries for the Setup .NET SDK step indicate that the NuGet packages have been retrieved from cache:

Cache Size: ~50 MB (52461352 B)
/usr/bin/tar -xf /home/runner/work/_temp/38c773d8-0fc9-4d9f-8768-d3c88c688974/cache.tzst -P -C /home/runner/work/20240726-dotnet-github-actions-cache/20240726-dotnet-github-actions-cache --use-compress-program unzstd
Cache restored successfully
Cache restored from key: dotnet-cache-Linux-9287935246dcf0938da38f834ff13de067c8c0708863a007cc9e061b1bad0a22
Received 52461352 of 52461352 (100.0%), 50.0 MBs/sec

Of course, once any lock file changes, the changed NuGet packages will again be saved to cache with a new key.

You can find a working workflow file with a small sample project in my GitHub repository. I created separate commits for the changes I made to the workflow file.

Although it isn't all that complicated to configure NuGet package caching in GitHub Actions using the cache action, it's even more convenient to use the caching support in the setup-dotnet action instead. It's mostly preconfigured for you and therefore even easier to get working as long as your repository fulfills the requirements, i.e. uses NuGet lock files. Which is a good idea even if you're not caching NuGet packages since you can't really have reproducible builds without them.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License