Continuous integration using TeamCity, Rake, Albacore, GitHub and NUnit for .Net – Part 2

This is the second part of my documentation of how I have put up a Continuous integration flow with TeamCity, Rake, Albacore, GitHub and NUnit for a .Net project.

What do I want to achieve?

In this part I will prepare my solution to be built from TeamCity; and to do this I will use Rake via Albacore. More specifically, I will show how to accomplish the following:

  • Manage the assembly version
  • Do a clean release build
  • Execute our NUnit tests
  • Create a ZIP package for deployment
  • Create a NuGet package for deployment

We will not look at the integration with TeamCity in this article, that’s for the future. However, after this article you will have a build, test and deploy process that you can trigger via the command prompt. And with just adding one additional NuGet publish task, you could as well publish the generated NuGet package.

As of now, I have decided to go with a solution where as much logic of my building process is kept in the source code. Whit this I mean, that I, using Albacore, will try to create Rake tasks that takes care of this instead of putting this responsibility on the continuous integration (CI) server. By doing this I hope to get a simpler CI server configuration, and for me as a developer, I like to find the specs for the build with the source code instead of in a CI server. Lets hope I can stick to this when looking closer at TeamCity in future articles.

Install Ruby, Rake, Albacore & Version bumper

First we need to install Ruby and then the necessary gems: Rake gem, Albacore gem and the Version bumper gem. This is something I will do on both my client as well as on my server where TeamCity is installed, since this is where the builds will be executed.

I used the installer from: http://rubyinstaller.org/downloads/ and at the time of this writing the latest version was: Ruby 1.9.2-p180.

During the installation I explicitly selected the two checkboxes to add it to the Path environment variable and to associate it with Ruby files.

Install the required gems

Since I chosen to register ruby in my environment path, to install the required gems is a real breeze. After the installation was finished I just fired up the command prompt and typed:

gem install rake
gem install albacore
gem install version_bumper

Meet Albacore

Just to provide you with a quick insight in what Albacore is, I have pasted some lines from the Albacore Wiki, below:

Albacore is intended to be a professional quality suite of Rake tasks to help automate the process of building a .NET based system. – https://github.com/derickbailey/albacore/wiki

Albacore is a build system that is based on Ruby’s Rake. Within it, you can use several different build ‘tasks’ that help you forge the build of your choice. – https://github.com/derickbailey/Albacore/wiki/Getting-Started

The Rakefile

A rakefile is a collection of tasks – work to be performed. A rakefile is also a valid ruby script, meaning you can write any executable ruby code that you wish to use with your tasks, in this file. – https://github.com/derickbailey/Albacore/wiki/Getting-Started

The Rakefile shoud be named: “Rakefile.rb“; and it is case-insensitive. According to the documentation it should reside in the “project root”. The project I will use for this guide is a rather simple one, and it’s named “PineCone” (GitHub site) and the folderstructure looks like this:

As you can see I have chosen to put my Rakefile and my NuSpec in a folder called Build and not directly in the root. In the Build folder, I have created tasks so that there will be a Builds folder created. This folder is with advantage excluded from Git using the .gitignore file. In the Builds folder there will be a folder created for each build, with a naming convention: [ProjectName]-v[Version]-[BuildConfigName]. In that folder the binaries, testresult and ZIP-package as well as the NuGet-package will be created.

A few words about the solution and its layout

To keep the complexity down, I’ve deliberately selected this, as of now, fairly simple project:

I will be adding more projects to this solution in the future but then I would like to share the versioning info as well as copyrights etc. For this I have selected to have a SharedAssemblyInfo added as a linked file. This shared assembly info file is the one we will use in our Albacore AssemblyInfoTask later on in the article.

SharedAssemblyInfo.cs

using System.Reflection;

#if DEBUG
[assembly: AssemblyProduct("PineCone (Debug)")]
[assembly: AssemblyConfiguration("Debug")]
#else
[assembly: AssemblyProduct("PineCone (Release)")]
[assembly: AssemblyConfiguration("Release")]
#endif

[assembly: AssemblyDescription("Library used for managing primitive value members in object-graphs as key-values.")]
[assembly: AssemblyCompany("Daniel Wertheim")]
[assembly: AssemblyCopyright("Copyright © Daniel Wertheim")]
[assembly: AssemblyTrademark("")]

[assembly: AssemblyVersion("1.1.0.0")]
[assembly: AssemblyFileVersion("1.1.0.0")]

