Thursday, July 19, 2018

Automated TFS builds with Docker

In this post, I'll show you how to get MSBuild, xUnit, and JustMock working in a Docker Windows container and have it compile an Asp.Net MVC application with some Unit Tests.
From Environment perspective, we have TFS 2017 Update1 on Windows 2016 servers.

The key driving factor for this solution is to isolate builds. With the increase in the variety of builds generated by build server, the list of dependencies installed locally also increases. The list was getting bigger starting from various versions of Visual Studio, multiple versions of the unit testing framework to name a few.

So let's get going.

Step number #1 is to setup environment

  1. Create a Win2016 DataCenter VM for TFS builds. Make sure the Version is 1607 (LTSB) as Docker UCP has some compatibility issues with latest versions. Let's call it Build Server.
  2. Create a Service account which will be used to run VSTS agent
  3. Install and configure VSTS agent. You can refer to this article.
  4. Create a new local group called 'docker-users'
  5. Add the user which is used to configure VSTS agent to this group e.g. TFSSERVICE@org.com
  6. Install Docker on Windows 2016. Refer this article for instructions.
  7. Test your Docker EE installation by running the hello-world container
docker container run hello-world:nanoserver

        You may run into issues if you have anti-virus software installed on Build Server.
        For me adding the exceptions to SEP worked.
        Add exceptions to SEP for following folders:
  • C:\ProgramData\docker\*
  • C:\Program Files\docker\*

