Automated versioning and publishing of a Non-SDK-style NuGet packages using GitHub Actions
•Posts in this series
- Automated versioning and publishing of a Non-SDK-style NuGet packages using GitHub Actions (This post)
- Automated versioning and publishing of a SDK-style NuGet packages using GitHub Actions
This is a series of two articles looking at how I implemented automated packaging and building of NuGet packages in GitHub actions for two open-source libraries; JsonPatch and GuidOne. One was a SDK style project, and the other was a Non-SDK style project which made for a good look the the differences between the packaging processes.
In this first article we look at the process that we used for JsonPatch. It is a Non-SDK style project so the process is a bit more convoluted than a simple SDK style project and the process we used covers the following points.
- Support for multiple .NET versions
- Versioning & release notes using GitHub Releases
- Set the repository metadata
Background
I maintain a little open-source library called JsonPatch which is primarily distributed through NuGet. Currently I just ad-hoc build the package using the NuGet Package Explorer and manually upload the resulting package through the online interface. By automating this process I hope that the package will be updated more frequently so that contributers submissions are published without too much of a delay. I also like that the package will be created in a reliable, repeatable manner; I won’t have that niggling worry that I’ve forgotten something.
The workflow
The project is built for every pull-request and commit against the main branch, but only packaged and published when a release is created (which defines the version and notes for the published package). Admittedly this was designed with GitHub in mind as it relies on their Releases feature for versioning but can be adapted to most other build systems with similar concepts. Given releases are defined by esentially tagging the main branch this makes it compatible with most development processes such as Git Flow, Trunk-based, or Feature-based.
Step 1: Build and Test
The first step is to make sure we can build the library that we want to distribute. This includes running all tests and ensuring they are successful. We will run our build and test workflow on every commit or pull-request to the main branch so we know that
- Any commits to the main branch are valid
- Any PRs that come through won’t break the build
For the library JsonPatch we need to build two projects, one for .NET 4.8 and another for .NET 6 - here is our workflow file that we place in .github/workflows/build.yml. It’s nice and simple so I haven’t annotated it and the most interesting bit is setting up MSBuild and VSTest so we can compile and test the legacy .NET Framework version of the library.
name: Build JsonPatch library
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
name: Build JsonPatch library
runs-on: windows-latest
steps:
- name: '📄 Checkout'
uses: actions/checkout@v3
- name: 🛠️ Setup MSBuild
uses: microsoft/setup-msbuild@v1
- name: 🛠️ Setup NuGet
uses: nuget/setup-nuget@v1
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
nuget-version: '5.x'
- name: 🍎 Restore NuGet packages
run: nuget restore JsonPatch.sln
- name: 🚀 Build .NET 4.8 JsonPatch.dll Tests
run: msbuild /p:Configuration=Release /p:IncludeSymbols=true src/JsonPatch.Tests/JsonPatch.Tests.csproj
- name: 👟 Run .NET 4.8 JsonPatch.dll Tests
uses: microsoft/[email protected]
with:
testAssembly: JsonPatch.Tests*.dll
searchFolder: src/JsonPatch.Tests/bin/Release/
runInParallel: true
- name: 🚀 Build .NET 4.8 JsonPatch.dll
run: msbuild /p:Configuration=Release /p:IncludeSymbols=true src/JsonPatch/JsonPatch.csproj
- name: 🚀 Build .NET 6 JsonPatchCore.dll
run: dotnet build src/JsonPatchCore/JsonPatchCore.csproj --configuration Release
This workflow will ensure the library can be built and runs all of the unit tests, this includes checking all the pull-requests which are submitted to the main branch.
Cool cool cool, nice green checkmark if all the tests pass, but we’re still missing the most important step - packaging and publishing the NuGet package so that others can use all the latest changes.
Step 2: The .nuspec file
Because one of the projects we’re packaging up is .NET Framework, this is a Non-SDK style project and we’re going to be packing it up using the NuGet CLI and a .nuspec file. We also generate two different dlls, one for .NET Framework and the other for .NET which makes for a little more complicated NuGet package as we will be using NuGet’s support for multiple .NET versions.
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>JsonPatch</id>
<version>$version$</version>
<title>JsonPatch</title>
<authors>Michael McKenna</authors>
<owners>Michael McKenna</owners>
<tags>json json-patch jsonpatch minimal api minimal-api patch</tags>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/myquay/JsonPatch</projectUrl>
<repository type="git" url="https://github.com/myquay/JsonPatch.git" branch="master" commit="$commit$" />
<description>
... omitted ...
</description>
<releaseNotes>
$releasenotes$
</releaseNotes>
<copyright>Copyright 2012-2023 Michael McKenna</copyright>
<dependencies>
<group targetFramework="net48">
<dependency id="Newtonsoft.Json" version="13.0.1" />
<dependency id="Microsoft.AspNet.WebApi.Client" version="5.2.9" />
</group>
</dependencies>
<frameworkReferences>
<group targetFramework="net6.0">
<dependency name="Microsoft.AspNetCore.App"/>
</group>
</frameworkReferences>
</metadata>
</package>
The .nuspec file is going to be used in the packaging workflow triggered when we create a new release, not when we commit code, this workflow will build and package the NuGet file. The workflow will replace the tokens such as $version$ and $releasenotes$ with information from the release that triggers the workflow. The tokens will be replaced using NuGet’s replacement tokens feature. The .nuspec file itself is kept in source at the root of the solution so any updates to the package metadata are managed and released using the same process as the library source.
Step 3: The NuGet packaging workflow
We’re going to create a new workflow called release.yml which is going to be a superset of our previous pipeline, in addition to building and testing the library it will also pack it.
The trigger for this workflow will be on the creation of a new Release in GitHub.
name: Build, Package & Release JsonPatch library
on:
release:
types: [published]
Once triggered, build and test the library that’s to be packaged up.
jobs:
build:
name: Build JsonPatch library
runs-on: windows-latest
steps:
- name: '📄 Checkout'
uses: actions/checkout@v3
- name: 🛠️ Setup MSBuild
uses: microsoft/setup-msbuild@v1
- name: 🛠️ Setup NuGet
uses: nuget/setup-nuget@v1
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
nuget-version: '5.x'
- name: 🍎 Restore NuGet packages
run: nuget restore JsonPatch.sln
- name: 🚀 Build .NET 4.8 JsonPatch.dll Tests
run: msbuild /p:Configuration=Release /p:IncludeSymbols=true src/JsonPatch.Tests/JsonPatch.Tests.csproj
- name: 👟 Run .NET 4.8 JsonPatch.dll Tests
uses: microsoft/[email protected]
with:
testAssembly: JsonPatch.Tests*.dll
searchFolder: src/JsonPatch.Tests/bin/Release/
runInParallel: true
- name: 🚀 Build .NET 4.8 JsonPatch.dll
run: msbuild /p:Configuration=Release /p:IncludeSymbols=true src/JsonPatch/JsonPatch.csproj
- name: 🚀 Build .NET 6 JsonPatchCore.dll
run: dotnet build src/JsonPatchCore/JsonPatchCore.csproj --configuration Release
Then we copy to built DLLs to a staging directory along with the .nuspec file ready to be packaged up.
- name: 📄 Copy DLLs and NuSpec to working folder
run: |
mkdir \pack\json-patch\lib\net6.0
mkdir \pack\json-patch\lib\net48
copy src\JsonPatch.nuspec \pack\json-patch
copy src\JsonPatch\bin\Release\JsonPatch.Common.dll \pack\json-patch\lib\net48
copy src\JsonPatch\bin\Release\JsonPatch.Common.pdb \pack\json-patch\lib\net48
copy src\JsonPatch\bin\Release\JsonPatch.dll \pack\json-patch\lib\net48
copy src\JsonPatch\bin\Release\JsonPatch.pdb \pack\json-patch\lib\net48
copy src\JsonPatchCore\bin\Release\net6.0\JsonPatch.Common.dll \pack\json-patch\lib\net6.0
copy src\JsonPatchCore\bin\Release\net6.0\JsonPatch.Common.pdb \pack\json-patch\lib\net6.0
copy src\JsonPatchCore\bin\Release\net6.0\JsonPatchCore.dll \pack\json-patch\lib\net6.0
copy src\JsonPatchCore\bin\Release\net6.0\JsonPatchCore.pdb \pack\json-patch\lib\net6.0
copy src\JsonPatchCore\bin\Release\net6.0\JsonPatchCore.xml \pack\json-patch\lib\net6.0
Then we pack the NuGet package, the secret sauce here is using the NuGet replacement tokens feature to automatically update the version, release notes, and commit hash of the .nuspec file. We are assuming that a release triggers the workflow which is how we have access to the version and notes in the GitHub context.
- name: 📦 Pack NuGet package
run: nuget pack \pack\json-patch\JsonPatch.nuspec -p version=${{github.event.release.tag_name}} -p releasenotes="${{github.event.release.body}}" -p commit=${{github.sha}}
shell: powershell
Finally we archive the package and publish it to NuGet
- name: 💾 Archive package
uses: actions/upload-artifact@v3
with:
name: nuget-package
path: \a\JsonPatch\JsonPatch\JsonPatch.*.nupkg
- name: 🌐 Push NuGet package live
run: nuget push \a\JsonPatch\JsonPatch\JsonPatch.*.nupkg -src https://api.nuget.org/v3/index.json
shell: powershell
Now whenever we create a release in our GitHub repository it’ll be automatically packaged up and pushed to NuGet.