NuGet packages and releases in GitHub Actions

September 27th 2024 NuGet .NET GitHub

As I wanted to make my side project more easily available to others by releasing it to GitHub and NuGet, I tried automating this step in GitHub Actions as well. It took me less time to get it working than expected.

I chose dotnet-version-cli for managing the version number. Apart from automatically increasing the version number depending on the type of the release, it also creates a corresponding tag in git. I decided to use that as an indicator for my GitHub Actions workflow to run the additional release steps after the build.

I wanted to keep both the continuous build with tests and the release part in the same workflow to avoid the issue of check runs being assigned to the wrong workflow run when there is more than one run per commit. This meant that I needed a way to identify whether there is a release tag on the commit that triggered the workflow run.

To do that, I first tried to add the tags as an additional trigger for my workflow. It would set the GITHUB_REF to the name of the tag which I could use as a condition for executing the additional release steps. Unfortunately, it didn't work as expected. When a commit with a matching tag was pushed, it triggered my workflow twice: once for the push on the main branch and once for the tag. Having two builds for the same commit was the exact thing I was trying to avoid.

I ended up checking for the tag on the current commit using git. For that to work, I first had to add the fetch-depth option to the actions/checkout action to also fetch tags:

- name: Checkout code  
  uses: actions/checkout@v4  
  with:  
    fetch-depth: 0

Only then could I try reading the tag value into an environment variable:

- name: Check for tag in current commit  
  run: echo "tag=$(git describe --tags --abbrev=0 --match 'v*' --exact-match)" >> "$GITHUB_ENV"

This allowed to check its value when deciding whether to run a particular release-only step:

if: startsWith(env.tag, 'v')

The first part of my release process was publishing the package to NuGet. I started by adding a bunch of NuGet specific properties to the project file:

<PropertyGroup>
  <PackageOutputPath>../nupkg</PackageOutputPath>
  <PackageId>[redacted]</PackageId>
  <PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
  <PackageReadmeFile>README.md</PackageReadmeFile>
  <PackageDescription>[redacted]</PackageDescription>
  <PackageTags>[redacted]</PackageTags>
  <Authors>[redacted]</Authors>
  <RepositoryUrl>[redacted]</RepositoryUrl>
  <PackageProjectUrl>[redacted]</PackageProjectUrl>
</PropertyGroup>

<ItemGroup>
  <None Include="../LICENSE.txt" Pack="true" PackagePath="LICENSE.txt" />
  <None Include="../README.md" Pack="true" PackagePath="README.md" />
</ItemGroup>

I then added a step to create the package (without building the code as that is already done in an earlier step before running the tests):

- name: Create NuGet package  
  if: startsWith(env.tag, 'v')  
  run: dotnet pack --configuration Release --no-build

It was now time to push the package to NuGet Gallery. For that, I had to create an API key. I restricted permissions as much as possible (I uploaded the first version of the package by hand so that I could do that):

  • I allowed it to Push only new package versions.
  • I selected only that one package from the list of Available Packages. To avoid having to recreate the key too often, I kept the maximum expiration period of 365 days. Once generated, I copied the key into a new NUGET_API_KEY repository secret (configurable on the Settings > Secrets and variables > Actions page).

With that in place, I could add a step to my workflow for publishing the package (thanks to the PackageOutputPath property in my project file I could use the simple nupkg/*.nupkg path to reference the generated file):

- name: Publish NuGet package  
  if: startsWith(env.tag, 'v')  
  run: dotnet nuget push nupkg/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://nuget.org

The final part of my release process was creating a new GitHub release with that same NuGet package. For that to work, I had to grant the required permission to my workflow:

permissions:  
  contents: write

In my step, I took advantage of the fact that the GitHub CLI is available in all workflows by default:

- name: Create GitHub release  
  if: startsWith(env.tag, 'v')  
  env:  
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}  
    tag: ${{ env.tag }}  
  run: gh release create "$tag" nupkg/*.nupkg --generate-notes

A few details worth pointing out:

  • The GitHub CLI requires the GitHub token to be stored in the GH_TOKEN environment variable.
  • I passed the tag into the tag environment variable so that I could use it in the arguments.
  • I left it to GitHub to auto-generate release notes for me. For now, I kept the defaults, but I will likely further configure them later.
  • I passed to the path to the NuGet package to include it in the release.

It took me only a couple of hours to get all of this working. I'm pretty happy with the result. It's not perfect, but it's a good starting point which I can tweak further as needed.

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

Copyright
Creative Commons License