Last Updated on 2026-06-14 by James Croft
Automating your .NET library’s build and publish pipeline removes human error from the release process and lets you ship with confidence. Azure Pipelines gives you a multi-stage YAML pipeline that builds, tests, packs, and publishes your NuGet packages with native integration into Azure DevOps, Azure Artifacts, and NuGet.org.
In this guide, I’ll walk through a complete multi-stage pipeline for publishing NuGet packages with Azure Pipelines. If you’re using GitHub instead, check out my companion guide on publishing NuGet packages with GitHub Actions.
What you’ll learn
- Create a NuGet.org API key for publishing
- Configure secrets securely in Azure Pipelines
- Set up a multi-stage YAML pipeline with separate Build and Publish stages
- Configure Source Link, symbol packages, and modern versioning
- Publish to NuGet.org and Azure Artifacts
Creating a NuGet.org API key
Before publishing to NuGet.org, you’ll need an API key. Navigate to your NuGet.org account and generate one:
- Key Name: something like
azure-pipelines-publish - Expires In: up to 365 days (NuGet will remind you before expiry)
- Scopes: select Push
- Glob Pattern: scope to your package name pattern for security (e.g.,
MyOrg.*)

Copy the key immediately, you won’t see it again.
Configure your API key in Azure Pipelines
You have two options for storing your NuGet.org credentials in Azure DevOps:
Option 1: Service Connection (Recommended)
Navigate to Project Settings → Service connections → New service connection → NuGet. Create a connection named NuGetOrg with your API key. This is the cleanest approach, the pipeline references the connection by name, and credentials are managed centrally.
Option 2: Variable Group with Key Vault
Navigate to Pipelines → Library and create a variable group. Add your API key as a secret variable (use the padlock icon to secure it). For even better security, link the variable group to an Azure Key Vault to manage secrets there.
Prepare your project for packaging
Configure your .csproj with NuGet metadata, Source Link, and symbol packages:
<PropertyGroup>
<!-- Your Package Setup -->
<!-- Package metadata -->
<PackageId>YourLibrary</PackageId>
<Authors>Your Name</Authors>
<Description>A clear description of what your library does</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/you/your-library</PackageProjectUrl>
<RepositoryUrl>https://github.com/you/your-library</RepositoryUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<!-- Source Link — lets consumers step into your source while debugging -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<!-- Deterministic builds for CI -->
<ContinuousIntegrationBuild Condition="'$(TF_BUILD)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.203" PrivateAssets="All" />
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
A few things worth highlighting:
- Source Link enables consumers to step into your library’s source code while debugging. It costs you nothing to set up and dramatically improves the debugging experience for your users.
- Symbol packages (
.snupkg) publish alongside your main package to NuGet.org’s symbol server, no separate hosting needed. - Deterministic builds ensure the same source always produces the same binary, which matters for supply chain security and reproducibility.
- Note that Azure Pipelines sets
TF_BUILD=trueautomatically, so theContinuousIntegrationBuildcondition works out of the box.
- Note that Azure Pipelines sets
Set up your multi-stage pipeline
Here’s the complete Azure Pipelines YAML I use. It separates CI (Build + Test) from CD (Pack + Publish) into distinct stages, with the Publish stage only running on release tags:
How this pipeline works
Build stage (every push and PR):
- Installs .NET 10 on the build agent
- Restores, builds, and runs tests
- If any step fails, the pipeline stops and PRs are blocked
Publish stage (only on version tags):
- The
conditionensures this stage only runs when av*tag is pushed - The version number is extracted from the tag name, e.g.
v2.1.0becomes2.1.0 dotnet packcreates the NuGet packages with the correct version- Packages are published as pipeline artifacts (downloadable later for auditing)
NuGetCommand@2pushes to NuGet.org using the service connection
Key differences from older approaches
If you’ve been using an older version of this pipeline, here are the significant changes:
- Multi-stage pipeline: separating Build and Publish into stages gives clear visual separation in Azure DevOps and lets you add approval gates between them
- Tag-based versioning: tags are simpler, Git-standard, and don’t require maintaining custom scripts
- Service connections: replaces raw API keys stored in variable groups. Service connections are more secure, centrally managed, and auditable
ubuntu-latest: most .NET builds don’t need Windows. Ubuntu agents are faster to provision and cheaper on hosted pools
Versioning your packages
The pipeline extracts the version from git tags. When you’re ready to release:
git tag v2.1.0
git push origin v2.1.0
For prereleases, use semantic versioning:
git tag v2.1.0-preview.1
git push origin v2.1.0-preview.1
Modern versioning alternatives
If you prefer automated versioning, the same tools work with Azure Pipelines:
- MinVer – minimal, convention-based versioning from git tags
- GitVersion – automatic semantic versioning from git history
- Nerdbank.GitVersioning – version stamping from a
version.jsonfile
Publishing to Azure Artifacts
If you maintain internal packages, Azure Artifacts provides private NuGet feeds within your Azure DevOps organization. Add this step to publish to an internal feed:
- task: NuGetCommand@2
displayName: 'Push to Azure Artifacts'
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg'
publishVstsFeed: 'your-project/your-feed'
allowPackageConflicts: true
No API keys needed, Azure Pipelines authenticates to Azure Artifacts automatically using the build service identity. The allowPackageConflicts option prevents failures when re-publishing an existing version.
You can publish to both NuGet.org and Azure Artifacts in the same pipeline, just add both push steps!
Common mistakes to avoid
Using windows-latest when ubuntu-latest will do. Unless your library specifically needs Windows APIs or tools during the build, Ubuntu agents provision faster and are cheaper on Microsoft-hosted pools.
Storing API keys in plain variable groups. Use service connections for NuGet.org, or link your variable group to Azure Key Vault.
Not publishing pipeline artifacts. Even though packages go to NuGet.org, publishing them as pipeline artifacts gives you a downloadable record of exactly what was shipped. Useful for audits and debugging.
Skipping the pr: trigger. Use them to ensure every PR builds and tests before merge.
Summary
With this multi-stage pipeline, a single git tag produces a built, tested, packed, and published NuGet package with Source Link for debugging, symbol packages for the symbol server, and pipeline artifacts for auditing.
Here’s the release workflow:
- Merge your changes to
main - Create and push a version tag:
git tag v2.1.0 && git push origin v2.1.0 - The Build stage runs and validates
- The Publish stage packs and pushes to NuGet.org
- Your package appears on NuGet.org within minutes
For the GitHub equivalent, check out my guide on publishing NuGet packages with GitHub Actions. And if your library targets multiple frameworks, read my guide on multi-targeting .NET libraries for maximum compatibility.
This article was originally published in January 2021 and has been updated to reflect multi-stage YAML pipelines, .NET 10, service connections, modern versioning, and Source Link best practices.
Found this article useful?
Thank you for taking the time to read this article. I’m committing to sharing more of my knowledge through my blog and open-source projects! You’ll also catch me in casual conversation on Bluesky and LinkedIn too.
If you enjoy what you see, please consider subscribing to get a notification when new articles go live!
Discover more from James Croft
Subscribe to get the latest posts sent to your email.






