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."
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)
Before we start defining the build, we need to bootstrap Cake. This is done simply by downloading the
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.
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:
Publish. Even if a bit overkill, I’ll go through each.
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.
Restore task simply calls
dotnet restore at the solution level to restore all required dependencies of the projects.
Build task basically runs
dotnet build -c Release at the solution level. The task also defines it’s dependent on the
Restore tasks, so those are executed before this one.
Test task executes the tests in the respective project but adds some other settings pertaining to the code coverage report generation -
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.
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.
Package task packs the library project into a pretty little NuGet package we can share on the interwebs.
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.
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
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
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.
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.
Level up every day