Creating a CI/CD pipeline for a .NET library: Part 2 - Defining the build with Cake and publishing to NuGet

joaofbantunes

João Antunes

Posted on July 31, 2018

Creating a CI/CD pipeline for a .NET library: Part 2 - Defining the build with Cake and publishing to NuGet

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.

Create NuGet API Key

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:

MacOS Coverage Upload 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.

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.

💖 💪 🙅 🚩
joaofbantunes
João Antunes

Posted on July 31, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related