A NuGet package workflow using GitHub Actions

Background

I’ve had a few ideas for this year’s C# Advent, but this one feels the most relevant at this time. The client I’m currently working with, builds their NuGet packages on developers’ machines; it’s a practice that generally works and is not too labour intensive, but occasionally steps are missed and problems can occur. The relative infrequency of builds means developers are not sufficiently motivated to automate the task, but as we’ll see, it’s not too difficult. This post walks through my preferred solution.

Is this for me?

However you build your NuGet packages, I hope you’ll find this post useful. I’m going to walk through how to build a simple NuGet package (along with a test project) in GitHub Actions. We will progress from building every push to a main branch, through building Pull Requests to publishing to a NuGet package repository. Later on I’ll demonstrate how to build pre-release NuGet packages and end by showing you my attempt at auto-generating release notes.

TL;DR;

The code and workflows demonstrated here can be found in GitHub at https://github.com/acraven/blog-nuget-workflow-github-actions. Feel free to fork the repository, push some commits, cross your fingers, and watch the workflows run. I’ve tried to keep the commits in line with the sections in this post so you can refer back if needs be.

Getting Started

I’ve created a baseline solution containing a library project and a test project. I’ve pushed that as the first commit to my repository — hopefully, if your existing repository is not too different, we can add the workflows together.

Build and Test Every Push to Main

The first thing we should do is ensure that the code can be built, and all tests run successfully after pushing to the main branch. We are going to configure GitHub Actions to do this by adding a new file.

on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Build
run: dotnet build --configuration Release
- name: Test
run: dotnet test --configuration Release --no-build

Build and Test Your Pull Requests

Replace the trigger condition in the ci.yml file as shown below and commit the change, but don’t push just yet.

on:
push:
branches:
- main
pull_request:
branches:
- main

How to Publish to a NuGet Package Repository

Create a new file .github/workflows/release.yml in your repository containing the following snippet, but don’t commit just yet. Before we continue, you’ll need to change the name of the package in the push step as well as changing the username in the [package repository] source attribute.

on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Verify commit exists in origin/main
run: |
git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
git branch --remote --contains | grep origin/main
- name: Set VERSION variable from tag
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
- name: Build
run: dotnet build --configuration Release /p:Version=${VERSION}
- name: Test
run: dotnet test --configuration Release /p:Version=${VERSION} --no-build
- name: Pack
run: dotnet pack --configuration Release /p:Version=${VERSION} --no-build --output .
- name: Push
run: dotnet nuget push NuGet.Workflow.${VERSION}.nupkg --source https://nuget.pkg.github.com/acraven/index.json --api-key ${GITHUB_TOKEN}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  • The verify step ensures that the commit tagged with the version number exists in the main branch. This could easily be removed, but it protects me and others (but mostly me) from incorrectly tagging a short lived branch.
  • The version (without the v prefix) is extracted from the tag and used to set the VERSION environment variable for the remaining steps.
  • The build and test steps are the same except that the VERSION is embedded in the package build.
  • The pack step prepares the package for pushing to a package repository. The .nuget extension is the convention, but it’s just a zip file. If you were to add a .zip extension you can view the contents, which can be useful for debugging purposes.
  • Finally the push step pushes the package to the GitHub Packages package repository. You could push to another NuGet package repository by changing the source attribute, but you’ll need to provide an access token via an explicit GitHub Secret.
  <PropertyGroup>
<PackageId>NuGet.Workflow</PackageId>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/acraven/blog-nuget-workflow-github-actions</RepositoryUrl>
</PropertyGroup>

What about Pre-release packages?

There are times when developers need to be able share packages that aren’t considered complete. The NuGet specification enables this with the concept of a Pre-release flag — adding a - and a suffix to the version number of the package is enough to set this flag. I happen to use -preview### but other patterns including alpha### and beta### are equally as good.

on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+-preview[0-9][0-9][0-9]"
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set VERSION variable from tag
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
- name: Pack
run: dotnet pack --configuration Release /p:Version=${VERSION} --output .
- name: Push
run: dotnet nuget push NuGet.Workflow.${VERSION}.nupkg --source https://nuget.pkg.github.com/acraven/index.json --api-key ${GITHUB_TOKEN}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

What about Release Notes?

I was challenged by a colleague to add release notes to the package build, which I accepted, and kind of achieved, but it feels like I cheated a little as you’ll see. It’s very much a work-in-progress so feel free to skip this section.

    - name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Verify commit exists in origin/main
run: git branch --remote --contains | grep origin/main
- name: Extract release notes
run: |
git log --pretty=format:'%d %s' ${GITHUB_REF} | perl -pe 's| \(.*tag: v(\d+.\d+.\d+(-preview\d{3})?)(, .*?)*\)|\n## \1\n|g' > RELEASE-NOTES.txt
  <Target Name="PrepareReleaseNotes" BeforeTargets="GenerateNuspec">
<PropertyGroup>
<PackageDescription>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../../RELEASE-NOTES"))</PackageDescription>
</PropertyGroup>
</Target>

Summary

We’ve seen how to automate workflows for developing and publishing NuGet packages. There are many other ways of achieving this, but I’ve found this approach successful for managing departmental NuGet packages where there are generally only double digits of consumers with good communication channels. For public NuGet packages with potentially 10s or 100s of 1000s of consumers, skipping testing for pre-release packages is probably not desirable and the depth of the release notes would no doubt be found to be lacking.

Credits

Credit is due to https://dev.to/j_sakamoto and https://dev.to/dimay for their post https://dev.to/j_sakamoto/writing-a-nuget-package-release-notes-in-an-outside-of-a-csproj-file-3f94 and comment respectively. Their solution of how to add an external release notes file into a .csproj file was most useful.

Freelance Engineer, Architect, Problem Solver etc. https://twitter.com/acraven_dev