Azure Mobile Service deployment with Team City part 3. Build Azure Mobile Service with PowerShell using psake

Tags: CI, NuGet, Powershell, TeamCity

TeamCity is a very powerful tool and you can take advantage of all its configuration options to create your CI and CD. Although TeamCity provides you with almost everything you need, like restoring packages, update version, build, running unit tests, etc. I’m going to write all these steps in a PowerShell script. It is easier to use what TeamCity offers from the start but when the complexity of building and deploying increases it will be difficult to maintain.

Having the deployment process as a set of scripts will give you some advantages, like:

  • Run and test the deployment process on your machine
  • Evolve the deployment system incrementally
  • Ability to test every step in isolation
  • Checking changes in source control and ability to revert to previous versions if needed

The scripts will be written in PowerShell and I will use psake which is a PowerShell tool to provide task-oriented dependencies.

Initially we’ll start with three scripts:

  • local.ps1 – we’ll use this script to run the entire set of script on local machine; it contains default parameters and it executes run-build.ps1
  • run-build.ps1 – the script is called by TeamCity providing all the necessary parameters. It invokes the build.ps1 script using psake module
  • builid.ps1 – contains all the steps necessary to build the project

 

Tools

First we need to get psake scripts from the git releases page and copy them to the Tools folder. We need the nuget.exe as well. Download it from Command-Line Utility URL . Copy nuget.exe in the Tools folder.

 

Local script

All the scripts are located in the Scripts folder.

local.ps1 script clears the screen and executes the run-build.ps1 script. It contains a few default parameters. One of the parameter is debugConfiguration. We use that for debugging purposes. For example when we work on the deployment task we might not want to run all the tasks which are time consuming, like restore packages so we’ll ignore it.

 

Run-build script

The run-build.ps1 script imports psake module and executes build.ps1 script in the context of the psake module. It invokes the script with parameters which don’t have default values and properties which have default values.

 

Build script

All the steps before running the build and the build itself are done in this build.ps1 script.

We’ll start with changing how psake prints out the tasks it runs. This will make a better separation between tasks and will be easier to read.

FormatTaskName "$([Environment]::NewLine)==================== $(Get-Date -format T) - Executing {0} ===================="

We’ll use the precondition parameter to conditionally run a task. For example, if we don’t want to restore packages we’ll use the $debugConfiguration.restorePackage parameter.