With Albacore you could set each of the attributes above, but I will only affect versioning. Why? As of now it would be more natural for me to look in AssemblyInfo or SharedAssemblyInfo for these kind of attributes, and then look for versioning info in the build definitions.

I have two build configurations:

  • Debug
  • Release

Both configurations builds all projects, so the binaries being deployed are the same being tested by our test project; hence I don’t have a build configuration like “Deploy” that only compiles the the actual project and not the tests.

XML Documentation file

In Release mode the XML Documentation file is generated. To get rid of all warnings about not having a XML comment for all public members, I have added a Suppressed warning for this.

The complete Rakefile

Before having a look at the Rakefile, remember this is my first writings of Ruby code. Now, lets have a look at the complete and finished Rakefile and then break it down.

#--------------------------------------
# Dependencies
#--------------------------------------
require 'albacore'
require 'version_bumper'
#--------------------------------------
# My environment vars
#--------------------------------------
@env_solutionfolderpath = "../Solution/"
@env_projectname = "PineCone"
@env_buildconfigname = "Release"

def env_buildversion
  bumper_version.to_s
end

def env_projectfullname
  "#{@env_projectname}-v#{env_buildversion}-#{@env_buildconfigname}"
end

def env_buildfolderpath
  "Builds/#{env_projectfullname}/"
end
#--------------------------------------
# Albacore flow controlling tasks
#--------------------------------------
desc "Fixes version, compiles the solution, executes tests and deploys."
task :default => [:buildIt, :testIt, :deployIt]

desc "Fixes version and compiles."
task :buildIt => [:versionIt, :compileIt, :copyBinaries]

desc "Executes all tests."
task :testIt => [:runUnitTests]

desc "Creates ZIP and NuGet packages."
task :deployIt => [:createZipPackage, :createNuGetPackage]
#--------------------------------------
# Albacore tasks
#--------------------------------------
desc "Bumpes new version."
task :bumpVersion do
  bumper_version.bump_build
  bumper_version.write('VERSION')
end

desc "Updates version info."
assemblyinfo :versionIt => :bumpVersion do |asm|
  sharedAssemblyInfoPath = "#{@env_solutionfolderpath}SharedAssemblyInfo.cs"
  
  asm.input_file = sharedAssemblyInfoPath
  asm.output_file = sharedAssemblyInfoPath
  asm.version = env_buildversion
  asm.file_version = env_buildversion  
end

desc "Creates clean build folder structure."
task :createCleanBuildFolder do
  FileUtils.rm_rf(env_buildfolderpath)
  FileUtils.mkdir_p("#{env_buildfolderpath}Binaries")
end

desc "Clean and build the solution."
msbuild :compileIt => :createCleanBuildFolder do |msb|
  msb.properties :configuration => @env_buildconfigname
  msb.targets :Clean, :Build
  msb.solution = "#{@env_solutionfolderpath}#{@env_projectname}.sln"
end

desc "Copy binaries to output."
task :copyBinaries do
  FileUtils.cp_r(FileList["#{@env_solutionfolderpath}Source/#{@env_projectname}/bin/#{@env_buildconfigname}/*.*"], "#{env_buildfolderpath}Binaries/")
end

