Building a NuGet Package Publishing Pipeline
Publishing a NuGet package manually works for a one-off release, but any library that's actively maintained needs an automated pipeline. This article walks through setting up a complete build-test-pack-publish workflow using GitHub Actions.
Package Metadata
Start with proper metadata in your .csproj. NuGet.org uses this to display your package:
<PropertyGroup>
<PackageId>MyCompany.MyLibrary</PackageId>
<Description>A brief description of what this library does.</Description>
<Authors>Your Name</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/mycompany/mylibrary</PackageProjectUrl>
<RepositoryUrl>https://github.com/mycompany/mylibrary</RepositoryUrl>
<PackageTags>dotnet;utilities</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<None Include="../../README.md" Pack="true" PackagePath="/" />
</ItemGroup>
The PackageReadmeFile property tells NuGet to include your README in the package and display it on NuGet.org. It's one of the most impactful things you can do for discoverability.
Local Packing and Testing
Before automating, verify your package builds correctly:
dotnet pack src/MyLibrary/MyLibrary.csproj -c Release -o ./nupkgs
Inspect the resulting .nupkg file (it's just a zip) to check that the right assemblies and metadata are included. You can also push to a local feed for testing:
dotnet nuget add source ./nupkgs --name local
dotnet add package MyCompany.MyLibrary --source local
The CI/CD Workflow
Here's a complete GitHub Actions workflow that builds, tests, packs, and publishes:
name: NuGet Publish
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build -c Release --no-restore -p:Version=${{ steps.version.outputs.VERSION }}
- name: Test
run: dotnet test -c Release --no-build
- name: Pack
run: dotnet pack -c Release --no-build -p:PackageVersion=${{ steps.version.outputs.VERSION }} -o ./nupkgs
- name: Push to NuGet
run: dotnet nuget push ./nupkgs/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
The workflow triggers on version tags (e.g., v1.2.3). It extracts the version number from the tag and passes it through to the build and pack steps. The --skip-duplicate flag prevents failures if you accidentally re-push a version.
Publishing Pre-Release Packages
For pre-release testing, publish packages from pull requests or development branches to a GitHub Packages feed:
publish-prerelease:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Build and pack
run: |
VERSION="0.0.0-preview.$(date +%Y%m%d%H%M%S)"
dotnet pack -c Release -p:PackageVersion=$VERSION -o ./nupkgs
- name: Push to GitHub Packages
run: dotnet nuget push ./nupkgs/*.nupkg --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json
This gives consumers a way to test unreleased changes without waiting for a formal release.
Signing Packages
For trusted packages, sign them before publishing:
dotnet nuget sign ./nupkgs/*.nupkg \
--certificate-path cert.pfx \
--certificate-password $CERT_PASSWORD \
--timestamper http://timestamp.digicert.com
NuGet.org validates signatures and marks signed packages in the UI. The timestamper ensures the signature remains valid after the certificate expires.
Multi-Targeting
If your library supports multiple frameworks, specify them in the project file:
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
The dotnet pack command automatically includes all target framework assemblies in the package. Consumers get the best matching version for their project.
API Key Security
Never commit your NuGet API key. Store it as a GitHub repository secret (NUGET_API_KEY). On NuGet.org, scope your API keys to specific packages with push-only permissions, and set an expiry date. Rotate keys regularly.
Summary
A NuGet publishing pipeline removes human error from the release process. Tag a commit, and the package is built, tested, packed, and published automatically. Use pre-release feeds for testing, sign your packages for trust, and always let CI handle the actual publish step.