Step number #2 is to build docker image

  1. Expand maximum container disk size
    • Stop docker service by executing:

      sc.exe stop docker

    • From elevated POSH console execute:

      New-Item -Name daemon.json -Path C:\ProgramData\docker\config\ -ItemType File
    • Edit daemon.json and add

      {
       "storage-opts": [
          "size=120GB"
        ],
        "group" : "docker-users"
      }

      Refer this and this.

    • Start docker service

      sc.exe start docker

  2. Create a directory for necessary tools and files
    mkdir f:\BuildTools
  3. Create a dockerfile
    New-Item -Name Dockerfile -Path F:\BuildTools\ -ItemType File -Force
  4. Edit Dockerfile to have contents as:
    # escape=`
    # Use the latest Windows Server Core image with .NET Framework 4.7.2.
    FROM microsoft/dotnet-framework:4.7.2-sdk
    LABEL maintainer="me@org.com"

    # Download the WebDeploy.
    RUN mkdir c:\install
    ADD WebDeploy_amd64_en-US.msi c:\install\WebDeploy_amd64_en-US.msi
    WORKDIR c:\install
    RUN powershell start-Process msiexec.exe -ArgumentList "/i","c:\install\WebDeploy_amd64_en-US.msi","/qn" -Wait

    # Download the Build Tools bootstrapper.
    ADD https://aka.ms/vs/15/release/vs_buildtools.exe C:\install\vs_buildtools.exe

    # Install Build Tools excluding workloads and components with known issues.
    # https://docs.microsoft.com/en-us/visualstudio/install/use-command-line-parameters-to-install-visual-studio
    # https://docs.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools
    SHELL ["cmd", "/S", "/C"]
    RUN C:\install\vs_buildtools.exe --quiet --wait --norestart --nocache `
    --installPath C:\BuildTools `
    --add Microsoft.Net.Component.4.7.1.SDK `
    || IF "%ERRORLEVEL%"=="3010" EXIT 0

    # Install DotNet 3.5
    RUN mkdir c:\install\sxs
    ADD microsoft-windows-netfx3-ondemand-package.cab c:\install\sxs\microsoft-windows-netfx3-ondemand-package.cab
    WORKDIR c:\install
    RUN powershell Install-WindowsFeature -Name NET-Framework-Features -Source C:\install\sxs -Verbose

    # Environment Variables for Telerik JustMock ENV COR_ENABLE_PROFILING 1
    ENV JUSTMOCK_INSTANCE 0
    ENV COR_PROFILER="{B7ABE522-A68F-44F2-925B-81E7488E9EC0}"

    # Start developer command prompt with any other commands specified.
    # ENTRYPOINT C:\BuildTools\Common7\Tools\VsDevCmd.bat &&

    # Default to PowerShell if no other command specified.
    CMD ["powershell.exe", "-NoLogo", "-ExecutionPolicy", "Bypass"]
  5. Copy the WebDeploy installer in F:\BuildTools along with the DockerFile. You can get WebDeploy_amd64_en-US.msi from here
  6. Copy the .Net Framework 3.5 installer in F:\BuildTools along with the DockerFile. This is needed because our projects have a dependency on Telerik JustMock. And integrating JM via MSBuild requires legacy .Net framework. You can grab the microsoft-windows-netfx3-ondemand-package.cab by mounting Windows Server Core iso.
  7. Build the image
    # docker build -t vsbuildtools2017:web-v1 -m 2GB .
  8. Test by running the container
    docker run -it vsbuildtools2017:web-v1

Step number #3 is to upload your Docker image to Azure registry

This step is optional but I added it here just in case it is applicable in your scenarios too. We used Azure Container Registry for hosting our images.
  1. Install the latest version of AZ CLI from here
  2. Login with
    az login --use-device-code az acr login --name {RegistryName}
  3. Find the tag
    az acr list --resource-group {ResourceGroupName} --query "[].{acrLoginServer:loginServer}" --output table
    Output: {RegistryName}.azurecr.io
  4. Add tag
    docker tag vsbuildtools2017:web-v1 {RegistryName}.azurecr.io/vsbuildtools2017:web-v1
  5. Push the image to ACR
    docker push {RegistryName}.azurecr.io/vsbuildtools2017:web-v1
  6. And to pull the image from ACR, I used a "Run Powershell" task in my build definition which essentially does this:
    docker login $(RegistryName) --username $(DockerRegistryUserId) --password $(DockerRegistryPassword)
    docker pull vsbuildtools2017:web-v1

Step number #4 is to configure your Visual Studio MVC 5 project

  1. xUnit configuration
    • Refer to this article
    • Make sure UnitTest project in your solution does not refer "Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll" as it's in GAC and not found by docker container. Moreover, we are using xUnit Testing framework which is not dependent on this assembly as it can be added to a simple class library project as well via Nuget. It doesn't need to be a UnitTest project which by default brings down this assembly reference.
    • Add NuGet reference in Unit Test Project for "xunit.runner.msbuild" version 2.3.1
      If your project is built using .Net Framework 4.5.1 and lower then you need to:
      Install 4.5.2 Developer Pack from within VS (or here) by selecting properties of UnitTest project. Change the target framework for UnitTest project to 4.5.2
    • In your TFS source code repo add a new targets file XUnitRunner.targets. I placed it under $/{TFSProject}/Dev/build/ORG/MSBuild
      Contents of this target file :

      <Project DefaultTargets="RunXunitTests" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

      <PropertyGroup>

      <XunitRunnerAssembly>$(BUILD_SOURCESDIRECTORY)\packages\xunit.runner.msbuild.2.3.1\build\net452\xunit.runner.msbuild.net452.dll </XunitRunnerAssembly>

      <ProjectSourcesDirectory>$(BUILD_SOURCESDIRECTORY)</ProjectSourcesDirectory>

      <ProjectBuildConfiguration>$(BuildConfiguration)</ProjectBuildConfiguration>

      <TestRunOutputInHtml>$(PackageStagingDirectory)\TestRun.html </TestRunOutputInHtml>

      <TestRunOutputInXml>$(PackageStagingDirectory)\TestRun.xml</TestRunOutputInXml>

      <JustMockRunnerAssembly>$(ProjectSourcesDirectory)\SharedBinary\Telerik\JustMockProEdition\2018.2.511.5\Telerik.JustMock.MSBuild.dll</JustMockRunnerAssembly>

      <JustMockProfilerAssembly>$(ProjectSourcesDirectory)\SharedBinary\Telerik\JustMockProEdition\2018.2.511.5\Telerik.CodeWeaver.Profiler.dll </JustMockProfilerAssembly>

      </PropertyGroup>

      <UsingTask AssemblyFile="$(XunitRunnerAssembly)" TaskName="Xunit.Runner.MSBuild.xunit" />

      <ItemGroup>
      <TestAssemblies Include="$(ProjectSourcesDirectory)\**\bin\$(ProjectBuildConfiguration)\**\*test*.dll" Exclude="$(ProjectSourcesDirectory)\**\bin\$(ProjectBuildConfiguration)\**\*xunit*.dll" />

      </ItemGroup>

      <UsingTask AssemblyFile="$(JustMockRunnerAssembly)" TaskName="Telerik.JustMock.MSBuild.JustMockStart" />

      <UsingTask AssemblyFile="$(JustMockRunnerAssembly)" TaskName="Telerik.JustMock.MSBuild.JustMockStop" />

      <Target Name="BeforeTestConfiguration"> <JustMockStart ProfilerPath="$(JustMockProfilerAssembly)" />

      </Target>

      <Target Name="AfterTestConfiguration">

      <JustMockStop />

      </Target>

      <Target Name="RunXunitTests">

      <Message Text="(BUILD SOURCESDIRECTORY is) - $(ProjectSourcesDirectory)" Importance="high"/>

      <Message Text="(Assemblies from) - %(TestAssemblies.Identity)" Importance="High" />

      <JustMockStart />

      <xunit Assemblies="@(TestAssemblies)" DiagnosticMessages="true" Html="$(TestRunOutputInHtml)" Xml="$(TestRunOutputInXml)" IgnoreFailures="false" />

      <JustMockStop />

      </Target>

      </Project>

  2. Telerik JustMock (JM) integration
    • Add necessary binaries under $/{TFSProject}/Dev/SharedBinary/Telerik/JustMockProEdition/2018.2.511.5 directory. You can find these files from your Dev machine where JM is installed locally.
      • ImageBlacklist.cfg
      • Telerik.CodeWeaver.Profiler.dll
      • Telerik.JustMock.dll
      • Telerik.JustMock.MSBuild.dll
      • TelerikCodeWeaverProfiler.tlb
    • Edit the ImageBlacklist.cfg file and remove MSBuild entry from it.
    • JM needs some ENV variables which will be set by TFS Build definition. Some are made available in Docker image.
      In my build definition, I have a "Run Powershell" task which does set the ENV variables and looks like this:
      Write-Host "##vso[task.setvariable variable=COR_PROFILER]{B7ABE522-A68F-44F2-925B-81E7488E9EC0}"

      Write-Host "##vso[task.setvariable variable=COR_ENABLE_PROFILING]1"

      Write-Host "##vso[task.setvariable variable=JUSTMOCK_INSTANCE]0"

      Write-Host "##vso[task.setvariable variable=COR_PROFILER_PATH]$(BUILD.SOURCESDIRECTORY)\SharedBinary\Telerik\JustMockProEdition\2018.2.511.5\Telerik.CodeWeaver.Profiler.dll"

    • Refer more from here https://www.telerik.com/forums/use-justmock-in-msbuild
  3. Create a Publishing profile
    • Right-click Web project in Solution Explorer and select Publish
    • Click "Create new profile"
    • Select "IIS, FTP etc." and click OK
    • Select "Web Deploy Package" as publish method. Enter "AppNameWebDeployPkg" as Package location
    • Leave Site name blank
    • Click Next
    • Select Release from Configuration
    • Leave File Publish Options unchecked
    • Select "Use this connection string at runtime" checkbox for all Databases connection strings with these placeholder values. E.g.
      • AuthContext: __AuthContextStr__
      • MktgDatabaseEntities: __MktgDatabaseEntitiesStr__
      • Logging: __LoggingStr__
    • Click Save
    • Rename "CustomProfile" (newly created as above) as "Release"
    • Edit the profile pubxml file to include following statement as first after PropertGroup tags. Refer this
      <IncludeSetACLProviderOnDestination>False</IncludeSetACLProviderOnDestination>

Step number #5 is to run the container to build and test your solution

  1. Run the following command in a "Run Powershell" task to build the VS solution:
    docker run -i --rm -m 2GB --cpus="2" --name myweb -w C:/users/Administrator/Documents -v "$(Agent.BuildDirectory):c:/users/Administrator/Documents/myweb:rw" {RegistryName}.azurecr.io/vsbuildtools2017:web-v1 msbuild "myweb\s\CSS.sln" /nr:false /fl /flp:"logfile=myweb\s\css.sln-build.log" /p:platform="any cpu" /p:configuration="release" /p:RunCodeAnalysis=true /p:DeployOnBuild=true /p:PublishProfile=Release /p:PackageLocation="c:/users/Administrator/Documents/myweb/a/PublishedPackage"
  2. Run the following command in a "Run Powershell" from file task to run the unit test (xUnit) cases. These are file contents:
    param
    (
    $BuildDir #$(Agent.BuildDirectory)
    )

    $workDir = "C:/users/Administrator/Documents"
    docker run -i --rm -m 2GB --cpus="2" --name csswebtest -w "${workDir}"
    -v "${BuildDir}:${workDir}/cssweb:rw"
    -e COR_PROFILER_PATH="${workDir}/cssweb/s/SharedBinary/Telerik/JustMockProEdition/2018.2.511.5/Telerik.CodeWeaver.Profiler.dll" a23459876.azurecr.io/arch/ref/vsbuildtools2017:web-v1
    msbuild "cssweb\s\build\im\raft\msbuild\IM.XunitRunner.targets" /nr:false /fl /flp:"logfile=cssweb\s\css.xunitrunner.log" /p:platform="any cpu" /p:configuration="release" /p:XunitRunnerAssembly="${workDir}/cssweb/s/packages/xunit.runner.msbuild.2.3.1/build/net452/xunit.runner.msbuild.net452.dll" /p:ProjectSourcesDirectory="${workDir}/cssweb/s" /p:ProjectBuildConfiguration="release" /p:TestRunOutputInHtml="${workDir}/cssweb/a/CSS/TestRun.html" /p:TestRunOutputInXml="${workDir}/cssweb/a/CSS/TestRun.xml"

I hope this will prove a useful template and save some efforts in your CI/CD journey with Windows Containers.

Wednesday, April 4, 2018

Customize TFS XAML build template to create a Bug WI

With the release of TFS 2013, many new features have been introduced. And one of those is the script hooks which lets you add pre- and post-build as well as pre- and post-test hooks. These make customizing your build process a breeze.

Recently, I have been assigned a task to customize the build process so that it can create a BUG work item in case of build failure. Now you'd think that this a standard request and also available OOB in TFS. However, this time client wants to create the work items in different TFS project. They have a clear separation between ALM and VCS projects. There are obviously pros and cons of this separation, however, that’s another post in itself. And they were using TFS 2012 build templates. So I have to implement the custom hooks logic in a 2012 build – unfortunately, there is no straight forward way. Obviously, that's the reason they called me.

I came up with a less popular approach for 2012 builds. This post will walk you through the steps of customizing the default build template for a post-build script.

Step 1 - Customize Build template


Prepare a solution to customize your build process template:


I created a new VS 2013 solution and that solution includes this customized build process template. 
  1. Create a new solution named BuildProcessSource that contains a new Visual Basic workflow activity library code project called Templates. 

  2. Take a copy of standard build template which you want to customize. I used DefaultTemplate.11.1.xaml.
    Connect to your TFS project via Team Explorer. On the Builds page, click "New Build Definition" link.
    Download a copy of the default template - DefaultTemplate.11.1.xaml. Save your new template in the same folder that contains the code project you created earlier in this procedure as DefaultTemplate.11.1-CreateWI.xaml.
  3. In solution explorer, add the DefaultTemplate.11.1-CreateWI template to your Templates project using Existing Item context menu option. You don’t need the Activity1.xaml file, so you can delete it if you want.
  4. Set the Build Action property of your template to Content.
  5. Add the following references to your Templates code project:
    1. Microsoft.TeamFoundation.Build.Activities
    2. Microsoft.TeamFoundation.Build.Client
    3. Microsoft.TeamFoundation.Build.Workflow
    4. Microsoft.TeamFoundation.VersionControl.Client
  6. Save your project and upload your new solution. Check in pending changes.
  7. After you have uploaded a custom build process template to your team project as explained above, you can use the template from your build definitions. On the builds page, create or edit a build definition. On the build definition Process tab, choose Show details, and then choose New. After you choose New: Type or browse to the path to the template (DefaultTemplate.11.1-CreateWI) on your Team Foundation Server. After you specify the path to the template, you can select it from the list. Save your build definition.
  8. Above process is documented here 

Perform customizations on build process template:

  1. Double-click the DefaultTemplate.11.1-CreateWI.xaml template from your solution explorer after opening the VS 2013 project as created above.
  2. Add Arguments as:
    1. CreateCustomBugWI (Input, Boolean with default as False)
    2. TfsProjectForCustomBugWI (Input, String)
    3. PostBuildScriptPath (Input, String)
  3. Add Variables as:
    1. PostBuildScriptLocalPath (String, Scope: Handle Exception)
  4. Scroll down beneath the TryCatch activity called “Try Compile, Test, and Associate Changesets and Work Items”
  5. Look for Sequence activity titled as "If Create WorkItem". You'll find this under Catches>Handle Exception which is nested under "Try to Compile the Project" under "Compile and Test"
  6. Create a new Sequence activity under Else section block of "If CreateWorkItem" activity
  7. Add a new "If" activity under new Sequence activity and set the condition to CreateCustomBugWI to ensure it will only run when the argument is passed.
  8. Add in a Sequence activity in the Then block of new "If" activity with Display name as "Create Bug in case of build failure"
  9. Add a "ConvertWorkspaceItem" activity inside "Create Bug in case of build failure" activity with details as:
    1. Display name: Convert post-build script file name
    2. Input: PostBuildScriptPath
    3. Result: PostBuildScriptLocalPath
    4. Workspace: Workspace
  10. Add an "InvokeProcess" activity and join it with "Convert post-build script file name" with details as:
    1. Display name: Execute Post-build script
    2. File name: "PowerShell"
    3. Output Encoding: System.Text.Encoding.GetEncoding(System.Globalization.CultureInfo.InstalledUICulture.TextInfo.OEMCodePage)
    4. Arguments: String.Format(" ""& '{0}' '{1}' '{2}' '{3}' "" ", PostBuildScriptLocalPath, TfsProjectForCustomBugWI, BuildDetail.BuildNumber, BuildDetail.RequestedBy)
    5. To see results from the powershell command drop a "WriteBuildMessage" activity on the "Handle Standard Output" and pass the stdOutput variable to the Message property.
    6. Drop a "WriteBuildError" activity on the "Handle Error Output" and pass the errOutput variable to the Message property.
  11. This leads to the following result

  12. To publish it, check in the Build Process Template

Step 2 - Prepare PowerShell script


Do ensure that ExecutionPolicy defined on build server is set to Remotesigned.
You can set the policy by executing below command in PowerShell console:

set-executionpolicy remotesigned

The script itself is pretty straight-forward. It expects 3 script parameters:
  1. ProjectName
    It's a mandatory string parameter which is set from build definition editor.
  2. BuildNumber
    It's a mandatory string parameter which is set from customized build template.
  3. BuildRequestedBy
    It's a mandatory string parameter which is set from customized build template.
It then invokes TFS REST api to create a work item of type BUG in specified TFS project. It sets following fields for the new WI:
  1. Title (/fields/System.Title)
  2. Assigned To (/fields/System.AssignedTo)
  3. Repro steps (/fields/Microsoft.VSTS.TCM.ReproSteps)
  4. Severity (/fields/Microsoft.VSTS.Common.Severity)
  5. Reason (/fields/System.Reason)
  6. Comments (/fields/System.History)
You can find more technical details here and here 

I have enclosed some snippets from the script below:

param
(
    [Parameter(Mandatory)]
[string]
$ProjectName,        #Coming from XAML build definition argument
    [Parameter(Mandatory)]
[string]
$BuildNumber,        #Coming from Customized TFS XAML build
   
    [Parameter(Mandatory)]
[string]
$BuildRequestedBy    #Coming from Customized TFS XAML build
)
$bodyUpdate = "
[
{`"op`":`"add`",`"path`":`"/fields/System.Title`",`"value`":`"Build Failure in Build: $BuildNumber`"},
{`"op`":`"add`",`"path`":`"/fields/System.AssignedTo`",`"value`":`"$BuildRequestedBy`"},
{`"op`":`"add`",`"path`":`"/fields/Microsoft.VSTS.TCM.ReproSteps`",`"value`":`"Start the build using TFS Build`"},
{`"op`":`"add`",`"path`":`"/fields/Microsoft.VSTS.Common.Severity`",`"value`":`"1 - Critical`"},
{`"op`":`"add`",`"path`":`"/fields/System.Reason`",`"value`":`"Build Failure`"},
{`"op`":`"add`",`"path`":`"/fields/System.History`",`"value`":`"This work item was created by TFS Build on a build failure.`"},
]"

$url = "$ProjectName/_apis/wit/workitems/`$Bug?api-version=2.2"
Invoke-RestMethod $url -Method Patch -Body $bodyupdate -UseDefaultCredentials -ContentType "application/json-patch+json" -Verbose

Please note, you can add more robust error handling in the script depending upon your needs.

Step 3 - Configure build definition


Now create a new build definition, and make sure to:
  1. Add the folder containing your PowerShell script as a folder mapping in the Source Settings tab of your build definition.

  2. Specify the build script arguments properly. For instance, set "CreateCustomBugWI" to True. And specify the TFS project url for "TfsProjectForCustomBugWI" argument like "http://tfsServer:8080/tfs/Collection/Project"


Open your build definition to edit and specify the TFS version control path to the post-build script and also the TFS project url where you want the BUG WI to be created in case of build failure.

Step 4 - Verification


Now try to run your build, which you expect to fail. I deliberately introduced compile time errors and checked-in to make the TFS build fail. Once the build fails, you must see a new Bug created in your specified TFS project which can be verified from your browser.