GitHub Packages: Publishing NuGet Packages using NUKE (with GitVersion and GitHub Actions)
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.
The Library
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
Install Nuke
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.
Create a Personal Access Token (PAT)
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.
Adding a new Nuget Source
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.
Create the Nuget Package
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.
Push the Nuget Package
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:
Create a GitHub Action Workflow
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.