Monday, December 10, 2018

Integration of Cake build script with TeamCity

Cake is a great tool for organizing a delivery pipeline for your application. I like it because it lets me to write the pipeline using C#, the language I know well. The great property of Cake, PSake and other similar frameworks is that they allow as to use the same building script on a local development machine and on CI servers. Here I'll explain how to integrate Cake with TeamCity.

I'll assume you have initial knowledge of Cake and TeamCity. Otherwise, you can start with reading:


Now let's talk about Cake and TeamCity together.

Logging


Cake pipeline usually consists of several tasks. It would be good to have a separate section for each such task in the TeamCity build log. I want to have a collapsible section for each Cake task in log:


Cake API contains methods TeamCity.WriteStartBuildBlock and TeamCity.WriteEndBuildBlock. Although it is possible to use them in each task, it can be automated. In Cake, there are TaskSetup and TaskTeardown methods that will be called before and after execution of each task. They can be used to start and end TeamCity block:

TaskSetup(setupContext =>
{
   if(TeamCity.IsRunningOnTeamCity)
   {
      TeamCity.WriteStartBuildBlock(setupContext.Task.Name);
   }
});

TaskTeardown(teardownContext =>
{
   if(TeamCity.IsRunningOnTeamCity)
   {
      TeamCity.WriteEndBuildBlock(teardownContext.Task.Name);
   }
});

Here TeamCity.IsRunningOnTeamCity property is used to execute the code only if it runs on TeamCity.

Now we have collapsable blocks in the build log. But still, we can improve it a little bit more.

Usually, tasks of Cake tend to have short names like Build, Test, Clean. In this case, it is easier to run them from the command line. But in the build log, I'd prefer to have more expanded descriptions of Cake tasks. And it is possible to provide such descriptions. To set description of a task, use Description method:

Task("Clean")
.Description("Create and clean folders with results")
.Does(() => { ... });

Now, these descriptions can be used to form build log blocks:

TaskSetup(setupContext =>
{
   if(TeamCity.IsRunningOnTeamCity)
   {
      TeamCity.WriteStartBuildBlock(setupContext.Task.Description ?? setupContext.Task.Name);
   }
});

TaskTeardown(teardownContext =>
{
   if(TeamCity.IsRunningOnTeamCity)
   {
      TeamCity.WriteEndProgress(teardownContext.Task.Description ?? teardownContext.Task.Name);
   }
});

It allows improving readability of the build log.

Progress indication


If running a Cake script takes a lot of time, it would be great to see, which task is executing now.



It can be done using TeamCity.WriteStartProgress and TeamCity.WriteEndProgress methods. Their calls can be inserted into the same TaskSetup and TaskTeardown:

TaskSetup(setupContext =>
{
   if(TeamCity.IsRunningOnTeamCity)
   {
      TeamCity.WriteStartBuildBlock(setupContext.Task.Description ?? setupContext.Task.Name);

      TeamCity.WriteStartProgress(setupContext.Task.Description ?? setupContext.Task.Name);
   }
});

TaskTeardown(teardownContext =>
{
   if(TeamCity.IsRunningOnTeamCity)
   {
      TeamCity.WriteEndProgress(teardownContext.Task.Description ?? teardownContext.Task.Name);

      TeamCity.WriteEndBuildBlock(teardownContext.Task.Description ?? teardownContext.Task.Name);
   }
});

Tests results


If you run some tests in your Cake task, it would be great to show the results of their execution in TeamCity.



It can be done using TeamCity.ImportData method. This method accepts two parameters: string description of data type and path to a file with data. For example, if MSTest is used for tests, here is how you can execute tests and inform TeamCity about their results:

Task("Run-Tests")
.Description("Run tests")
.IsDependentOn("Clean")
.IsDependentOn("Build")
.Does(() => {
   var testDllsPattern = string.Format("./**/bin/{0}/*.*Tests.dll", configuration);

   var testDlls = GetFiles(testDllsPattern);

   var testResultsFile = System.IO.Path.Combine(temporaryFolder, "testResults.trx");

   MSTest(testDlls, new MSTestSettings() {
      ResultsFile = testResultsFile
   });

   if(TeamCity.IsRunningOnTeamCity)
   {
      TeamCity.ImportData("mstest", testResultsFile);
   }
});

TeamCity supports several types of tests. Instead of mstest you can use nunitvstest and several more.

Code coverage analysis


TeamCity can show results of code coverage by tests.


Now TeamCity supports integration with DotCover tool. Let me show how to use DotCover in your Cake script. First of all, DotCover must be installed by adding:

#tool "nuget:?package=JetBrains.dotCover.CommandLineTools"

Now it can be used in your task:

Task("Analyse-Test-Coverage")
.Description("Analyse code coverage by tests")
.IsDependentOn("Clean")
.IsDependentOn("Build")
.Does(() => {
   var coverageResultFile = System.IO.Path.Combine(temporaryFolder, "coverageResult.dcvr");

   var testDllsPattern = string.Format("./**/bin/{0}/*.*Tests.dll", configuration);

   var testDlls = GetFiles(testDllsPattern);

   var testResultsFile = System.IO.Path.Combine(temporaryFolder, "testResults.trx");

   DotCoverCover(tool => {
         tool.MSTest(testDlls, new MSTestSettings() {
            ResultsFile = testResultsFile
         });
      },
      new FilePath(coverageResultFile),
      new DotCoverCoverSettings()
         .WithFilter("+:Application")
         .WithFilter("-:Application.*Tests")
      );

   if(TeamCity.IsRunningOnTeamCity)
   {
      TeamCity.ImportData("mstest", testResultsFile);

      TeamCity.ImportDotCoverCoverage(coverageResultFile);
   }
});

As you can see, during this task tests were also run. So we can inform TeamCity both about test results and coverage analysis results. Method TeamCity.ImportDotCoverCoverage does the last thing.

Publishing artifacts


TeamCity allows you to publish some artifacts that will be available for each build. A good candidate for such artifacts is a NuGet package created during the build process:


In order to do it, place all your artifacts in a folder. Then you can publish this folder using TeamCity.PublishArtifacts:

Task("Publish-Artifacts-On-TeamCity")
.Description("Publish artifacts on TeamCity")
.IsDependentOn("Create-NuGet-Package")
.WithCriteria(TeamCity.IsRunningOnTeamCity)
.Does(() => {
   TeamCity.PublishArtifacts(artifactsFolder);
});

Conclusion


I hope these short code snippets will save you some time and effort if you want to run your Cake script on TeamCity. The full version of the Cake script and application you can find at GitHub. Good luck!

No comments:

Post a Comment