GitHub Actions Template for .NET NuGet packages

This post outlines the template workflows I use for GitHub Actions to build, test and deploy .NET nuget packages.

It consists of two workflows, CI and CD. As these are templates, each repo will require some level of customization, but the structure remains relatively similar. Each step is denoted the with the current up-to-date version of the action available at the time of writing this post.

Continuous Integration

Continuous Integration (CI) is responsible for builds when new Pull Requests are opened, new commits are pushed, or master gets updated (for example a Pull Request is merged). CI is triggered on push or by workflow_dispatch which means a manual trigger.

name: CI

on: [push, workflow_dispatch]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Setup .NET
      uses: actions/setup-dotnet@v2
    - name: Install dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --no-restore
    - name: Test
      run: dotnet test --no-restore --no-build

It consists of 5 steps. First, checking out the source code. Second step installs dotnet CLI for the upcoming steps. This steps usually can customize which version of the SDK to use (or to use a pre-release version of it).

Next, dotnet restore is run to restore nuget package dependencies of the source. This step may define additional nuget sources for example to access an organization's internal packages.

Build dotnet build builds the binaries from the source code. Because the dependencies has been already restored a --no-restore is passed along. Build is usually customized with a path the solution or csproj project file to be built. It may also customize --output folder, especially interesting when the output is later packaged or deployed. Further customizations might involve setting the --configuration to Release or building a selfcontained application.

The final step of CI is testing. Testing could involve unit tests, functional tests, integration tests etc. For nuget packages the most common to have is, unit tests. Testing is instrumented with --no-restore --no-build flags as the restore and build steps for CI usually involves building the unit tests projects as well.

Continuous Deployment

Continuous Deployment (CD) is usually triggered for a new release of the library. As in this case the release means a new nuget package, it also means pushing the package to nuget.org.

name: CD

on:
  release:
    types: [created]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Setup .NET
      uses: actions/setup-dotnet@v2
    - name: Set VERSION variable from tag
      run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
    - name: Restore dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --configuration Release --no-restore -p:Version=${VERSION}
    - name: Create package
      run: dotnet pack --configuration Release --no-build --include-symbols -p:Version=${VERSION}
    - name: Publish
      run: dotnet nuget push **/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key $nuget_api_key --skip-duplicate
      env:
        nuget_api_key: ${{ secrets.NUGET_API_KEY }}

This workflow is triggered when a release is created for the repostiory on GitHub.

The first two steps of the CD workflow consists of the same steps as the CI. The first difference is Set Version step which set a version environment variable based on the tag added at the time of creating the release. This version must match the pattern of v1.2.3 where 1.2.3 version number should follow SemVer semantics. The version number may also be suffixed with a -prerelease label.

Restore and builds steps are similar to CI as well. The closer they stand, the more certain we can be that building the release will be successful. Build usually uses Release configuration and sets -p:Version=${VERSION} switch which applies the version number read from the tag on the output artifacts. Optionally one can execute tests after building the library, this template omits testing.

The final two steps are about packaging the library and uploading it to nuget. First dotnet pack command creates the nuget package. Next dotnet nuget push pushes the package to nuget. It uses a nuget_api_key key variable, which must be set as a GitHub Secret for actions in the repository settings page.