Creating a CI/CD pipeline for a .NET library: Part 2 - Defining the build with Cake and publishing to NuGet
João Antunes
Posted on July 31, 2018
Intro
In this post I'll talk about defining the build steps using Cake and publishing the result to NuGet. From the official site: "Cake (C# Make) is a cross-platform build automation system with a C# DSL for tasks such as compiling code, copying files and folders, running unit tests, compressing files and building NuGet packages."
Why Cake
Cake is one of several options available to define what needs to be done to build a project.
The first thing that comes to mind to define the CI/CD tasks would probably be to use the tools already provided by something like AppVeyor, VSTS, Travis CI and all the other options.
The first problem with this approach is in a case like this post introduces, where one wants to build in more than one CI provider, to test multiple operating systems, and that would mean repeating the tasks in each provider (and the same applies if we want to migrate to a different provider).
One more advantage of using Cake instead of directly depending on the CI providers is that we can run the build script locally. This is not only useful to test, but also if our build is not super straightforward, we can just use the scripts while in development.
Another option would be to create a PowerShell or Shell script defining the tasks. This would be provider agnostic but probably not operating system agnostic (although PowerShell now also runs on other platforms, so it would be possible).
Cake solves these problems and adds a couple of extra bonuses:
- We write things in C#, which is probably easier for many C# developers
- There are lots of builtin and pluggable helpers
- Also, worst case scenario where there’s something missing, it’s C#, we can just code what’s missing! (and I have an example of that in this post)
Bootstrapping Cake
Before we start defining the build, we need to bootstrap Cake. This is done simply by downloading the build.ps1
and build.sh
from its resources repository. Then executing one of these files (depending on your development operating system) will download the required dependencies and create a build.cake
file, where the build will be defined.
Defining the build tasks
Now we can open our build.cake
file and start defining the required tasks. I’ll walk through my project’s build definition file (which was better organized after reading this post).
At the start of the file I’m declaring the dependencies on plugins and tools that’ll be used. There’s also a using NuGet;
statement, because like I mentioned earlier, this is C# and I wanted to do some shenanigans.
Next there’s a bunch of variable definitions that’ll be used when defining the tasks. Some are just hardcoded constants, like project and file paths, others are created using arguments that are passed to the build script.
Now let’s get into the tasks. There are seven of them: Clean
, Restore
, Build
, Test
, UploadCoverage
, Package
and Publish
. Even if a bit overkill, I’ll go through each.
The Clean
task deletes the artifacts directory and creates a new empty one to put everything that'll be generated in the next steps in there. It also runs dotnet clean
at the solution level.
The Restore
task simply calls dotnet restore
at the solution level to restore all required dependencies of the projects.
The Build
task basically runs dotnet build -c Release
at the solution level. The task also defines it’s dependent on the Clean
and Restore
tasks, so those are executed before this one.
The Test
task executes the tests in the respective project but adds some other settings pertaining to the code coverage report generation - CollectCoverage
, CoverletOutputFormat
and CoverletOutput
.
Coverlet is a tool used to generate the coverage reports in .NET Core.
After the tests are run, the generated code coverage report is moved to the artifacts directory. This is more for organization and to avoid having loose files in the development environment, as in the CI servers a new build is initiated from a completely clean environment.
The UploadCoverage
task, well… uploads the coverage report 😛
It uses a plugin to upload to Coveralls, getting as input the coverage report file path and token to authenticate itself with the service. The token is passed as an argument to the build script, being stored as an environment variable by the CI provider - I’ll talk about in the next post, on the section about AppVeyor.
The Package
task packs the library project into a pretty little NuGet package we can share on the interwebs.
The Publish
task publishes the generated NuGet package into nuget.org.
This is probably a questionable decision, as the usual approach would be to just create the package and publish afterwards if all is well. But as this is more of a didactic project than something serious and I’m kind of lazy to be publishing packages manually, I publish it directly in the build script, but only in release builds (done on the master branch).
It’s here the C# shenanigans come into play. To avoid trying to publish when the package version is the same - for instance when I’m just making adjustments to the build script and haven’t changed the project code - I’m importing the Nuget.Core
NuGet package to use the API to check if this package version is already published.
To push the package to NuGet we need an API key. We can use a general key or create one for each project, which I would say it’s the ideal for security reasons. To create one you can go over here, hit create, give it a name, the owner of the package it pushes (for instance you may want an organization instead of yourself), set some options and the packages this key has access to.
Defining build targets
The final thing in the build.cake
file is the definition of build targets, which are tasks like the others, I’m just using them to try and make clear what should be used as argument when invoking the build script.
To target a specific task we could add the argument --target NAME_OF_TASK
when invoking the build script. If nothing is passed then Default
is assumed, which does a complete build, depending on the environment/branch - the development branch goes only ‘till the UploadCoverage
task and the master branch goes all the way to the Publish
one.
Running the build locally
Now that we defined the Cake build script, we can run it locally, just as it is going to be run in the cloud CI providers.
On Windows we can do:
.\build.ps1 --currentBranch=develop --nugetApiKey=NUGET_API_KEY --coverallsToken=COVERALLS_TOKEN
On Linux/MacOS the only difference would be the use of ./build.sh
instead of .\build.ps1
.
Like I mentioned in the previous section, not passing a target, Default
is used. Alternatively we could do, for example:
.\build.ps1 --currentBranch=develop --nugetApiKey=NUGET_API_KEY --coverallsToken=COVERALLS_TOKEN --target BuildAndTest
Note: while running on MacOS, the Coveralls publication fails with a weird error:
As I only want the coverage to be published from one environment and I’m using AppVeyor as the primary one, it doesn’t annoy me too much and haven’t wasted time investigating the issue, but I’ll leave the note for future reference.
Outro
On the Cake and NuGet part we're good to go, on the next post, we'll play around with AppVeyor and Travis CI to run the build in the cloud.
- Part 1 - Intro
- Part 2 - Defining the build with Cake and publishing to NuGet (this post)
- Part 3 - Building on AppVeyor and Travis CI
- Part 4 - Code coverage on Coveralls, badges and wrap up
The accompanying code for these posts is here (tagged to be sure the code reflects what's written here in the future).
PS: originally posted here.
Posted on July 31, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.