Implementing a Build and Release Pipeline for Open Source NuGet Packages using Azure Pipelines

Implementing a Build and Release Pipeline for Open Source NuGet Packages using Azure Pipelines

Recently, I saw an article called NuGet Package Publication Checklist from Steve “ardalis” Smith about how he releases his open source NuGet package GuardClauses. It’s an awesome package that one should definitely try out in their projects. But how the NuGet package is released was mostly a manual process that can be improved a lot.

I reached out to him on GitHub and offered to help out with improving the release process using Azure Pipelines since Azure Pipelines and GitHub integration is super awesome and it makes it super easy to get the job done. And with Steve’s go ahead I started working on the pipeline. He had already integrated his project with Azure Pipelines with a basic YAML build, so what I had to do was improve it. In this article I will describe step by step, what I did to improve this release process and some tips you can use as well.

The Requirement

  • The pipeline needs to build and run the unit tests and make sure they were passing. That is obvious
  • The Pull Requests created against the master branch must have a passing CI build and it is a required check to merge and complete the PR.
  • Release artifacts should not be generated when the build is triggered from a Pull Request. It should only Build and run the unit tests.
  • A release needs to be created and deployed to NuGet.org only when Steve updates the version of the NuGet package by modifying the .csproj file and pushes tag to GitHub with the version of the new package.

The Implementation

The implementation is separated in to 2 parts, the build pipeline and the release pipeline. The build pipeline is defined using YAML and included alongside the source code, unfortunately there is no support for YAML release pipelines so it’s created using the designer experience on Azure DevOps project.

Building the NuGet Package

The CI pipeline includes 5 tasks, 3 of the are .Net CLI Tasks that Restores the NuGet packages, build and runs the unit tests, the Copy Files Task will copy the generated .nupkg to the Artifacts Staging directory and the final Public Build Artifacts Task will publish the build artifacts.

Since we will be sharing the same pipeline in the PR build as well as the final release, it’s inefficient to run the Copy File and Publish Artifacts tasks in a PR. So, we needed to disable those 2 tasks. This is really simple to do using Pipeline Task conditions, the following expression will skip the Copy Files and Publish Build Artifacts tasks if the build is triggered from a PR.

and(succeeded(), ne(variables\['Build.Reason'\], 'PullRequest'))

Next, the matter of triggering the pipeline. We defined master branch as the PR trigger, and also defined master and ref/tags/* so that the pipeline is triggered by creating a tag on the master branch. This will be the trigger that will release the package to NuGet.org.

 That is basically it for the build pipeline and the following azure-pipelines.yml file contains the entire pipeline definition.

trigger:
  branches:
      include:
      - master
      - refs/tags/*
pr:
- master

pool:
  vmImage: 'VS2017-Win2016'

variables:
  BuildConfiguration: 'Release'

steps:
- task: DotNetCoreCLI@2
  displayName: Restore
  inputs:
    command: restore
    projects: '**/*.csproj'

- task: DotNetCoreCLI@2
  displayName: Build
  inputs:
    projects: '**/*.csproj'
    arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'

- task: DotNetCoreCLI@2
  displayName: Test
  inputs:
    command: test
    projects: '**/*[Tt]ests/*.csproj'
    arguments: '--configuration $(BuildConfiguration)'

- task: CopyFiles@2
  displayName: 'Copy Files'
  inputs:
    SourceFolder: '$(Build.ArtifactStagingDirectory)'
    Contents: '**\*.nupkg'
    TargetFolder: '$(Build.ArtifactStagingDirectory)\Package'
  condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact'
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\Package'
  condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))

Releasing the NuGet Package

As of writing this, Azure DevOps does not support YAML release definitions, so it has to be created using the Visual Designer on Azure Pipelines.

First, we need to create a Service Connection to push packages to Nuget.org. we can generate an API key on NuGet.org and create the Service Connection. Steve provided his API key, and we created the Service Connection to NuGet.

image 01

Next, we needed single environment in the pipeline that includes a single pipeline task that pushes the package to NuGet. We used the NuGet task and used the push action and provided the .nupkg file that comes with the latest build and used the previously created Service Connection to push the package.

Next the matter of triggering the release. Our requirement was to only release a package when the master branch is tagged with the version. So, we needed to add a path filter to the release trigger. And the same path refs/tags/* filter can be used here and once that is added, it was completed.

image 02

And that is it, we are done. The manual steps took to publish the package is now replaced with a fully automated build and release pipeline with tight PR validation. Awesome.

What We Can Improve?

Still there are some improvements that we can make to the pipeline.

  • At the moment, to release a package Steve needs to update the package version and create a tag and push to GitHub in order to trigger the release. Pushing the tag also serves the purpose of creating release on GitHub. We can change this to automatically version the package and the release the package to NuGet, create the tag and release on GitHub automatically as part of the release pipeline.

A Tip for Creating the Pipeline

If the YAML pipeline definition is intimidating for you or if you are too lazy to type all these in (like me) you can do the following.

I usually create the entire pipeline using the Visual Designer, and then execute and test the implementation well and once I’m happy with how the pipeline turned out, I use the View YAML button on the Visual Designer to get the YAML for the pipeline. I can get the YAML for single task or for the entire pipeline.

Then I paste the YAML in to the azure-pipelines.yml file and do the necessary tweaks go get it finished and I’m usually done in half the time. It’s awesome.

Conclusion

I hope you the reader got an idea of how easy it is to create CI/CD for your Open Source projects on GitHub with the Azure Pipelines integration with GitHub. The free offering for GitHub is outstanding and you should definitely use these tools for your Open Source projects.

You Might Also Like
Comments