desc "Run unit tests."
nunit :runUnitTests do |nunit|
  nunit.command = "#{@env_solutionfolderpath}packages/NUnit.2.5.10.11092/tools/nunit-console.exe"
  nunit.options "/framework=v4.0.30319","/xml=#{env_buildfolderpath}/NUnit-results-#{@env_projectname}-UnitTests.xml"
  nunit.assemblies = FileList["#{@env_solutionfolderpath}Tests/**/#{@env_buildconfigname}/*.UnitTests.dll"].exclude(/obj\//)
end

desc "Creates ZIPs package of binaries folder."
zip :createZipPackage do |zip|
     zip.directories_to_zip "#{env_buildfolderpath}Binaries/"
     zip.output_file = "../#{env_projectfullname}.zip"
end

desc "Creates NuGet package"
exec :createNuGetPackage do |cmd|
  cmd.command = "NuGet.exe"
  cmd.parameters = "pack #{@env_projectname}.nuspec -version #{env_buildversion} -nodefaultexcludes -outputdirectory #{env_buildfolderpath} -basepath #{env_buildfolderpath}\Binaries"
end

Rakefile – “Dependencies”

We know that there’s a dependency on the albacore library, hence we need to define it using the require keyword. I’m also making use of version bumper, so we need to include that as well. I’m using version bumper to bump the versions of each build, as well as persist this information in the VERSION file, located in the Build folder.

require 'albacore'
require 'version_bumper'

Rakefile – “My environment vars”

As of now, before integrating with TeamCity, I have identified some few things I wan’t to define as some sort of shared configurations between my task. E.g the version to use for my project(s) and the path of the solution folder as well as the buildconfiguration name.

@env_solutionfolderpath = "../Solution/"
@env_projectname = "PineCone"
@env_buildconfigname = "Release"

def env_buildversion
  bumper_version.to_s
end

def env_projectfullname
  "#{@env_projectname}-v#{env_buildversion}-#{@env_buildconfigname}"
end

def env_buildfolderpath
  "Builds/#{env_projectfullname}/"
end

If you like you can also provide some shared configuration variables to Albacore, e.g. defining the NUnit test runner in one place.

Rakefile – “Albacore flow controlling tasks”

I like to see them as compositions of tasks with the purpose of controlling the flow in the build process. You could of course skip a lot of these, and instead “chain” the tasks directly, as I have done in the assemblyinfo task versionIt, where I state that before executing this task, the bumpVersion task needs to have been executed.

desc "Fixes version, compiles the solution, executes tests and deploys."
task :default => [:buildIt, :testIt, :deployIt]

desc "Fixes version and compiles."
task :buildIt => [:versionIt, :compileIt, :copyBinaries]

desc "Executes all tests."
task :testIt => [:runUnitTests]

desc "Creates ZIP and NuGet packages."
task :deployIt => [:createZipPackage, :createNuGetPackage]

I have selected to create the tasks below, so that I can execute just the build step or just the test step, …., etc. This can be achieved by passing the taskname to rake at the command prompt.

rake buildIt

this will only execute the tasks: versionIt, createCleanBuildFolder and compileIt.

Define a default task

If you don’t want to specify each task to be executed at the command prompt, you need to create a default task in the rakefile. If not you will be promted with a message stating:

Don’t know how to build task ‘default’.

In our case our default task is composed by three other tasks: buildIt, testI and deployIt. If you do have a look at the testIt task you will see that you can point on other tasks that in turn points to other tasks. So the testIt task could in turn have pointed to our runUnitTests task as well as a MSpec test task.

Rakefile – “Albacore tasks”

Now, it’s time to have a look at our actual worker task, the tasks that actually does something. There are a bunch of pre made tasks and you can create your own custom task etc.

Tasks: versionIt & bumpVersion

As stated above, I will only affect the version attribute in the SharedAssemblyInfo file and nothing else. The task used is the AssemblyInfoTask, which lets you set values to your assemblyinfo attributes. Note that I have specified a value for the input_file attribute on the task. If not, only the attributes being assigned a value on the Albacore task would be represented in the output_file and the rest would have been removed. In my case I have chosen to use a SharedAssemblyInfo file that acts as a template for all my projects in the solution. The only attributes I’m setting via Albacore and Rake is the version attributes.

desc "Updates version info."
assemblyinfo :versionIt => :bumpVersion do |asm|
  sharedAssemblyInfoPath = "#{@env_solutionfolderpath}SharedAssemblyInfo.cs"
  
  asm.input_file = sharedAssemblyInfoPath
  asm.output_file = sharedAssemblyInfoPath
  asm.version = env_buildversion
  asm.file_version = env_buildversion  
end

This task is dependent on the bumpVersion task which uses version bumper to read the current version string from the VERSION-file and then increment the build number by one. The version string from the version bumper is accessed via the property env_buildversion, defined under My environment vars.

desc "Bumpes new version."
task :bumpVersion do
  bumper_version.bump_build
  bumper_version.write('VERSION')
end

Tasks: compileIt, createCleanBuildFolder & copyBinaries

After the version has been set, it’s time for compilation so that we have something to test and deploy. It is the compileIt task that is being executed, but it states that before compileIt can execute, createCleanBuildFolder needs to have been executed; which in turn just creates a clean build folder. The copyBinaries task just copies the binaries to a folder named Binaries located in the build folder. This simplifies the creation of the ZIP- and NuGet packages.

desc "Creates clean build folder structure."
task :createCleanBuildFolder do
  FileUtils.rm_rf(env_buildfolderpath)
  FileUtils.mkdir_p("#{env_buildfolderpath}Binaries")
end

desc "Clean and build the solution."
msbuild :compileIt => :createCleanBuildFolder do |msb|
  msb.properties :configuration => @env_buildconfigname
  msb.targets :Clean, :Build
  msb.solution = "#{@env_solutionfolderpath}#{@env_projectname}.sln"
end

desc "Copy binaries to output."
task :copyBinaries do
  FileUtils.cp_r(FileList["#{@env_solutionfolderpath}Source/#{@env_projectname}/bin/#{@env_buildconfigname}/*.*"], "#{env_buildfolderpath}Binaries/")
end

Read more about the MSBuild task.

Task: unitTests

As of now, this solution has no integration tests. Just simple unit tests that needs to be executed. By convention I have determined that all my pure unit test assemblies should end with UnitTests, hence I only extract those assemblies and pass them to the NUnit test runner.

desc "Run unit tests."
nunit :runUnitTests do |nunit|
  nunit.command = "#{@env_solutionfolderpath}packages/NUnit.2.5.10.11092/tools/nunit-console.exe"
  nunit.options "/framework=v4.0.30319","/xml=#{env_buildfolderpath}/NUnit-results-#{@env_projectname}-UnitTests.xml"
  nunit.assemblies = FileList["#{@env_solutionfolderpath}Tests/**/#{@env_buildconfigname}/*.UnitTests.dll"].exclude(/obj\//)
end

In your scenario, you will probably have more test assemblies and might not want to have the NUnit-Console as a dependency to every project. Perhaps there should be a tool repository or something instead? Since I’m using the NuGet NUnit package in my unit test project, I get the tools placed in the packages repository for my solution which I then use. Learn more about the NUnit task.

The task I have defined also makes use of NUnit console runner switches. I have used two options:

  • nunit.options '/framework=v4.0.30319': tells the testrunner to use the .Net framework 4.0 to execute the tests.
  • nunit.options '/xml=#{env_buildfolderpath}/NUnit-results-#{@env_projectname}-UnitTests.xml': specifies the name and path of the resulting XML-file.

Note! It says: nunit.options, plural and not singular. If you provide two lines with different values for nunit.options, then last in wins.

I have used the Rake FileList helper to resolve all the unit test assemblies to be executed. It also takes some of My environment vars into account, so that I don’t have to hard code and manage the assemblies to treat as unit tests by hand.

Task: createZipPackage

Not much to say here. Just ZIP the Binaries folder under the Build folder using the ZIP task.

desc "Creates ZIPs package of binaries folder."
zip :createZipPackage do |zip|
     zip.directories_to_zip "#{env_buildfolderpath}Binaries/"
     zip.output_file = "../#{env_projectfullname}.zip"
end

Task: createNuGetPackage

There’s actually specific tasks for managing NuGet specifications and packages. I have decided to keep my NuSpec file and not generate it using either NuGet.exe from the projectfile, nor via the Albacore NuSpec task. I tried the NuGetPack task but couldn’t get it to use the NuGet.exe switch version, which I wan’t, since I would like to inject the build version into the package. Instead I relied on the simple Exec task, to call NuGet.exe with the switches I want.

desc "Creates NuGet package"
exec :createNuGetPackage do |cmd|
  cmd.command = "NuGet.exe"
  cmd.parameters = "pack #{@env_projectname}.nuspec -version #{env_buildversion} -nodefaultexcludes -outputdirectory #{env_buildfolderpath} -basepath #{env_buildfolderpath}\Binaries"
end

What have we accomplished?

Just use the command prompt and type:

rake

and you will get tested and packaged binaries, in versioned build folders:

Note that files are getting updated

When executing the version tasks, two files will get updated. The VERSION-file that contains the current version number as well as the SharedAssemblyInfo.cs file.

What’s next?

In the next part, Part 3, I will integrate GitHub and TeamCity so that our buildserver can get something to build.

//Daniel

6 thoughts on “Continuous integration using TeamCity, Rake, Albacore, GitHub and NUnit for .Net – Part 2

  1. Pingback: Continuous integration using TeamCity, Rake, Albacore, GitHub and NUnit for .Net – Part 1 « Daniel Wertheim

  2. Pingback: Continuous integration using TeamCity, Rake, Albacore, GitHub and NUnit for .Net – Part 3 « Daniel Wertheim

  3. Pingback: Eliminating Friction, Part 2: The Build Script « SaintGimp

  4. Pingback: Eliminating Friction, Part 2: The Build Script « SaintGimp

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s