cake-build

How we use Cake to build .NET Core apps and version assemblies

Jason Taylor Developer Tips, Tricks & Resources

Let Them Eat Cake!

Apologies for the bad pun. I couldn’t help it. Today, we’re going to talk about CakeBuild, and why Stackify decided to start using it. But first, a little background.

We like to begin with the end in mind at Stackify. As our platform started to unfold into its’ first compiled bits nearly five years ago, it quickly became apparent that we needed some decent structure around how we built and deployed the many layers of our complex stack.

Like most shops, we were in search of an ALM system that could encompass our sprint / work item management, and build services. We wanted something SaaS based as well, and ultimately we landed with the Atlassian JIRA suite. Their CI server, Bamboo, has orchestrated our builds and deploys ever since then.

Doing a build on your build server and workstation should be exactly the same.

Something that has always bothered me about CI tools and servers like this is that they rarely work in the same way as building on your machine. When you hit F5 in Visual Studio, there are a lot of different build options set that we simply don’t think about, that you need to mirror in your MSBUILD task in Bamboo. Then, you have to think about the next steps: where the artifacts go, transforms applied, what you’re doing with the output, etc.

Point is, there have been far too many times where building locally doesn’t have the same result as building on the CI server. And getting the CI server to build and behave the way we want it to is always less than ideal.

This all came to a head a few months ago when we started converting Prefix, our ASP.NET profiler, to .Net Core. I needed our build server to:

  • Pull the Git repo
  • Version the assembly
  • Check in the modified assembly version change
  • Build it – now with the .Net CLI, and not MSBUILD
  • Nupack it
  • Ship to our Myget server
  • Pull request this release branch back to ‘master’ in our repo

Traditionally, I’d do this with a series of Powershell Script Tasks in Bamboo, but there’s one big pain point: we use AWS Elastic EC2 instances for build agents, and to test even the slightest change you need to create a new image, and spin it up in Bamboo and have it build. It’s extremely time consuming and frustrating.

Enter Cake!

I needed a task runner that would allow anyone on our dev team to execute all of these steps locally in just the same way our build server would, both to validate the process works correctly; and in the event our build server was down or had other issues.

I came across CakeBuild and read the docs. It just made sense. With Cake, you get:

  • A C# based DSL (no more Powershell or command line scripting!!)
  • A huge library of built-in scripts for nearly any task you would need.
  • A system that supports third party scripts
  • It’s all nuget based. It pulls in everything it needs.
  • Good support for .Net Core

On paper, it was perfect. So let’s take a look at what it does.

I’m going to skip over the “getting started” steps, and focus on how I’ve accomplished some of the specifics, with a big focus on assembly versioning.

Task: Version Assemblies

But first, a word on our versioning methodology. With .Net Core, you can keep the assembly version in project.json, which makes a lot of sense because it’s easy to manipulate. It’s just a json string.

When we are prepping a new release, we first cut a “release candidate” branch in Git, and it will be named with the target version, i.e:

root/rc/1.2

We will do our testing and once done, cut the actual release branch, i.e.

root/release/1.2

If we have to release a fix for the version, we work on that same branch. That next builds will be 1.2.2, 1.2.3, 1.2.4, etc etc.

For all other builds, the version number will follow the default format in .Net core, i.e:

1.2.0-*, where “*” gets replaced with the branch name + build number, so you’d end up with a 1.2.0-develop-1, or 1.2.2-rc-3, etc. This allows us to push a nuget package and dll that that is testable but never ever will be confused with a release version. If you use any pre-release .Net core packages, this will seem familiar.

So, here is what my Cake task looks like to version. I’ll break it down.

var target = Argument("target", "Default");
var configuration = Argument("configuration", "Release");
var buildVersion = "";
var buildSuffix = "";
var branch = "";
bool bambooRelease = false;

Task("Version").Does(() => {
	branch = (EnvironmentVariable("bamboo_planRepository_branchName") ?? "unknown");
	var buildNumber = (EnvironmentVariable("bamboo_buildNumber") ?? "0");
	var json = ParseJsonFromFile("project.json")
	var version = json["version"].ToString();

See that? I check the environment variables (via a built in script) which will give me the branch name if I’m running in Bamboo, otherwise I don’t care, and “Unknown” is substituted (I could still get it with a Git task, but I was lazy and it wasn’t necessary). I then also parse the project.json again with a built in script.

Next, I use another built in script to check to see if we are, in fact, running in Bamboo.

if(Bamboo.IsRunningOnBamboo){
		Information("Running on Bamboo");
		Information(version);
		Information(branch);
		Information(buildNumber);
			
		if(branch.Contains("release/"))
		{
			bambooRelease = true;
			Information("Plan is release");
			var relVer = branch.Split('/')[1];
			var newVer = relVer+ "." + buildNumber + "-*";

Here I am looking for the branch name to support my versioning. If it is release, I know the next part is my Major.Minor. I append on the build number, and serialize the JSON back out to the project.json file.

//only write back if it is a release build
			json["version"]=newVer;
			SerializeJsonToFile("project.json",json);
			
		}
		Else{

For non-release, it will generate something like “1.2.0-rc-1” and in my specific case, I don’t need to write it back out to the file.

	Information("Plan is not release");
			var cleanBranchName = branch.Replace("/","-");
			buildSuffix = cleanBranchName+"-"+buildNumber;
			var newVer = version.Replace("*",cleanBranchName + "-" + buildNumber);;
			buildVersion = newVer;
		}
		
	}
	else{
		
			Information("Local Build");
			
			var cleanBranchName = "local";
			buildSuffix = cleanBranchName+"-"+buildNumber;
			var newVer = version.Replace("*",cleanBranchName + "-" + buildNumber);;
			buildVersion = newVer;
		}
	Information(buildVersion);
	Information(buildSuffix);
		
});

