Jenkins serving Cake: our recipe for Windows

profile picture

Dusty Engineer

5 Jan 2017  ·  12 min read


We walk you through our complete automated build process for the Universal Windows Platform, using Git, Jenkins and Cake.

Building and deploying applications is a process with many repetitive steps. Integrating the new code, syncing translations, cleaning up old files, setting higher version numbers, building the beta and release builds, running tests, packaging and signing packages, deploying beta builds, backing up release builds, … If you’ve ever created an application, you’re familiar with the process. It’s time-consuming and prone to errors.

In other words, not an ideal situation – which makes this process a perfect candidate for automation!

Now, we’re hardly the first to consider this: there’s tons of documentation and numerous tools out there for build automation, often with Jenkins at the center. But if you’re talking about automated builds for Windows apps, it seems to be hard to break out of the Microsoft ecosystem. In November Five’s setup, Windows development is only one (small) part of our complete development team – and creating a completely separate process would be a bit much… Which is why we’re happy to share our own solution, using Git, Jenkins and Cake! Fasten your seatbelts and take a deep breath… Let’s get this cake on the road!

Getting the ingredients right

Let’s go into some more detail concerning the different “ingredients” we need for this particular Cake recipe… Starting with the basics: Git and Git flow!

Git

Git probably doesn’t require an introduction for most of you. Git is a free and open source distributed version control system, designed to handle everything from small to very large projects quickly and efficiently. In other words, we use Git to check in our code.

Git flow

This Git branching strategy lays at the heart of our automated build process (you can get some more in-depth information in this excellent article).

Git flow gives us two baselines for tracking the history of a project:

  • master: holds the official release history
  • develop: functions as integration branch

Development will take place on one of these branches:

  • Feature branches: Every new feature has to be developed on a feature branch, with develop as its parent branch. Features never interact directly with master; when a feature is complete, it gets merged back into develop.
  • Release branches: When an app is ready to be released, we create a release branch. This starts the next release cycle. The Jenkins yml and Cake build script we describe in this post are optimized for this release cycle.

Git Flow

More essential ingredients: Jenkins and Cake

Jenkins

Jenkins is an open source automation server, with hundreds of plugins to support building, deploying and automating any project. That’s one sentence containing all of the major reasons why Jenkins is the best fit for our company!

The automation server has to support multiple kinds of software, multiple platforms and multiple OSs. Our in-house setup consists of one master server (running on Ubuntu), two MacOS slaves (running on just as many Mac Mini’s) and one Windows 10 slave running in a VMWare virtualized environment. That gives us eight workers to execute our many jobs.

Cake