Task RestoreNuGetPackages -Depends Clean -Precondition {return $debugConfiguration -eq $null -or $debugConfiguration.restorePackage } {

The task will run when precondition returns true. I check for null value of the debugConfiguration variable because it will be  null when running from TeamCity so we don’t want to skip the step.

Bear in mind that if the task execution depends on another task then that task will not run if precondition of the task in execution returns false.

We’ll start with adding the default task.

Task Default -Depends DeployPackage

 

Clean

First of all we need to clean everything. For that we’ll build the solution with Clean target and also delete the packages folder.

Task Clean -Precondition { return $debugConfiguration -eq $null -or $debugConfiguration.clean } {
 Exec {
        msbuild $solutionToBuild /t:Clean /verbosity:$verbosity /nologo /p:Configuration=$config /p:VisualStudioVersion=12.0
    }

  if (Test-Path "..\Source\packages") {
     Remove-Item "..\Source\packages" -Recurse -Force -ErrorAction Stop
  }
  else {
    "Didn't find the 'packages' folder."
  }
}

 

Restore packages

Every single build needs to restore all the packages, in this way we make sure that packages are not missing on the build machine.

Task RestoreNuGetPackages -Depends Clean -Precondition { return $debugConfiguration -eq $null -or $debugConfiguration.restorePackage } {
    if (!(Test-Path $nugetExe)){
        throw "nuget.exe could not be found on this machine. Please check: $nugetExe"
    }
    Exec {
      & $nugetExe restore $solutionToBuild
    }
}

Our mobile service project references a NuGet package hosted on the TeamCity machine. But the NuGet feed requires authentication. In order to restore that package on the TeamCity machine during the build process we need to allow the restore process to authenticate to that NuGet feed. There is a command line we need to run on the TeamCity VM.

Login on the TeamCity server and make sure that you use the same account you used when you installed the TeamCity agent. It’s important that the agent runs under the same account. We configured the agent in step 1 where I mentioned that. NuGet should add the configuration file (NuGet.config) in the same user’s folder.

For example, if you run the agent under the ‘teamcity’ user name then when you add the NuGet source as below (run the below command) it will create or update the file in this location:

C:\Users\teamcity\AppData\Roaming\NuGet\NuGet.Config

This is the command line you need to run:

NuGet.exe Sources Add –Name [GIVE_A_NAME] -Source [TEAMCITY_FEED] –UserName [USER_NAME] –Password [PASSWORD]

Sources – TeamCity NuGet feed. Check the configure NuGet server post

UserName – user name you provided when you setup TeamCity. You can create a new user name just for this purpose but in this case you’ll need to run the agent under this user

Password – user name’s password

 

Update assembly version

There are different methods to update the assembly version. You can keep the version in one AssemblyInfo.cs file and add that file as a link to all the projects but I don’t like this idea because you can forget to do that and some of your libraries will not have the version updated. Instead of that I iterate through all the projects and update the version for every single AssemblyInfo.cs file.

Task UpdateVersion -Precondition { return $debugConfiguration -eq $null -or $debugConfiguration.updateVersion } {
    (Get-ChildItem -Path $sourceDirectory -Filter AssemblyInfo.cs -Recurse) |
    ForEach-Object {
      (Get-Content $_.FullName) |
        ForEach-Object {
          $_ -replace 'AssemblyVersion.+$',"AssemblyVersion(`"$version`")]" `
          -replace 'AssemblyFileVersion.+$',"AssemblyFileVersion(`"$version`")]"
        } |
        Out-File $_.FullName
    }
}

 

Build

The build process should create a package which will be deployed to Azure. In order to do this we’ll use DeployOnBuild=true and PublishProfile=PublishProfile.xml parameters.

The PublishProfile.pubxml file doesn’t exist. We need to generate it. In order to do this we have the pubxml.template file in the Scripts folder. It contains the following xml:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <WebPublishMethod>FileSystem</WebPublishMethod>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish />
    <LaunchSiteAfterPublish>False</LaunchSiteAfterPublish>
    <ExcludeApp_Data>True</ExcludeApp_Data>
    <publishUrl>{0}</publishUrl>
    <DeleteExistingFiles>True</DeleteExistingFiles>
  </PropertyGroup>
</Project>

You can change it if you need to. We’re interested in the publishUrl value – we need to populate it during the build process with the package location. If you’re going to have a fixed package location then you can hard-code it but in our case the location will be driven by the TeamCity. We’ll generate the package in the “[Root]\Artifacts” folder. Root is the folder where our Scripts and Source folders are located.

The PublishProfile.pubxml file is generated in the method “GetPublishProfile” and will be saved in “[Root]\Artifacts” folder. Later it is overwritten by the build with the package. To some extend this simulates the same process you have when you deploy from Visual Studio.

function GetPublishProfile() {
    # Get the publish xml template and generate the .pubxml file
    $scriptPath = Split-Path -parent $PSCommandPath

    if (Test-Path $artifactDirectory) {
        Remove-Item $artifactDirectory -Recurse -Force -ErrorAction Stop
    }

    [String]$template = Get-Content $scriptPath\pubxml.template

    mkdir $artifactDirectory | Out-Null
    $xml = $template -f $artifactDirectory
    $outputPublishProfile = Join-Path $artifactDirectory "PublishProfile.pubxml"
    $xml | Out-File -Encoding utf8 -FilePath $outputPublishProfile

    return $outputPublishProfile
}
 
Task Build -Depends RestoreNuGetPackages, UpdateVersion {
    $publishProfile = GetPublishProfile 

    Exec {
        msbuild $projectToBuild /p:DeployOnBuild=true /p:PublishProfile=$publishProfile /verbosity:$verbosity /nologo /p:Configuration=$config /p:VisualStudioVersion=12.0
    }
}

 

Unit Tests

All the Unit Tests were implemented using xunit. First we need to build the solution which will create the Test*.dll files. After that we’ll run every Test*.dll file by xunit.exe.

xunit.exe file is added as a NuGet package. In order to do that we need to add this to our Test project, otherwise it will create a .nuget folder with packages.config file and we’ll put it under the solution folder. You can add it as I NuGet package for the solution but I don't really like it for small projects.

  <package id="xunit.runner.console" version="2.0.0" />

The full source code of the Unit Test

Task Test -Depends Build {
  if (!(Test-Path $xunitConsole))
  {
    throw "xunit.console.exe does not exist. Please check $xunitConsole file."
  }

   "************* Build solution [$solutionToBuild] for Unit Testing *************"
  # Need to run the solution to build all the tests. The Build Task run only the csproj file
  exec {
    msbuild $solutionToBuild /verbosity:m /p:Configuration=$config /p:VisualStudioVersion=12.0 /nologo
  }

  $assembliesToTest = (Get-ChildItem "$sourceDirectory" -Recurse -Include "*Test*.dll" -Name | Select-String "bin")

  $atLeastOneTestRun = $false

  "************* Running Unit Tests *************"
  foreach ($testFile in $assembliesToTest){
    "*************** Testing $testFile ***************"
    $fullNameTestFile = Join-Path $sourceDirectory $testFile
    
    & $xunitConsole $fullNameTestFile
    $atLeastOneTestRun = $true
  }

  if ($atLeastOneTestRun -eq $false){
    Write-Output "Unit Tests didn't run!"
    exit(1)
  }
}

 

Now we can build and test our project. In the next post I will explain how to deploy an Azure Mobile Service.

< Previous post

Comments

comments powered by Disqus