Off the back of two great articles about Continuous Integrations and Delivery (CI/CD) by Oren Novotny (OSS Build and Release with VSTS) and James Montemagno (Continuous Integration & Delivery for Xamarin & .NET Libraries with VSTS) I came up with my own CI/CD pipeline that fits my needs for some components I publish as nuget packages.
As it borrows from both approaches but does some things slightly differently I thought it would be useful to share.
Firstly, I would recommend reading James’ and Orens’ articles. You may want to implement them wholesale or borrow from them like me to come up with your own solution.
Use SDK style projects and say goodbye to nuspec files
As suggested by James, converting projects into the new SDK csproj format with multi-targeting where necessary makes this process so much easier. It reduces the number of projects you have to deal with and removes the pain of having to keep dependencies in projects and nuspec files in sync, as you can remove any need for nuspec files.
James has another great article on this here: Converting Xamarin libraries to sdk style multi targeted projects
The main points of my branching strategy are as follows:
- I use the well know gitflow branching model where branches are created off develop for feature development
- There are 2 main branches; develop and master
- PR’s builds simply build and test the code
- develop has a CI build trigger that creates preview nuget packages
- master has a CI build trigger that creates release nuget packages
As suggested by Oren, I use Nerdbank.GitVersioining. By including a version.json file with the source code, GitVersioning will version my assemblies, nuget packages and builds. Here’s an example of a version.json file I use:
In this case my major.minor is 3.1. I’ve indicated the public release comes out of master. This makes GitVersioning remove the commit id from the version number.
I’ve also enabled the build number in the cloud build so that VSTS builds reflect that correct build number for complete traceability.
I flip-flopped between James’ suggestion of building both preview and release packages in a single build script and Orens’ suggestion of releasing out of master.
Building twice in a single build gave me an issue with GitVersioning as the build version number would reflect the last build task that completed meaning you’d see the build number flip from preview format (with the commit id) to release format (without the commit id) and that’s not what I wanted. I also just felt that building twice was kind of unnecessary.
I liked the idea of using master as the trigger for releases, that just makes sense to me, but I didn’t want to have more than one build script to maintain, i.e. one for develop and one for master.
Fortunately VSTS task groups and task conditions came to the rescue and I was able to use the same build Task Group for all my build definitions:
Task 1: Nuget restore
The first task (group) simply restores the nuget packages for the solution:
Tasks 2, 3, 4 and 5: Build
As you can see above I have several build tasks. These run based on conditions I’ve specified on the tasks. The first two are used for continuous integration, the third for pull requests and the last is if a variable called $(Forced) is set to true.
Here’s the condition on the first build, it runs if reason for the build is CI and the source branch is develop:
The second build task runs if a CI build is run and the source branch is master. The difference being that the master branch creates release packages, whereas the develop branch creates preview packages.
The third build task runs if the build is for a PR: and(succeeded(), eq(variables[‘Build.Reason’], ‘PullRequest’))
The fourth is run if a variable called $(Forced) is set to true. This allows us to use the build for any else that isn’t a PR or CI build.
Using conditions I can have a single build definition that I can use to build preview releases out of develop and public releases out of master.
You can read about VSTS Task conditions here: VSTS Task Conditions.
I use similar arguments to James solution to do my build:
In the CI (develop) and forced builds the build and pack targets are specified, the PackageOutputPath is set to be a folder called preview or forced in the $(build.artifactstagingdirectory), and set the PublicRelease argument to false for the develop and forced builds. This tells GitVersioning to add the commit id to the version number.
The other builds use combinations of these arguments with release builds going into a folder called release and PublicRelease set to true to strip off the commit id from the version.
I run tests using the dotnet test task. This is very straightforward. One tip: I just want it to run the tests, not build the code again so I run the tests with the –no-build argument:
The publish artifacts task simply publishes the artifacts from the $(build.artifactstagingdirectory) to VSTS:
After this I end up with either preview or release package artefacts in VSTS ready for the release pipeline.
Continuous Integration Trigger
The next thing to do is enable a trigger to perform builds whenever the code in develop or master changes. This in turn will trigger the release mechanism described below. To do this go to the Triggers tab of your build definition and switch on Continuous Integration for the develop and master branches:
Now, whenever code changes in these branches a build will run.
The flow of the CI/CD pipeline is as follows:
Pre-release builds (from develop)
- A developer makes a pull request to bring code into the develop branch
- The CI build validation on the develop branch creates pre-release versions of the nuget package(s) which are published as build artefacts.
- The nuget package is versioned in the format Major.Minor.CommitHeight-CommitId
- The release pipeline triggers due to the build of the develop branch
- The release pipeline publishes the preview nuget packages to an internal nuget feed
Release builds (from master)
- A developer makes pull request to bring code into the master branch (usually this will be from develop)
- The release build validation on the master branch creates release versions of the nuget packages
- The nuget packages are versioned as Major.Minior-CommitHeight (CommitId is removed)
- The release pipeline triggers due to changes to the master branch.
- The release pipeline publishes the release nuget packages to a release nuget feed.
In VSTS this looks like this:
The continuous deployment trigger fires whenever a new build is available from the develop or master branches builds:
The develop->preview environment triggers when a build is done in the develop branch whereas the master->artifactory environment triggers when a build is done in the master branch.
Each environment only has one task, to publish the artefacts (nuget packages) to a feed:
- Develop->preview publishes the preview packages to an internal VSTS feed.
- Master->artifactory publishes the release packages to our release artifactory feed
Note the publish task takes the packages from the preview folder. For release this folder is called release, i.e. they match the folders the build used when it created the packages.
That’s it. Thanks to James’ and Orens’ articles I was able to use some of their suggestions and create a CI/CD pipeline with my own twist to meet my needs. This really shows off the flexibility of using VSTS for (among other things) builds and releases.