16 Jun

Quick CI for your app

by Filip Paluch Cake AppVeyor CI

Have you ever had a problem with expanding project? Number of packages and dependencies have increased and slowly you started to get lost in your own project? I will introduce you to continuous integration, and your life will become easier.

What is Cake and AppVeyor?

Cake (C# Make) is a cross platform build automation system with a C# DSL to do things like compiling code, copy files/folders, running unit tests, compress files and build NuGet packages. Cake is built on top of the Roslyn and Mono compiler which enables you to write your build scripts in C#. The source code for Cake is hosted on GitHub and includes everything needed to build it yourself.

Source

AppVeyor - Continuous Delivery service for Windows.

Initialization

Ok, now I will show you how to create simple CI for your C# project. First, we should create C# project and next define where Cake files will be located. I prefer approach, where solution is located in Source folder and the Cake files are inside Build folder.
Your thing is how you to arrange your files.

Ok, now we can download Cake, there are two possibilities to do this:

  • Clone or download repository and copy build.ps1 file to your solution.
  • Install the Cake bootstrapper. The bootstrapper is used to download Cake and the tools required by the build script. For Windows:
    Invoke-WebRequest http://cakebuild.net/download/bootstrapper/windows -OutFile build.ps1

    I choose second possibility, inside PowerShell Interactive Window in Visual Studio I executed above command and Build.ps1 file has been added to my Build folder.

Build.ps1 - This is a bootstrapper powershell script that ensures you have Cake and required dependencies installed. The bootstrapper script is also responsible for invoking Cake. This file is optional, and not a hard requirement. If you would prefer not to use PowerShell you can invoke Cake directly from the command line, once you have downloaded and extracted it.

The last thing to add is build.cake file. This is the most important file because inside it we will implement our logic. Please add build.cake file to folder where you have added build.ps1 file.

Now you can run the build script. Please open up a Powershell prompt and execute bootstrapper script:

PS> .\build.ps1

As you can see, script detected that you do not have Cake and automatically download it from NuGet.

Congratulations, you have run you first Cake script!

Tasks

Unit of work in Cake is represented by Tasks.

Task("MyFirstTask")
  .Does(() =>
{
    Information("Hello World!");
});

RunTarget("MyFirstTask");

Ok, now you can past code above to build.cake file and run it. You should see Hello World on your console.

Dependencies

Each task can define dependencies, so you can freely define your work flow. Imagine a situation when you want to build solution in the first step, and next create nuget package.

Task("Build")
    .Does(() =>
{
    //Build solution
});

Task("CreateNugetPackage")
    .IsDependentOn("Build")
    .Does(() =>
{
    //Create nuget package
});

RunTarget("CreateNugetPackage");

Now you know the most basic Cake functionality. Base on it, you can write a very simple CI for you application. More advanced add-ins I will present at the end of the article .

Ok, let's do this!

Before we begin, we have to think about what we want to achieve. The simplest work flow for your CI might look like:

  • Restore nuget packages
  • Clean solution
  • Build solution in release mode
  • Run tests (unit or integration)
  • Create nuget packages
  • Push packages to nuget.org.

Ok, let's start with the first task.

Inside cake file add :

var task = Argument("target", "PushNuGetPackage");

Task("RestoreNuGetPackages")
    .Does(() =>
{
    Information("Restoring nuget packages for solution")
    NuGetRestore("../Source/CakeTutorial.sln");
});

NuGetRestore function restores NuGet packages for the specified target. Of course it is possible to download all solutions located in the specified folder and then perform NuGetRestore function for each of them.

var solutions = GetFiles("./**/*.sln");
foreach(var solution in solutions)
{
    Information("Restoring {0}", solution);
    NuGetRestore(solution);
}

Source

In the next step we would like to clean solution.


Task("Clean")
    .IsDependentOn("RestoreNuGetPackages")
    .Does(() =>
{
    CleanDirectories("../Source/**/bin");
    CleanDirectories("../Source/**/obj");
});

Function CleanDirectories cleans the directories matching the specified pattern. About all "directory operations" functions you can read here: Directory operations

Third step, build solution in release mode.


Task("Build")
    .IsDependentOn("Clean")
    .Does(() =>
{
    MSBuild("../Source/CakeTutorial.sln", settings => settings.SetConfiguration("Release"));
});

MSBuild function builds the specified solution. In settings you can specify mode. More information and examples you can find here MSBuild

Next step, run unit tests.


Task("RunUnitTests")
    .IsDependentOn("Build")
    .Does(() =>
{
    NUnit("../Source/CakeTutorial.Tests/bin/Release/CakeTutorial.Tests.dll", new NUnitSettings {
        ToolPath = "../Source/packages/NUnit.ConsoleRunner.3.2.1/tools/nunit3-console.exe"
    });
});

NUnit function runs all unit tests located in the assmeblies matching the specified pattern. Cake supports also XUnit and VSTests.

Create nuget packages

There are two posibilities to create nuget package. You can:

  • Create NuGetPackSettings class in task and fill all required fields.
  • Load package settings from nuspec file. When you choose this way, you can replace some of the properties in nuspec, such as version.

Task("CreateNuGetPackage")
    .IsDependentOn("RunUnitTests")
    .Does(() =>
{
    new NuGetPackSettings {
            Id                      = "CakeTutorial",
            Version                 = "1.0.1",
            Title                   = "The tile of the package",
            Authors                 = new[] {"Author"},
            Owners                  = new[] {"Owner"},
            Description             = "The description of the package",
            Summary                 = "Excellent summary of what the package does",
            ProjectUrl              = new Uri("https://github.com/SomeUser/Test/"),
            IconUrl                 = new Uri("http://cdn.rawgit.com/SomeUser/Test/master/icons/test.png"),
            LicenseUrl              = new Uri("https://github.com/SomeUser/Test/blob/master/LICENSE.md"),
            Copyright               = "Some company",
            ReleaseNotes            = new [] {"ReleaseNotes"},
            Tags                    = new [] {"Tags"},
            RequireLicenseAcceptance= false,
            Symbols                 = false,
            NoPackageAnalysis       = true,
            Dependencies            = new []{ new NuSpecDependency{
                                     Id              = "SomeLibrary",
                                     Version         = "1.0.0.1"
                                    }},
            Files                   = new [] { new NuSpecContent { 
                                                Source = "bin/Test.dll", Target = "bin"},
                                             },
            BasePath                = "./src/Test/bin/release",
            OutputDirectory         = "./nuget"
                            };

    NuGetPack(nuGetPackSettings);
});

All functions available in NuGetAliases are described here NuGetAliases

Last task, push package to nuget.org

To send a package you should pass package path and the nuget server credentials.


Task("PushNuGetPackage")
    .IsDependentOn("CreateNuGetPackage")
    .Does(() =>
{
    var package = "../CakeTutorial.1.0.1.nupkg";

    NuGetPush(package, new NuGetPushSettings {
        Source = "https://nuget.org/",
        ApiKey = "NugetApiKey"
    });
});

Summary

Productions projects can have a lot of build scripts. It is good practice to write scripts, so that all possible variables are parameterized. So that all variables will be injected from the build server, and scripts will be universal.

Useful functionality

Cake offers a lot of extensions, I will present some of the most useful.

Script aliases

As I mentioned Cake is built on top of the Roslyn and Mono compiler which enables you to write your build scripts in C#. It gives you a lot of opportunities for customization and testing ! The possibility of testing gives a big advantage as against to for example Psake, where tasks are written in PowerShell language.

Creating an alias

To start with writing aliases, add a reference to the Cake.Core nuget package to library project.

PM> Install-Package Cake.Core

A script alias method is simply an extension method for ICakeContext.


using Cake.Core;
using Cake.Core.Annotations;

public static class CakeTutorialExtension
{
    [CakeMethodAlias]
    public static int GetDescription(this ICakeContext context)
    {
        return "It's working";
    }
}

Using the alias

Compile the assembly and add a reference to it in the build script via the #r directive.

#r "extensions/CakeTutorialExtension.dll"

Now, you can use extension in script.


Task("Test")
    .Does(() =>
{
    Information("Description {0}", GetDescription());
});

Criteria

Task can have criteria predicate, which specifies if it can be executed.


var shouldRun = Argument<bool>("shouldRun", false);
Task("Start")
    .WithCriteria(()=> shouldRun)
    .Does(() =>
{
        Information("It's working!!");
});

RunTarget("Start");

You can pass arguments to script like this:

Cake.exe .\argument.cake -shouldRun=true

Error Handling

Task can handle exception thrown during execution and then properly handle it.


Task("Start")
    .Does(() =>
{
})
.OnError(exception =>
{
    // Handle the error here.
});

RunTarget("Start");

To skip an exception, you can use the ContinueOnError task extension.

Task("Start")
    .ContinueOnError()
    .Does(() =>
{

});
RunTarget("Start");

If something went wrong you can throw an exception to indicate it. The Cake script runner will return exit code 1 to indicate that something went wrong.


var shouldRun = Argument<bool>("shouldRun", false);
Task("Start")
    .Does(() =>
{
        if(shouldRun == false){
            throw new Exception("Something went wrong");
        }
});

RunTarget("Start");

If you want to run some code regardless of how a task exited, you can use the Finally task extension.

Task("Start")
    .Does(() =>
{
})
.Finally(() =>
{  
    // Do something
});
RunTarget("Start");

Setup and teardown

Very often you would like to run some code before executing first task or after last task. You can do this using Setup and TearDown extensions.


Setup(() =>
{
    // Executed before the first task.
});

Teardown(() =>
{
    // Executed after the last task.
});

Passing a target to the script

In many cases, you may need to pass in a parameter which the task should be executed. In parameter you can define task which should be executed.

./build.ps1 -Target Publish

var task = Argument("target", "Start");

Task("Start")
    .Does(() =>
{
});

Task("Publish")
    .IsDependentOn("Start")
    .Does(() =>
{
});

RunTarget(task);

PowerShell addin

Cake-Build addin that extends Cake with Powershell commands. Allows you to execute Powershell comannds inside tasks.


#addin "Cake.Powershell"

Task("PowershellScript")
    .Does(() =>
{
    StartPowershellScript("Write-Host", args =>
        {
            args.AppendQuoted("It's working");
        });
});

AppVeyor

Ok, now when we know something about Cake, it's time to get to know the build server AppVeyor. First what you need to do is register your account. AppVeyor supports logging by GitHub, BitBucket, Visual Studio Online. When you loggin, you will see page where you can add your project. Click to the new project button and select repository. Last think what you need to do is configure build process. Select chosen project and go to settings tab. There are the most important thinks to configure.

  • General settings allows you to configure:
  • project name,
  • next build number
  • build version format
  • Default branch

    General settings

  • Environment settings allows you to define environment variables. If you want to transfer variables to Cake tasks you can do this like this:
Task("PushNuGetPackage")
    .IsDependentOn("CreateNuGetPackage")
    .Does(() =>
{
    var package = "../CakeTutorial." + EnvironmentVariable("APPVEYOR_BUILD_VERSION") +".nupkg";

    NuGetPush(package, new NuGetPushSettings {
        Source = "https://nuget.org/",
        ApiKey = EnvironmentVariable("NuGet_API_KEY")
    });
});

Environment settings

  • Build settings allows you to define build script

Build settings

Ok, now your CI is done. Every time when you add some changes in project, build will be automatically run. This is a very small demonstration of the possibilities offered by the Cake and AppVeyor.

About the author:

Filip Paluch

Versatile software developer in BT Skyrise, specializes in .NET, but he is also keen on javascript, html and many more technologies. Filip is a teamplayer and clean code enthusiastic. Code review with him is always a journey which help you learn a lot. Privately, a lover of travel.

Next Post Previous Post

blog comments powered by Disqus