Building and Publishing a NuGet Package with Azure DevOps Pipelines

May 29, 2025    TFS/VSTS/AzureDevOps DevOps NuGet CI/CD YAML

Building and Publishing a NuGet Package with Azure DevOps Pipelines

A MS Copilot generated image of ADO and Nuget with conveyor belts

Microsoft has good documentation for this and this article was helpful , but I still had trials finding the correct way to version the package, so I’m sharing it here (for you and me in the future).

This package had an existing .Net Framework version. I added a .Net 8 version. I fell into the trap of going for the complicated approach first. I attempted to have a condition to switch between .Net and the Framework templates. It would have been easier to start with 2 different .yml files and setup path filters in the ADO UI and I ended up here.

Side note: I have decided to not use the term “Core” anymore. “Core” was used for the new version of .Net up until .Net 5. The History of .Net explains it well.

Iterations

I went through a lot of iterations (which is typical for my pipeline building experiences of the past) before I finally got the package to publish. 31 builds on the .Net pipeline to be exact.

Approach

First create the initial YAML pipeline(s) in the main branch.

Then in the ADO UI > Edit the Pipeline. In the YAML tab, you can set the YAML path and filename if you decide to change the default. In Variables, set those that can be overridden in the yml and in Triggers add Path filters as needed.

You can iterate in the branch before a PR, but choosing the run pipeline > select the branch

At Some Point Create a .nuspec file

Setup the Artifact Feed permissions for Feed Publisher (Contributor) to publish a Nuget Package. Also see . I had to read both of these and with trial and error finally got it working

  • “To publish your packages to a feed using Azure Pipelines, make sure that both the Project Collection Build Service and your project’s Build Service identities are granted the Feed Publisher (Contributor) role assigned in your feed settings.”
  • “The project-level build identity is named [Project name] Build Service ([Organization name]), for example FabrikamFiber Build Service (codesharing-demo).”
  • I still was getting permission denied, this SO answer had “For Yaml Pipeline, you can navigate to Project Settings -> Settings and check the option: Limit job authorization scope to current project for non-release pipelines. If the option is enabled, it will use Project Level Build Service account, or it will use Organization Level account.”, but it didn’t help me.
  • I had created a Project level feed instead of an Organization level feed. Use the Project Level one!
  • I was using the script: | nuget.exe push and still getting “permission denied”
  • after switching to windows-latest, and the @NugetCommand@1 push command it worked

Then setup branch policies to protect your main branch and require the pipeline to build and test your code before completing. Use the IsMain variable as a condition to only publish when merged into main.

.Net Yaml

# Version is set via byBuildNumber, set tne name in the yml to to 1.0.$(rev:.r), then use versioningScheme: byBuildNumber
# reference" https://stackoverflow.com/a/57553743/54288, https://learn.microsoft.com/en-us/azure/devops/pipelines/artifacts/nuget?view=azure-devops&tabs=yaml#package-versioning
name: 1.0.$(rev:r)

trigger:
  - main

pool:
  # I wanted to use ubuntu-latest, but couldn't get the nuget push permissions to work
  vmImage: windows-latest

variables: 
  Name: YourNugetProject.csproj
  Project: "folder/YourNugetProject.csproj"
  #'https://pkgs.dev.azure.com/YourOrg/YourRepo/_packaging/YourFeed/nuget/v3/index.json'
  # GUIDs copied from pipeline editor > add task > Nuget Command output
  # using the url I got a "##[warning]Could not create provenance session: %s
    ##[warning]{"$id":"1","innerException":null,"message":"A potentially dangerous Request.Path value was detected from the client (:).","typeName":"System.Web.HttpException, System.Web","typeKey":"HttpException","errorCode":0,"eventId":0}"
  PublishVstsFeed: "Guid1/Guid2"
  # set in the UI Variables: BuildConfiguration: 'Release'
  IsMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]