And that’s it. Assembly versioning done easy and flexibly with CakeBuild.

Task: Nuget Package

The next step to build and pack is just as easy. Check it out.

Task("Pack").Does(() => {
	if(buildSuffix!=""){
		DotNetCorePack("project.json", new DotNetCorePackSettings{ Configuration = configuration, VersionSuffix = buildSuffix });
	}
	else{
	    DotNetCorePack("project.json", new DotNetCorePackSettings{ Configuration = configuration });
	}

	var outputdir = "bin\\"+configuration;
	CopyFileToDirectory("NugetPack.ps1",outputdir);
	var wd = DirectoryPath.FromString(outputdir);
	
	if(Bamboo.IsRunningOnBamboo)
	{
		StartProcess("powershell.exe", new ProcessSettings { Arguments = "-file NugetPack.ps1", WorkingDirectory = wd});
		StartProcess("powershell.exe", new ProcessSettings {Arguments = "-file MygetPush.ps1", WorkingDirectory = wd});
	}
});

The DotNetCorePack script actually builds the project and the nuget package in one step. It runs a build using the .Net CLI, which means that if I’m targeting multiple frameworks, they will all be in the package, so I can reference in projects that target NETStandard, Netcoreapp, and full .Net framework. All in one simple step.

The NugetPack.ps1 file is a script that I actually created and have checked into the project. It simply checks for the existence of a nuget package (and symbols package) and then generates MygetPush.ps1 which is a script that will actually push the package to my private Myget server. It only does this for bamboo builds, which means that if I run this Cake script locally, I’m not going to end up with a bunch of superfluous packages on my build server. You could put all of this logic directly in the Cake script if you wanted to; I already had those scripts from my previous process, so I just left it as it was.

Task: Commit and Push

The last step is to commit and push the change to my project.json file.

Task("CheckinVersion").Does(() => {

	if(bambooRelease){
		var repPath = DirectoryPath.FromString(".");
		var filePaths = new FilePath[]{".\\project.json"};
		GitAdd(repPath, filePaths);
		GitCommit(repPath,"Bamboo Build Task","[email protected]","Bamboo has detected a release build - versioning assembly");
		
		GitPush(repPath);
		
		var repositoryDirectoryPath = DirectoryPath.FromString(".");

		var gitBranch = GitBranchCurrent(repositoryDirectoryPath);
		var remote = (gitBranch.Remotes[0].PushUrl).Split('/');
		var repName = remote[remote.Count()-1];


		string json = "{\\\"destination\\\": { \\\"branch\\\":{ \\\"name\\\":\\\"master\\\"}},\\\"source\\\":{\\\"branch\\\": {\\\"name\\\":\\\""+branch+"\\\"}},\\\"title\\\":\\\"Merge Bamboo Build\\\"}";


		StartProcess("C:\\curl\\bin\\curl.exe", new ProcessSettings { Arguments = "-v -u [our user name] \"[git repo url]"+repName+"/pullrequests\" -H \"Content-Type: application/json\" -d \" "+ json + "\""});
		}
}
);

Note that I have redacted some details, but again, I use the built-in scripts that Cake provides to commit and check in any file changes on my current branch (which happens to be my modified project.json with the new version number). I then use CURL to post a Pull Request to the api of my Bitbucket repo. The only part of this that was painful was the escape characters needed for the json that gets posted.

Putting the icing on top of the cake.

Sorry. Another bad cake pun. I couldn’t help it.

One thing that you’ve probably noticed in this is that I’m still using Bamboo. And that’s because Cake is simply a DSL for executing tasks. I still need an orchestration engine (like Bamboo) to kick off and execute the cake builds. But, Bamboo is now rather agnostic about the build. I’ve gone from having 10 or so complicated, hard to test Bamboo tasks to two simple tasks:

  1. Pull repo.
  2. Execute build.ps1

I’ve now also achieved a lot of portability. I could very easily migrate to a different CI server with a small level of effort.
You’re not limited to .Net Core project either. I love this so much that I went back and implemented this same pattern for my .Net 4.5 class libraries. There are just a couple of differences, for example there are scripts to specifically work with your AssemblyInfo.cs file, i.e:

bambooRelease = true;
Information("Plan is release");
var relVer = branch.Split('/')[1];

var newVer = relVer + "." + buildNumber;
var newProductVersion = newVer + "-*";

CreateAssemblyInfo(assemblyPath, new AssemblyInfoSettings{
	Product = assemblyProduct,
	Title = assemblyTitle,
	Company = assemblyCompany,
	Guid = guid,
	Version = newVer,
	FileVersion = newVer,
	InformationalVersion  = newProductVersion,
	Copyright = "Stackify, LLC " + DateTime.Now.Year
});

My methodology is the same for versioning, I just use a different mechanism to do it for these older projects. What I love about this, is that I previously had to do this through a custom MSBUILD task, which required a lot of finesse (and pain and alcohol) to get to work correctly on the build server and not break building locally. It was far too fragile.

If you think this can help make your life easier, check out CakeBuild. Don’t stop with just the build, either. Cake has scripts and add-ins for deployment (IIS, Azure, Docker, etc), for SQL tasks, to work with TFS work items, NUnit, etc. And if it doesn’t support something you need to do, you can simply create it yourself and publish as an add-on.

That really just leaves us with one last, horrible pun. With CakeBuild, you can have your cake and eat it too.

Have you tried cake? Comment below and tell us what you think!