I build a lot of .NET libraries that include simple .NET Standard libraries to fully cross-platform plugins for Xamarin and Windows that target up to 10 different platforms. When I got started building libraries and shipping them to NuGet I went down a simple path of just building everything locally on my machine and manually uploading them to NuGet. This worked great for a single library, but as soon as I took on a second... third... forth... and so on, things got out of hand and I knew I had to put some sort of CI/CD in place. I went down many paths over the years and have now settled on using Visual Studio Team Services to fully automate the build of every library I create and a full distribution pipe line that goes to MyGet then to NuGet preview, and finally NuGet public release with a click of a button. Now I can develop and let VSTS handle everything for me so I can be productive:
Productive day... Thanks @VSTS for fully automating all my NuGet packages. pic.twitter.com/pDzYUXnG5I
— James Montemagno 🙈 (@JamesMontemagno) May 10, 2018
If you look at this screenshot from VSTS it is everything that I wanted for so long in my development cycle. I push code, things are built, and automatically packages are pushed. In this post I am going to show you how I set this up for my projects and how you can too in just a few minutes.
.NET Standard and Multi-Target Projects
Before we start to setup our CI/CD for libraries I highly recommend you move your projects to SDK style projects. You can read my full guide on how to convert a PCL to .NET Standard or how to convert a Xamarin.iOS or Xamarin.Android project to SDK Style.
Everything that I develop now is based on a single .NET Standard 2.0 library or a Multi-Targeted project for plugins. I have previously written on how to convert your PCLs to .NET Standard and also have templates for Visual Studio 2017 to create Multi-Target projects for plugins.
The reason to use these types of projects is that they include full NuGet packaging including all metadata and dependencies. This also means you can fully delete the .nuspec file that always gets out of sync. It is worth your time to update any and all projects to these types of projects to simplify your life.
If you need to learn more about multi-targeting be sure to check out Oren's amazing MSBuildSdkExtras package which is the core of my templates and all of my projects.
If you are building only an iOS or Android library do not fear as you can still just convert your existing library over to an SDK Style. Take a look at my MonoDroid.Toolkit csproj on GitHub.
Build Definition in VSTS
My builds inside of VSTS are actually really simplistic and only contain 6 steps to fully build my libraries. You can use the Hosted Visual Studio 2017 machines, however I have a small Intel NUC that runs a local VSTS agent so all my NuGets are cached which reduces my build times to seconds instead of minutes.
Empty Definition
Select New and pick your source code that you would like to build. I then like to select Empty process because we can easily fill it in.
NuGet Steps
Under Phase 1 we are going to add two NUGet steps.
First is the NuGet Tool Installer task and change the Version to 4.5.1
Second will be the regular NuGet task which we will use the restore command on our solution that we are going to build. I like to pick the specific .sln or folder where it lives (no need to build other stuff).
Build Steps
I build my libraries twice! That is right twice! Once for a "beta" NuGet and once again for "public" NuGet. They are super quick and all it is really doing is changing the version flags. The reason I do this is for my release definition that will allow me to have a single release that goes from beta to public easily. We will be building the .csproj specifically using the MSBuild task:
We will add it twice so we can build the beta and public releases. Name the first Build Beta and the second Build Public. For each of them set the following:
- Project: Select the .csproj specifically
- Configuration: $(BuildConfiguration)
For MSBuild Arguments user the following for Beta:
/t:restore;build;pack /p:PackageVersion=$(Build.BuildNumber)-beta /p:PackageOutputPath=$(build.artifactstagingdirectory)/beta /p:AssemblyFileVersion=$(Build.BuildNumber)
For Public use:
/t:restore;build;pack /p:PackageVersion=$(Build.BuildNumber) /p:PackageOutputPath=$(build.artifactstagingdirectory)/public /p:AssemblyFileVersion=$(Build.BuildNumber)
This will ensure that the build numbers are configured correctly and copy the artifacts to different folders.
Optionally, you may have some additional unit tests or device runners that may need to be run here.
Copy and Publish
The next thing to add is the Copy Files task to stage the NuGet packages for release.
Set these properties:
- Source Folder: $(build.sourcesdirectory)
- Contents:
**\bin\$(BuildConfiguration)\**\*.nupkg
- Target Folder: $(build.artifactstagingdirectory)
Finally, add the Publish Build Artifacts task and set the following:
- Path to publish: $(build.artifactstagingdirectory)
- Artifact name: drop
- Artifact public location: Visual Studio Team Services/TFS
This will pull only .nupkg files that we will distribute into a folder that release can pick up.
All in all the definition should look like:
Configure Version Number
Under the Options tab in the build definition I like to format the Build number format such as: 4.5.0$(rev:.r)
This means that the next version would be: 4.5.0.1 and then 4.5.0.2.
Now if you change this to: 5.0.0$(rev:.r) it will reset the revision so it will be 5.0.0.1.
Other things!
You will probably want to setup *Triggers so CI and PR validation are configured. This means that every time master is pushed to a package is generated.
Release Definition in VSTS
Now we have our full build complete it is time to setup our release. Our goal for me was to automatically have every build of master go to MyGet and then manually approve the next release to go to NuGet (beta) and finally another approval to go to NuGet with the public non-beta package.
Head over to the Releases section and create a new release definition with an Empty process. Think of Environments as the stages of release. We can create three of them to define each release of the artifacts. Inside of each environment are a sequence of tasks, just like our build definition, that will be run. For my libraries it is really simple as each has just one task. However, before the NuGet beta and production environments I turn on pre-deployment approval steps.
Push to MyGet
In our first environment we will want to push to MyGet. Add the NuGet task with the following settings:
- Command: push
- Path to NuGet package:
$(System.DefaultWorkingDirectory)/**/drop/beta/*.nupkg
- Target Feed: External NuGet server
For this step you will want to configure the feed information from MyGet:
Push NuGet Beta
For this step you can select Clone or if you already create the new environment simply add the NuGet task again. The settings will be the same as above:
- Command: push
- Path to NuGet package:
$(System.DefaultWorkingDirectory)/**/drop/beta/*.nupkg
- Target Feed: External NuGet server
However, we will now want to select or configure NuGet as our destination:
Head back to Pipeline and for this environment we don't want it to automatically trigger, we want to approve this release. You will see a lightning bolt and a person shape on the left side of the environment to configure pre-deployment conditions. I turn on pre-deployment approvals and select myself and turn off the policies since I am a solo developer:
There is all sorts of rad stuff you can do here on pre and post-deployment so you should totally look into that for larger projects.
Push NuGet Production
Add or clone another environment for after the Beta environment. This is going to be almost the same as the previous step but we are going to change our Path to NuGet package:
- Command: push
- Path to NuGet package:
$(System.DefaultWorkingDirectory)/**/drop/public/*.nupkg
- Target Feed: External NuGet server
Notice I am using public here :)
Add Artifact
Finally, click the big Add artifact button on the left side and select the build definition that you created earlier. Also tap on that little lightning bolt and turn on the Continuous deployment trigger so this flow automatically happens!
CI/CD Pro!
You did it! You are totally a Continuous integration and delivery pro!! Enjoy a seamless deployment of every push to master to MyGet and then approval steps to push to NuGet for any and all of your .NET Libraries. <3 Feel free to ask questions below.