stages:
  - stage: BuildStage
    jobs:
      - job: Build
        steps:
          - task: UseDotNet@2
            displayName: "Use .NET Core sdk"
            inputs:
              packageType: sdk
              version: 8.x
              performMultiLevelLookup: true

          - task: DotNetCoreCLI@2
            displayName: "NuGet Restore"
            inputs:
              command: restore
              projects: "${{ variables.Project }}"

          - task: DotNetCoreCLI@2
            displayName: Build
            inputs:
              command: build
              projects: "${{ variables.Project }}"
              arguments: "--configuration $(BuildConfiguration)"

          # https://learn.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core?view=azure-devops&tabs=yaml-editor#collect-code-coverage-metrics-with-coverlet
          - task: DotNetCoreCLI@2
            displayName: 'dotnet test'
            inputs:
              command: 'test'
              projects: '**/*Tests/*.csproj'
              arguments: '--configuration $(BuildConfiguration) --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura'
              publishTestResults: true

          - task: PublishCodeCoverageResults@2
            displayName: 'Publish code coverage report'
            inputs:
              codeCoverageTool: 'Cobertura'
              summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'

          # versioned based on the name: set above
          - task: DotNetCoreCLI@2
            displayName: "Create .Net NuGet Package"
            inputs:
              command: pack
              packagesToPack: "${{ variables.Project }}"
              packDirectory: "$(Build.ArtifactStagingDirectory)/packages"
              arguments: "--configuration $(BuildConfiguration)"
              nobuild: true
              versioningScheme: byBuildNumber
    
    - job: Publish
      dependsOn: Build
      condition: eq(variables.IsMain, true)
        steps:
          # https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/nuget-command-v2?view=azure-pipelines
          - checkout: none
          - task: DownloadBuildArtifacts@0
            displayName: 'Download Build Artifacts'
            inputs:
              artifactName: Package
              downloadPath: $(Pipeline.Workspace)
          - task: NuGetToolInstaller@1                            
            displayName: 'NuGet Tool Installer'

          # NuGetCommand@2 fails with [error]The task has failed because you are using Ubuntu 24.04 or later without mono installed. See https://aka.ms/nuget-task-mono for more information. So I followed https://learn.microsoft.com/en-us/azure/devops/pipelines/artifacts/nuget?view=azure-devops&tabs=yaml#publish-nuget-packages-to-a-feed-in-the-same-organization
          ## https://stackoverflow.com/questions/79572994/azuredevops-nugetcommand2-push-to-dotnetcorecli2-custom-nuget-push-with-vsts
          # we could have used NuGetCommand@2 if using the windows-latest image

          - task: NuGetAuthenticate@1
            displayName: "NuGet Authenticate"

          ### if you got permission denied, see the note from https://learn.microsoft.com/en-us/azure/devops/pipelines/artifacts/nuget?view=azure-devops&tabs=yaml#publish-nuget-packages-to-a-feed-in-the-same-organization
          ### "To publish your packages to a feed using Azure Pipelines, make sure that both the Project Collection Build Service and your project's Build Service identities are granted the Feed Publisher (Contributor) role assigned in your feed settings."
          ### You can find the name by looking at the output from the Nuget Authenticate step above
          # For ubuntu-latest, but I couldn't get beyond "Permission Denied"
          # did I use nuget.exe incorrectly?
          # - script: |
          #     nuget.exe push -Source ${{ variables.PublishVstsFeed }} -ApiKey az $(Build.ArtifactStagingDirectory)\packages\${{ variables.Name }}.$(Build.BuildNumber)*.nupkg
          #   displayName: 'Push Nuget Package'

          # for Windows-latest          
          - task: NuGetCommand@2
            inputs:
              command: 'push'
              packagesToPush: "$(Pipeline.Workspace)/Package/packages/*.nupkg"
              nuGetFeedType: 'internal'
              publishVstsFeed: ${{ variables.PublishVstsFeed }}


Watch the Story for Good News
I gladly accept BTC Lightning Network tips at strike.me/aligned

Please consider using Brave and adding me to your BAT payment ledger. Then you won't have to see ads! (when I get to $100 in Google Ads for a payout (I'm at $97.66!), I pledge to turn off ads)

Use Brave

Also check out my Resources Page for referrals that would help me.


Swan logo
Use Swan Bitcoin to onramp with low fees and automatic daily cost averaging and get $10 in BTC when you sign up.