GitHub Packages: Publishing NuGet Packages using NUKE (with GitVersion and GitHub Actions)

Somebody who likes to code
Search for a command to run...

Somebody who likes to code
No comments yet. Be the first to comment.
Nuke is an open source, cross-platform build automation solution for .NET projects. Nuke prides itself on its simplicity and extensibility that makes build automation approachable for everyone.
In this post, we will explore the benefits of Nuke and its most common features as we deploy a web app to Azure. Our goal is to achieve a flexible, maintainable, automated build process. Nuke is an open source, cross-platform build automation soluti...
Google processes tens of thousands of code changes every day across a codebase of hundreds of millions of lines. The secret isn't magic tooling — it's a documented, principled approach to code review

Amazon Simple Notification Service (SNS) looks straightforward on the surface — publish a message, deliver it to subscribers — but the gap between a topic that "works in dev" and one that is productio

Amazon Simple Queue Service (SQS) is deceptively simple to get started with, but surprisingly easy to misconfigure in ways that cause silent message loss, runaway costs, duplicate processing storms, o

HTTP API is the right default choice for the majority of .NET serverless workloads. At 70% less cost than REST API, lower latency, and native JWT authorization, it covers most production requirements

AWS Lambda has become a cornerstone of modern serverless architectures, but writing a function that "just works" is very different from writing one that is performant, cost-effective, secure, and read

GitHub Packages is a platform for hosting and managing packages, including containers and other dependencies. GitHub Packages combines your source code and packages in one place to provide integrated permissions management and billing, so you can centralize your software development on GitHub.
In this article, we will further explore the capabilities of Nuke as an automation tool. Specifically, we will establish a CI/CD pipeline to upload a NuGet Package to GitHub Packages.
Let's create an empty class library, and run the following commands:
dotnet new classlib -o MyLib
dotnet new sln -n MyLib
dotnet sln add --in-root MyLib
dotnet tool install Nuke.GlobalTool --global
nuke :setup
Open the solution, navigate to the build_ project, and delete all the default targets in the Build.cs file.
Login into our GitHub account, go to Settings, Developer Settings, Personal Access Tokens, and Tokens (classic). Click the Generate new token (classic) button:

Click Generate token and copy the token for future use, in our case, to test our pipeline locally.
The first target will add GitHub Packages as a new NuGet source:
[Parameter()]
readonly string GitHubUser = GitHubActions.Instance?.RepositoryOwner;
[Parameter()]
[Secret]
readonly string GitHubToken;
Target AddSource => _ => _
.Requires(() => GitHubUser)
.Requires(() => GitHubToken)
.Executes(() =>
{
try
{
DotNetNuGetAddSource(s => s
.SetName("github")
.SetUsername(GitHubUser)
.SetPassword(GitHubToken)
.EnableStorePasswordInClearText()
.SetSource($"https://nuget.pkg.github.com/{GitHubUser}/index.json"));
}
catch
{
Log.Information("Source already added");
}
;
});
The GitHubUser and GitHubToken are required for the target. The GitHubUser will be initialized with the GITHUB_REPOSITORY_OWNER environment variable when run as part of a GitHub Action. The --store-password-in-clear-text option was enabled because GitHub Actions does not support encryption.
To generate a version number for our package, we will use GitVersion. Execute the following command:
nuke :add-package GitVersion.Tool
We will define two targets, one to clean up the output directory and the other to create the package:
[GitVersion]
readonly GitVersion GitVersion;
AbsolutePath PackagesDirectory => RootDirectory / "packages";
Target Clean => _ => _
.Executes(() =>
{
PackagesDirectory.CreateOrCleanDirectory();
});
Target Pack => _ => _
.DependsOn(Clean)
.DependsOn(AddSource)
.Executes(() =>
{
DotNetPack(s => s
.SetProject(RootDirectory / "MyLib")
.SetOutputDirectory(PackagesDirectory)
.SetPackageProjectUrl($"https://github.com/{GitHubUser}/github-packages-nuke")
.SetVersion(GitVersion.SemVer)
.SetPackageId("MyLib")
.SetAuthors($"{GitHubUser}")
.SetDescription("MyLib nuget package")
.SetConfiguration(Configuration));
});
The Pack target will depend on both the AddSource and Clean targets. A PackagesDirectory is defined to output the packages in that location.
The final target pushes all the NuGet packages to our output folder and depends on the Pack target:
Target Push => _ => _
.DependsOn(Pack)
.Executes(() =>
{
DotNetNuGetPush(s => s
.SetTargetPath(PackagesDirectory / "*.nupkg")
.SetApiKey(GitHubToken)
.SetSource("github"));
});
Run nuke Push --GitHubUser <GITHUB_USER> --GitHubToken <NUGET_TOKEN> to view our package on GitHub:

We can generate a GitHub Action workflow from our target definitions by adding the GitHubActions attribute at the top of the Build class:
[GitHubActions(
"Push",
GitHubActionsImage.UbuntuLatest,
On = new[] { GitHubActionsTrigger.WorkflowDispatch },
InvokedTargets = new[] { nameof(Push) },
EnableGitHubToken = true,
AutoGenerate = false)]
The workflow's name will be Push, invoking the target with the same name and triggered manually using the GitHubActionsTrigger.WorkflowDispatch property. The EnableGitHubToken property instructs the generator to include an environment variable with the GITHUB_TOKEN. Run nuke --generate-configuration GitHubActions_Push --host GitHubActions to generate the Push.yml file under the path .github\workflows.
The actions/checkout@v3 is optimized by default to fetch a single commit, but GitVersion requires the entire commit history. That's why we are customizing that workflow as follows:
name: Push
on: [workflow_dispatch]
jobs:
ubuntu-latest:
name: ubuntu-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 'Cache: .nuke/temp, ~/.nuget/packages'
uses: actions/cache@v3
with:
path: |
.nuke/temp
~/.nuget/packages
key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
- name: 'Run: Push'
run: ./build.cmd Push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Another option would be to include an additional step, such as:
- name: Fetch tags
run: git fetch --prune --unshallow --tags
To push our code to the repository, run the following commands:
git add .
git commit -m "initial commit"
git push
To update our package version, create a tag by using the following command:
git tag 2.0.0
git push origin --tags
Before executing the workflow, we need to grant sufficient permissions to the GITHUB_TOKEN. In our repository, go to Settings, Actions, General, and change the Workflow permissions to Read and write permissions:

Navigate to Packages, then to Package Settings, and click on the Add Repository button to add our repository. Then, change the Role to Admin:

Now we are ready to run the workflow:

Keep in mind that we can use the PAT (as a secret in our repository) instead of the GITHUB_TOKEN, which would allow us to bypass all the configuration we did for it.
You can find the final code here. Thanks, and happy coding.