GitHub DevOps — Part 3: Build and Publish NuGet Packages with GitHub Actions
Series Overview
- Multiple GitHub Accounts with SSH — Configure SSH for personal and work accounts
- Semantic Versioning with GitVersion — Automated versioning using GitFlow
- GitHub Actions Workflows (this article) — Build, version, and publish NuGet packages
What We’re Building
In this article, we’ll create a complete CI/CD pipeline that:
- Builds a .NET class library on every push
- Calculates the version number automatically using GitVersion (from Part 2)
- Packs it as a NuGet package
- Publishes it to GitHub’s NuGet package registry
- Creates a GitHub Release with the package attached
The entire pipeline runs on GitHub Actions with no manual version management.
Step 1: Create the Project
1
2
3
4
5
6
7
8
mkdir Greetings.Nuget.Demo
cd Greetings.Nuget.Demo
git init --initial-branch=main
dotnet new classlib -f net10.0 -o src/Greetings.Nuget.Demo
rm src/Greetings.Nuget.Demo/Class1.cs
dotnet new gitignore
dotnet new sln
dotnet sln add $(find . -name "*.csproj")
Step 2: Add the Library Code
Create src/Greetings.Nuget.Demo/Greetings.cs:
1
2
3
4
5
6
7
8
namespace Greetings.Nuget.Demo;
public class Greetings
{
public void SayHello() => Console.WriteLine("Hello from Greetings.Nuget.Demo");
public void SayGoodMorning() => Console.WriteLine("Good morning from Greetings.Nuget.Demo");
public void SayGreetings() => Console.WriteLine("Greetings from Greetings.Nuget.Demo");
}
Step 3: Configure NuGet Package Metadata
Update src/Greetings.Nuget.Demo/Greetings.Nuget.Demo.csproj:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- NuGet metadata -->
<PackageId>Greetings.Nuget.Demo</PackageId>
<Authors>Your Name</Authors>
<Company>Your Company</Company>
<Description>A demo NuGet package for greeting messages</Description>
<RepositoryUrl>https://github.com/your-org/Greetings.Nuget.Demo.git</RepositoryUrl>
<PackageTags>Demo;NuGet;Greetings</PackageTags>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>
</Project>
Update
Authors,Company, andRepositoryUrlto match your GitHub account.
Step 4: Set Up GitVersion
Initialise GitVersion with GitFlow and continuous delivery mode (see Part 2 for details):
1
dotnet-gitversion init
Select: Getting started wizard → GitFlow → Continuous delivery mode → Save and exit.
This creates GitVersion.yml:
1
2
3
4
5
mode: ContinuousDelivery
branches: {}
ignore:
sha: []
merge-message-formats: {}
Step 5: Create the GitHub Actions Workflow
Create .github/workflows/build-and-publish.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
name: Build & Publish NuGet
on:
push:
branches: [main]
paths-ignore:
- "**/*.md"
env:
PROJECT_PATH: "src/Greetings.Nuget.Demo/"
NUGET_REGISTRY: "https://nuget.pkg.github.com/$/index.json"
permissions:
contents: write
packages: write
jobs:
build:
name: Build & Version
runs-on: ubuntu-latest
outputs:
version: $
commits: $
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history required for GitVersion
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v3
with:
versionSpec: '6.x'
- name: Calculate version
uses: gittools/actions/gitversion/execute@v3
id: gitversion
- name: Display version
run: |
echo "SemVer: $"
echo "Commits since last version: $"
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Build and pack
run: >
dotnet pack $
-p:Version='$'
-c Release
- name: Upload package artifact
uses: actions/upload-artifact@v4
with:
name: nuget-package
path: $bin/Release/*.nupkg
release:
name: Publish & Release
runs-on: ubuntu-latest
needs: build
if: needs.build.outputs.commits > 0
steps:
- name: Download package
uses: actions/download-artifact@v4
with:
name: nuget-package
path: package
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Add GitHub NuGet source
run: >
dotnet nuget add source
--username $
--password $
--store-password-in-clear-text
--name github
"$"
- name: Push to GitHub Packages
run: >
dotnet nuget push package/*.nupkg
--api-key $
--source github
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
tag: $
name: Release $
artifacts: "package/*"
token: $
Understanding the Workflow
Two Jobs: Build and Release
The workflow is split into two jobs for clarity and safety:
Build job:
- Checks out the full Git history (
fetch-depth: 0— required for GitVersion) - Installs and runs GitVersion to calculate the version
- Builds and packs the NuGet package with the calculated version
- Uploads the
.nupkgas an artifact
Release job:
- Only runs if there are new commits (
commitsSinceVersionSource > 0) - Downloads the package artifact
- Pushes it to GitHub’s NuGet package registry
- Creates a tagged GitHub Release with the package attached
Authentication with GITHUB_TOKEN
The workflow uses secrets.GITHUB_TOKEN — a token that GitHub automatically creates for each workflow run. It requires the packages: write and contents: write permissions, declared at the top of the workflow.
Troubleshooting: If the token fails to push the package, create a Personal Access Token (classic) with the
write:packagesscope. Add it as a repository secret (e.g.,NUGET_TOKEN) and replace$with$.
Version Flow
The version number flows through the pipeline:
1
Git history → GitVersion → dotnet pack -p:Version=X.Y.Z → NuGet push → GitHub Release tag
No manual version bumping anywhere.
Step 6: Commit and Push
1
2
3
git add .
git commit -m 'initial setup with CI/CD'
git push -u origin main
The workflow triggers automatically. Check the Actions tab in your repository to see it run.
Consuming the Package
Once published, other projects can consume your package from GitHub’s registry.
Add the GitHub NuGet Source
1
2
3
4
5
6
dotnet nuget add source \
--username YOUR_USERNAME \
--password YOUR_GITHUB_TOKEN \
--store-password-in-clear-text \
--name github \
"https://nuget.pkg.github.com/YOUR_ORG/index.json"
Install the Package
1
dotnet add package Greetings.Nuget.Demo
Use in a NuGet.config File
For team projects, add a NuGet.config at the solution root:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="github" value="https://nuget.pkg.github.com/YOUR_ORG/index.json" />
</packageSources>
<packageSourceCredentials>
<github>
<add key="Username" value="YOUR_USERNAME" />
<add key="ClearTextPassword" value="%GITHUB_TOKEN%" />
</github>
</packageSourceCredentials>
</configuration>
Extending the Workflow
Add Tests
Insert a test step before packing:
1
2
- name: Run tests
run: dotnet test --configuration Release --no-build
Publish to nuget.org
Add a step to push to the public NuGet registry:
1
2
3
4
5
- name: Push to nuget.org
run: >
dotnet nuget push package/*.nupkg
--api-key $
--source https://api.nuget.org/v3/index.json
Build on Pull Requests
Add PR triggers for validation without publishing:
1
2
3
4
5
on:
push:
branches: [main]
pull_request:
branches: [main]
Then gate the release job:
1
if: github.ref == 'refs/heads/main' && needs.build.outputs.commits > 0
Key Takeaways
- GitVersion + GitHub Actions = automatic versioning. No manual version bumping, ever.
fetch-depth: 0is non-negotiable. GitVersion needs the full Git history to calculate versions correctly.- Split build and release into separate jobs. Build on every push, release only when there are new commits on main.
GITHUB_TOKENhandles authentication. Declare the required permissions in the workflow file.- The version flows end-to-end. From Git history → calculated version → package version → release tag.
Series Conclusion
Over this 3-part series we’ve built a complete GitHub DevOps foundation:
- Multiple accounts — SSH aliases for seamless multi-account workflows
- Automatic versioning — GitVersion derives version numbers from your branching strategy
- CI/CD pipelines — GitHub Actions builds, versions, and publishes packages with zero manual intervention
Together, these form the backbone of a professional .NET development workflow on GitHub.