Cake’s own website gives the best summary of the system: Cake (C# Make) is a 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.

A few more things we like about Cake:

  • It’s open-source and cross-platform. Cake is made with .NET Core, which means it can run on Windows, Linux and MacOS. Compiling and running applications is handled uniformly across these three with view commands.
  • It’s familiar. Cake is built on top of the Roslyn and Mono compiler, so you can write your build scripts in C#.
  • Cake is based on Make, a task-based build automation utility. All build steps are defined in tasks, which can in turn rely on other tasks. The ubiquity of Make, once again, makes the process easy to understand.
  • It supports plugins, which are easy to write yourself, and the most commonly-used tools for builds (MSBuild, MSTest).

Where we started, or: why Cake took the cake

Before we started using Cake to build our Windows 10 applications, we used a similar setup, but instead of Cake we used an MSBuild script.

What went wrong? Well, nothing! We could do everything what we wanted with the MSBuild script. In fact, the Cake build script does exactly the same thing our old MSBuild file took care of.

So why did we replace the MSBuild script with Cake?

The answer is pretty simple: MSBuild uses xml to configure the build task. Cake build scripts are written in C#. As C# developers we write C# code on a daily basis, which means Cake, unlike MSBuild, feels instantly familiar (and doesn’t have us researching syntax on a regular basis). This means writing and changing the build scripts is easier, quicker, and fewer mistakes are made. It also means that any developer can make these changes, without having to go through a learning curve first.

Some more advantages for us were the fact that we can make identical builds wherever we want with Cake (on our local machine, Jenkins, …) and the fact that it offers a Visual Studio Code plugin with syntax highlighting.

Our recipe: application lifecycle management via Git, Jenkins and Cake for UWP

Cake workflow

So how does our solution work?

Jenkins fetches the changes from Git via polling; gets and commits any new translations from Phraseapp; and starts the default Cake task.

Cake can then:

  • clean the solution
  • take care of the semantic versioning, based on git branches
  • restore the nuget package
  • perform the actual build of the applications
  • run unit tests
  • create an app package bundle
  • sign the app package bundle

At this point, Jenkins kicks back into action. It archives the packages, in our case to Dropbox and to a dedicated server, and uploads the beta builds to Hockey app. It then notifies the developers and/or project manager via Slack when new builds are available or have failed.

Now, let’s get down to the details to help you get the same workflow!

Preparing your Cake

Step 1: Preparing Git

Only files necessary for building the application are checked in with Git. All other files, like binaries and helper files, are ignored. We do this via a gitignore. We have to ignore following folders: tools and build (a custom folder we created to temporarily store the build artifacts).

To do this, simply add the following lines to the gitignore file; you can place them anywhere in the file.

#Cake
build
tools

Step 2: Acquiring Cake

You can download Cake from the website, install via Powershell or use the Visual Code plugin to acquire it. You only need two files to start working with Cake. You can place these files anywhere in the repository, but we prefer to place them at the root.

  • build.ps1: The bootstrapper powershell script. This file downloads Cake and its dependencies when needed. It contains a basic configuration and will start Cake.
  • build.cake: This file contains our buildscript. It will have some basic configuration by default.

With these files in place, we can run Cake for the first time. To do so, start powershell with administrator rights. Go to the root of your repository, for example:

cd c:\projects\ExampleApp

Execute the bootstrapper script:

.\build.ps1

That’s it! You should now see something like this:

Screenshot Powershell

Step 3: Customizing the bootstrapper

In our company, we use three build types (build configurations in Visual Studio), which are aligned on all platforms:

  • Debug: For development only
  • Beta: For beta distribution and testing
  • Release: The actual store build

The bootstrapper file (build.ps1) supports only Debug and Release by default. To better suit our needs, we replaced [ValidateSet("Release", "Debug")] with [ValidateSet("Release", "Beta")].

Step 4: Making our Cake file

First, we clean up the solution and remove the old build artifacts, to make sure we start with a clean slate:

Task("Clean")
    .Does(() =>
{
    // Remove old build artifacts from the build output folder
    CleanDirectories(projectPath + "/AppPackages");

    // Clean the solution, uwp supports multiple platforms
    foreach(var platform in supportedPlatforms)
    {
        MSBuild(solutionFile, configurator =>
            configurator.SetConfiguration(buildConfiguration)
                .SetVerbosity(Verbosity.Quiet)
                .SetMSBuildPlatform(MSBuildPlatform.x86)
                .SetPlatformTarget(platform)
                .WithTarget("Clean"));
    }
});

Next, we set up the semantic version of the app, using the GitVersion Tool. This version will be added in AssemblyInfo.cs. The GitVersion Tool is not part of the default Cake installation, so we have to add an import on top of the Cake.file:

#tool "nuget:?package=GitVersion.CommandLine"

Task("Versioning")
    .IsDependentOn("Clean")
    .Does(() =>
{
    GitVersion(new GitVersionSettings {
        UpdateAssemblyInfo = true
    });
});

The next step is using a default command to restore the packages.

Task("NugetPackageRestore")
    .IsDependentOn("Versioning")
    .Does(() =>
{
    NuGetRestore(solutionFile);
});

After this, we define the actual build. We do this multiple times, once for each platform.

Task("Build")
    .IsDependentOn("NugetPackageRestore")
    .Does(()=>{
    foreach(var platform in supportedPlatforms)
    {
        MSBuild(solutionFile, configurator =>
            configurator.SetConfiguration(buildConfiguration)
                .SetVerbosity(Verbosity.Quiet)
                .SetMSBuildPlatform(MSBuildPlatform.x86)
                .SetPlatformTarget(platform)
                .WithTarget("Build"));
    }
});

After the build of the application, we run all our tests. In this example we run only unit tests. All the names of our unit test projects are ending with “.UnitTests”. We can use a wildcard to find all our test projects.

Task("Test")
    .IsDependentOn("Build")
    .Does(()=>{
    MSTest("./Tests/**/*.UnitTests.dll");
});

As final step we sign the generated appxbundle. This is needed to install the app on Windows 10 mobile. If you want to install the app on a PC, just zip all files, because HockeyApp supports only one file for each app.

Task("Sign")
    .IsDependentOn("Test")
    .Does(()=>{

    if (!buildConfiguration.Equals("Release"))
    {
        var appxBundles = GetFiles(projectPath + "/AppPackages/**/*.appxbundle");

        Sign(appxBundles, new SignToolSignSettings {
                TimeStampUri = new Uri("http://timestamp.digicert.com"),
                CertPath = "certificate.pfx",
                Password = "Password"
        });
    }
});

The full file should look like this:

#tool "nuget:?package=GitVersion.CommandLine"

var target = Argument("target","Default");
var buildConfiguration = Argument("buildconfiguration","Release");

var solutionFile = "ExampleApp.sln";
var projectPath = "Source/Ui/ExampleApp.Universal";
var supportedPlatforms = new PlatformTarget[]
    {
        PlatformTarget.ARM,
        PlatformTarget.x64,
        PlatformTarget.x86,
    };

Task("Clean")
    .Does(() =>
{
    // Remove old build artifacts from the build output folder
    CleanDirectories(projectPath + "/AppPackages");

    // Clean the solution, uwp supports multiple platforms
    foreach(var platform in supportedPlatforms)
    {
        MSBuild(solutionFile, configurator =>
            configurator.SetConfiguration(buildConfiguration)
                .SetVerbosity(Verbosity.Quiet)
                .SetMSBuildPlatform(MSBuildPlatform.x86)
                .SetPlatformTarget(platform)
                .WithTarget("Clean"));
    }
});

Task("Versioning")
    .IsDependentOn("Clean")
    .Does(() =>
{
    GitVersion(new GitVersionSettings {
        UpdateAssemblyInfo = true
    });
});

Task("NugetPackageRestore")
    .IsDependentOn("Versioning")
    .Does(() =>
{
    NuGetRestore(solutionFile);
});

Task("Build")
    .IsDependentOn("NugetPackageRestore")
    .Does(()=>{
    foreach(var platform in supportedPlatforms)
    {
        MSBuild(solutionFile, configurator =>
            configurator.SetConfiguration(buildConfiguration)
                .SetVerbosity(Verbosity.Quiet)
                .SetMSBuildPlatform(MSBuildPlatform.x86)
                .SetPlatformTarget(platform)
                .WithTarget("Build"));
    }
});

Task("Test")
    .IsDependentOn("Build")
    .Does(()=>{
    MSTest("./Tests/**/*.UnitTests.dll");
});

Task("Sign")
    .IsDependentOn("Test")
    .Does(()=>{

    if (!buildConfiguration.Equals("Release"))
    {
        var appxBundles = GetFiles(projectPath + "/AppPackages/**/*.appxbundle");

        Sign(appxBundles, new SignToolSignSettings {
                TimeStampUri = new Uri("http://timestamp.digicert.com"),
                CertPath = "certificate.pfx",
                Password = "Password"
        });
    }
});

Task("Default")
    .IsDependentOn("Sign")
    .Does(()=>{
    Information("No target provided. Starting default task");
});

RunTarget(target);

You can invoke the following command in PowerShell. This will start the ‘Default’ task:

./build.ps1

If you want to specify the build configuration:

./build.ps1 -buildconfiguration=Beta

Serving the Cake

Step 1: Installing and configuring Jenkins

A detailed guide on how to set up Jenkins would be outside of the scope of this blog, but you can find all the details on the Jenkins home page.

Step 2: Configuring Jenkins jobs

When Jenkins is up and running (serving), you need to install two plugins: PowerShell+ and Git+.

When that’s done, create a new job in the form of a Freestyle project. Select Git as your source and configure the repository URL so Jenkins can pull your source code. You also want to enable SCM Polling as the build trigger, unless you want to always start your jobs manually.

Add a build step of the type “Windows Powershell”, and enter as command:

./build.ps1 -buildconfiguration=Release

Click Save to finalize your job configuration.

Step 3: Setting up PhraseApp

At November Five we use PhraseApp to distribute and synchronize our translations on mobile and web platforms. PhraseApp is a web-based translation management system which supports all major OSs. To configure PhraseApp we have to add a yaml configuration file (.phraseapp.yml) to our repository. Like the files needed for Cake, you can place this anywhere in the repository, but we prefer to place it in the root.

In the yaml file you define a project id (found in the PhraseApp project dashboard), the file format of the translation files and the support languages with their corresponding translation file. The yaml file for our example project would look like this:

phraseapp:
  project_id: project_id
  file_format: resx_windowsphone

  pull:

    targets:
    - file: ./Source/ExampleApp.Universal/Locals/Translations.resx
      params:
        locale_id: en

  push:

    sources:
  - file: ./Source/ExampleApp.Universal/Locals/Translations.resx
      params:
        locale_id: en

Note that, unlike PhraseApp’s own configuration manual, we didn’t add the access token. If we were to add this, we wouldn’t be able to track who added the translation, and it would allow everybody with access to our repository to see the token. Instead, every developer (and the Jenkins server) added the variable `PHRASEAPP_ACCESS_TOKEN to their system’s environment variables.

To synchronize the translation locally, you can use the CLI tool (download here) or the Visual Studio plugin (find it under Tools > Extensions and Updates > Online > Visual Studio Gallery by searching for “PhraseApp”)

And finally, some extra toppings

In this example, all values and configurations are in the Cake files. In a good buildscript however, config and code are separated. Many solutions I have seen, create an extra flavor file with extra CI/CD tasks, with all config in those tasks. The following example was created for a psake build file, but the principle is the same for Cake.

Task ProductionCI {

  Invoke-psake psakefile.ps1 CI -properties @{

    'solutionFileName' = 'Dummy.sln'

    'build_platform' = 'x86'

    'configuration'  = 'Release'

    'project_name'   = 'Dummy.UWP.Test'

  }

}

At November Five we have a in-house customisation for Jenkins to generate the jobs for a repository, based on a config file (in YAML format). This config file resides in the root of the repository of each project and is maintained by each developer. This way, job configs can be easily managed, updated, versioned and maintained: indispensable in a Jenkins setup with more than 1.500 individual jobs! We’ll go into more depth on this customization in a future blog post.

So by adding an extra flavor file, we would end up with two configurations: one for Jenkins and one for Cake. Instead, we reuse our Jenkins config yaml and expand it with the fields we need in Cake.

First we have to add an addin on top of our Cake script to install the Cake.Yaml plugin: #addin "Cake.Yaml".

Then we have to declare a helper class. This can be done anywhere in the Cake file, or in a separate file:

class Configuration
{
    [YamlDotNet.Serialization.YamlAlias("solutionFile")]
    public string SolutionFile {get; set;}

    [YamlDotNet.Serialization.YamlAlias("projectPath")]
    public string ProjectPath {get; set;}

    [YamlDotNet.Serialization.YamlAlias("supportedPlatforms")]
    public List<string> SupportedPlatforms {get; set;}

    public PlatformTarget GetPlatformTarget(string platform)
    {
        switch(platform)
        {
            case "x64": return PlatformTarget.x64;
            case "x86": return PlatformTarget.x86;
            case "ARM": return PlatformTarget.ARM;
            default: return PlatformTarget.MSIL;
        }
    }
}

The yml file then looks like this:

solutionFile: ExampleApp.sln
projectPath: "Source/Ui/ExampleApp.Universal"
supportedPlatforms: ['ARM', 'x86', 'x64']

We load the configuration at the setup phase. The setup phase will be started before all other tasks.

Setup(context =>
{
    configuration = DeserializeYamlFromFile<Configuration>("./jenkins.config.yml");
});

And that’s it! You’re only one last move away… Just run .\build.ps1 in PowerShell, and you’re good to go.

And with that, we’ve walked you through our entire Jenkins and Cake workflow! We hope you found something useful… Do you like our way of thinking? We’re looking for you! We’ve got a vacancy for a Windows developer open right now…