Adventures with MVC 6 - Episode II : Setting up Continuous Integration

For me, step two to setting up a new solution is usually to set up continuous integration (CI).

Here's my step by step guide to set up Continuous Integration (CI) with TFS Build 2015 (vNext) for DNX based projects.

Defining the continuous build definition

  1. Navigate to your TFS server build page (e.g. http://tfs.acme.net:8080/tfs/DefaultCollection/Acme/_build)
  2. Click the green + button near the top left of the page.
  3. Double click Visual Studio from the list of build definition templates

The default template is a good start. We will modify it with the following steps:

  1. Under the Visual Studio Build step, check the "Clean" checkbox. I like to know that we have no build artifacts lying around to confuse us.
  2. Click the X to the right of the Visual Studio Test step because we are using xUnit as I said in the previous post.
  3. Click the X to the right of the Index Sources and Publish Symbols. We don't need to persist anything during a continuous build.
  4. Click the X to the right of the Publish Build. Again we don't need to persist anything during a continuous build.
  5. It should look like this now:
Build Definition Build
  1. Click the Repository tab
  2. Change Clean to 'true'. I like to build with no artifacts lying around to confuse things.
  3. Under Mappings, set the Map field to the TFS path to the solution. Remove any unnecessary mappings.
  4. It should look like this now:
Build Definition Repository
  1. Click the Triggers tab
  2. Check the Continuous integration (CI). This guarantees that the project will build every time there is a code check-in.
  3. Add the path to the solution. It should look like this:
Build Definition Triggers
  1. Click save.
  2. Type a descriptive name. I use the following pattern {Project Name}.{TFS Branch}.{Build Definition Purpose}. This helps me find the build definitions easier when I have many of them.
Build Definition Save
  1. Click Ok.
  2. Click Queue build… and watch the build log fly by.

You should get the following build failure if the solution contains a DNX project like I had in my previous post:

    Error NU1009: The expected lock file doesn't exist. Please run "dnu restore" to generate a new lock file.

So we need to add a build step.

  1. Add a folder to TFS, call it BuildProcess
  2. Add a PowerShell script to that folder called PrebuildDnxProjects.ps1
  3. The contents of that file should resemble the script I read about here:
 param (
     [string]$path = $(throw "-path is required.")
  )

 Write-Output "Pre-building DNX projects using path: $path"

  # bootstrap DNVM into this session.
 &{$Branch='dev';iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/aspnet/Home/dev/dnvminstall.ps1'))}

 Get-ChildItem -Path "$path" -Filter "global.json" -Recurse | ForEach-Object {

  Write-Output "Loading '$($_.FullName)' to determine the 'latest' DNX version"

  # load up the global.json so we can find the DNX version
  $globalJson = Get-Content -Path $_.FullName -Raw -ErrorAction Ignore | ConvertFrom-Json -ErrorAction Ignore

  if($globalJson)
  {
   $dnxVersion = $globalJson.sdk.version
  }
  else
  {
   Write-Warning "Unable to locate global.json to determine using 'latest'"
   $dnxVersion = "latest"
  }

  # install DNX
  # only installs the default (x86, clr) runtime of the framework.
  # If you need additional architectures or runtimes you should add additional calls
  # ex: & $env:USERPROFILE\.dnx\bin\dnvm install $dnxVersion -r coreclr
  & $env:USERPROFILE\.dnx\bin\dnvm install $dnxVersion -Persistent

   # run DNU restore on all project.json files in the src folder including 2>1 to redirect stderr to stdout for badly behaved tools
  Get-ChildItem -Path $path -Filter project.json -Recurse | ForEach-Object {
   Write-Output "Preparing to call dnu restore on '$($_.FullName)'"
   dnu restore $_.FullName 2>1
  }
}
  1. Click the build definition title link (e.g. Acme.Disgronifier.Main.Continuous)
  2. Click Edit
  3. Click Add build step…
  4. Click Utility and click Add to the right of PowerShell
  5. Click Close
  6. Drag the new PowerShell build step above the Visual Studio Build step
  7. Set the Script filename to the file location in TFS (e.g. $/Acme/BuildProcess/PrebuildDnxProjects.ps1)
  8. Set the arguments to -path "$(Build.SourcesDirectory)"
  9. It should look like:
Build Definition Build
  1. Click on the Repository tab
  2. Add a mapping to the BuildProcess folder (e.g. $/Acme/BuildProcess). This way the build agent can download the PowerShell scripts
  3. Click Save
  4. Click Queue build… and confirm that the build succeeds.

At this point we should have a successful build assuming that the code compiles. I also want the tests to be run as well though. Because I am using xUnit tests as described in the previous post, this is a bit trickier. Thankfully, Marcel de Vries has a great post on how to do just this.

  1. Add a PowerShell script to the BuildProcess folder called RunDnxTests.ps1
  2. The contents of that file should resemble the following:
 param (
     [string]$path = $(throw "-path is required."),
  [string]$testFolderPattern = "*TEST*",
  [string]$folderExcludePattern = "*HELPER*"
  )

 Write-Output "Running tests in path '$path'"

 $testResultsFileName = "testresults.xml"
 $transformedTestResultsPrefix = "TEST-"
 $xsl = "$PSScriptRoot\NUnitXml.xslt" # this can transform xunit xml results to nunit xml format
 Write-Output "Loading XSLT '$xsl'"
 $xslt = New-Object System.Xml.Xsl.XslCompiledTransform;
 $xslt.Load($xsl);
 Write-Output "Completed Loading XSLT"

 # load dnvm so that the dnx command will work
 dnvm upgrade

 #remember the oginigal path
 $originalPath = Get-Location

 Get-ChildItem "$path" -Recurse -Filter "$testFolderPattern" -Exclude "$folderExcludePattern" -Directory | ForEach-Object {
  Get-ChildItem "$($_.FullName)" -Filter "project.json" | ForEach-Object {
   $currentPath = $_.Directory

   # set the current path so that test output is written to the same folder as the project
   Write-Output "Setting the current path to $currentPath"
   Set-Location -Path "$currentPath"

   Write-Output "Running tests for project '$($_.FullName)'"
   dnx --project "$_" test -xml $testResultsFileName

   $testResultsInput = "$currentPath\$testResultsFileName"
   $testResultsOutput = "$currentPath\$transformedTestResultsPrefix$testResultsFileName"
   Write-Output "Transforming test results '$testResultsInput' to '$testResultsOutput'"
   $xslt.Transform("$testResultsInput", "$testResultsOutput");
  }
 }

 #reset the current path back to the path before the loop
 Set-Location "$originalPath"
 Write-Output "Current path reset to $currentPath"

Write-Output "Test run on '$path' completed."
  1. Add another file to the same BuildProcess folder. Call this file NUnitXml.xslt
  2. The contents of that file should be:
 <?xml version="1.0" encoding="UTF-8" ?>
 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output cdata-section-elements="message stack-trace"/>

   <xsl:template match="/">
     <xsl:apply-templates/>
   </xsl:template>

   <xsl:template match="assemblies">
     <test-results name="Test results" errors="0" inconclusive="0" ignored="0" invalid="0" not-run="0">
       <xsl:attribute name="date">
         <xsl:value-of select="assembly[1]/@run-date"/>
       </xsl:attribute>
       <xsl:attribute name="time">
         <xsl:value-of select="assembly[1]/@run-time"/>
       </xsl:attribute>
       <xsl:attribute name="total">
         <xsl:value-of select="sum(assembly/@total)"/>
       </xsl:attribute>
       <xsl:attribute name="failures">
         <xsl:value-of select="sum(assembly/@failed)"/>
       </xsl:attribute>
       <xsl:attribute name="skipped">
         <xsl:value-of select="sum(assembly/@skipped)"/>
       </xsl:attribute>
       <environment os-version="unknown" platform="unknown" cwd="unknown" machine-name="unknown" user="unknown" user-domain="unknown">
         <xsl:attribute name="nunit-version">
           <xsl:value-of select="assembly[1]/@test-framework"/>
         </xsl:attribute>
         <xsl:attribute name="clr-version">
           <xsl:value-of select="assembly[1]/@environment"/>
         </xsl:attribute>
       </environment>
       <culture-info current-culture="unknown" current-uiculture="unknown" />
       <test-suite type="Assemblies" name="xUnit.net Tests" executed="True">
         <xsl:attribute name="success">
           <xsl:if test="sum(assembly/@failed) > 0">False</xsl:if>
           <xsl:if test="sum(assembly/@failed) = 0">True</xsl:if>
         </xsl:attribute>
         <xsl:attribute name="result">
           <xsl:if test="sum(assembly/@failed) > 0">Failure</xsl:if>
           <xsl:if test="sum(assembly/@failed) = 0">Success</xsl:if>
         </xsl:attribute>
         <xsl:attribute name="time">
           <xsl:value-of select="sum(assembly/@time)"/>
         </xsl:attribute>
         <results>
           <xsl:apply-templates select="assembly"/>
         </results>
       </test-suite>
     </test-results>
   </xsl:template>

   <xsl:template match="assembly">
     <test-suite type="Assembly" executed="True">
       <xsl:attribute name="name">
         <xsl:value-of select="@name"/>
       </xsl:attribute>
       <xsl:attribute name="result">
         <xsl:if test="@failed > 0">Failure</xsl:if>
         <xsl:if test="@failed = 0">Success</xsl:if>
       </xsl:attribute>
       <xsl:attribute name="success">
         <xsl:if test="@failed > 0">False</xsl:if>
         <xsl:if test="@failed = 0">True</xsl:if>
       </xsl:attribute>
       <xsl:attribute name="time">
         <xsl:value-of select="@time"/>
       </xsl:attribute>
       <results>
         <xsl:apply-templates select="collection"/>
       </results>
     </test-suite>
   </xsl:template>

   <xsl:template match="collection">
     <test-suite type="TestCollection" executed="True">
       <xsl:attribute name="name">
         <xsl:value-of select="@name"/>
       </xsl:attribute>
       <xsl:attribute name="result">
         <xsl:if test="@failed > 0">Failure</xsl:if>
         <xsl:if test="@failed = 0">Success</xsl:if>
       </xsl:attribute>
       <xsl:attribute name="success">
         <xsl:if test="@failed > 0">False</xsl:if>
         <xsl:if test="@failed = 0">True</xsl:if>
       </xsl:attribute>
       <xsl:attribute name="time">
         <xsl:value-of select="@time"/>
       </xsl:attribute>
       <xsl:if test="failure">
         <xsl:copy-of select="failure"/>
       </xsl:if>
       <xsl:if test="reason">
         <reason>
           <xsl:apply-templates select="reason"/>
         </reason>
       </xsl:if>
       <results>
         <xsl:apply-templates select="test"/>
       </results>
     </test-suite>
   </xsl:template>

   <xsl:template match="test">
     <test-case>
       <xsl:attribute name="name">
         <xsl:value-of select="@name"/>
       </xsl:attribute>
       <xsl:attribute name="executed">
         <xsl:if test="@result='Skip'">False</xsl:if>
         <xsl:if test="@result!='Skip'">True</xsl:if>
       </xsl:attribute>
       <xsl:attribute name="result">
         <xsl:if test="@result='Fail'">Failure</xsl:if>
         <xsl:if test="@result='Pass'">Success</xsl:if>
         <xsl:if test="@result='Skip'">Skipped</xsl:if>
       </xsl:attribute>
       <xsl:if test="@result!='Skip'">
         <xsl:attribute name="success">
           <xsl:if test="@result='Fail'">False</xsl:if>
           <xsl:if test="@result='Pass'">True</xsl:if>
         </xsl:attribute>
       </xsl:if>
       <xsl:if test="@time">
         <xsl:attribute name="time">
           <xsl:value-of select="@time"/>
         </xsl:attribute>
       </xsl:if>
       <xsl:if test="reason">
         <reason>
           <message>
             <xsl:apply-templates select="reason"/>
           </message>
         </reason>
       </xsl:if>
       <xsl:apply-templates select="traits"/>
       <xsl:apply-templates select="failure"/>
     </test-case>
   </xsl:template>

   <xsl:template match="traits">
     <properties>
       <xsl:apply-templates select="trait"/>
     </properties>
   </xsl:template>

   <xsl:template match="trait">
     <property>
       <xsl:attribute name="name">
         <xsl:value-of select="@name"/>
       </xsl:attribute>
       <xsl:attribute name="value">
         <xsl:value-of select="@value"/>
       </xsl:attribute>
     </property>
   </xsl:template>

   <xsl:template match="failure">
     <failure>
       <xsl:copy-of select="node()"/>
     </failure>
   </xsl:template>


</xsl:stylesheet>
  1. Click the build definition title link (e.g. Acme.Disgronifier.Main.Continuous)
  2. Click Edit
  3. Click Add build step…
  4. Click Utility and click Add to the right of PowerShell
  5. Click Test and click Add to the right of Publish Test Results
  6. Click Close
  7. Select the new PowerShell build step and set the Script filename to the new PowerShell script that was just added (e.g. $/Acme/BuildProcess/RunDnxTests.ps1)
  8. Set Arguments to -path "$(Build.SourcesDirectory)"
  9. Check Continue on Error
  10. Click the Publish Test Results build step
  11. Set Test Result Format to NUnit
  12. Check the Merge Test Results checkbox
  13. Click Save
  14. Click Queue build…

At this point the build should complete and produce test results that can be seen on the build page. It should look something like this:

Rest results

If you click the link bellow Test results, you can get an even more detailed report.

Finally, the last step is to turn this new build definition into a template.

Test summary
  1. Navigate to your TFS server build page (e.g. http://tfs.acme.net:8080/tfs/DefaultCollection/Acme/_build)
  2. Right click the previously made build definition (e.g. Acme.Disgronifier.Main.Continuous)
  3. Click Save as template…
  4. Name it Project.Branch.Continuous
  5. Click OK

Now next time you create a solution that needs a continuous build definition

  1. Navigate to your TFS server build page (e.g. http://tfs.acme.net:8080/tfs/DefaultCollection/Acme/_build)
  2. Click the green + button near the top left of the page.
  3. Click Custom
  4. Double click Project.Branch.Continuous
  5. Click on Repository
  6. Change Clean to 'true'.
  7. Under Mappings, set the Map field to the TFS path to the solution. Remove any unnecessary mappings.
  8. Click the Triggers tab
  9. Set the path to the solution.
  10. Click Save
  11. Name the new build definition based on the naming pattern {Project Name}.{Branch}.{Continuous}
  12. Click OK

At some later post I'll detail step by step how to set up alerts for when builds fail.