Executing psake build scripts from TeamCity

I came across an issue with a few of our TeamCity build configurations the other day; We had a situation where TeamCity was reporting ‘Success’ instead of ‘Failure’, lucky we spotted this early.

Executing psake build scripts from TeamCity

Post2-TeamCityIncorrectBuildStatus

Post2-TeamCityIncorrectExitCode

As can be seen above the build is failing with the following zero exit code.

[code language="bash"]
`default.ps1:Error executing command: msbuild $sln_file /t:Build /p:Configuration=Release /v:q [17:15:08]: Process exited with code 0`
[/code]

I had a sniff around and came to the conclusion the problem was in the way we were calling our psake build script from within a batch file. After a bit of googling I found this is a common stumbling block, there were some rather confusing and over the top approaches but I finally found this very useful article, this post builds on this approach and adds a bit more detail.

So we identified we were doing the following in our `build.bat`:

[code language="powershell"]powershell.exe -command "& {.build.ps1 Package}"[/code]

This is potentially wrong due to the following reasons:

  1. PowerShell by default continues running scripts on errors
  2. The executing batch file `build.bat` was not trapping the error status returned from the psake PowerShell script `default.ps1`.
  3. We weren’t importing the psake PowerShell module and therefore the $psake hashtable variables weren’t getting set.

I didn’t need to do anything for point 1 as we had however already implemented the exec {} helper function which forces psake to throw an error.

To resolve point 2 I referred to Dusty’s article , split it out a bit more and implemented the following in the original batch file `build.bat` and new build.ps1 script respectively

build.bat
[code language="bash"]@echo off
powershell.exe -NoProfile -ExecutionPolicy unrestricted -command ".build.ps1 %1;exit $LASTEXITCODE"
[/code]

[code language="bash"]
if %ERRORLEVEL% == 0 goto OK
echo ##teamcity[buildStatus status='FAILURE' text='{build.status.text} in compilation']
exit /B %ERRORLEVEL%

:OK
[/code]

build.ps1

[code language="powershell"]
param([string]$task = "compile")
Import-Module '.libpsakepsake.psm1';
Invoke-psake -t $task;
if ($Error -ne '')
{
Write-Host "ERROR: $error" -fore RED;
exit $error.Count
}
Remove-Module psake -ea 'SilentlyContinue';
[/code]

The above do the following:

  • `build.bat` wraps `build.ps1` and executes it, passing the first parameter passed to the batch file in, i.e. the psake task.
  • `build.ps1` then starts by importing the psake PowerShell module (we have this in a relative path in source control).
  • `build.ps1` then invokes the default psake build script in the current working folder i.e. `default.ps1`, passing the first parameter passed to the batch file in as the psake task. `Default.ps1` obviously does all the build work for us.
  • the if block then simply checks the Powershell $error array and returns a count of the errors to `build.bat`
  • `build.bat` echo’s out a nice TeamCity buildStatus which then shows up in the TeamCity gui.
  • `build.bat` fails with the ERRORLEVEL returned by `build.ps1`.
  • finally the psake PowerShell module is removed.

Point 3 was resolved above by explicitly importing the psake PowerShell module. I also tapped the following into a PowerShell command line.

[code language="powershell"]Get-Help Invoke-psake –full[/code]

Which gives, amongst other detail, the following Output definition.

[code language="bash"].....
OUTPUTS
If there is an exception and '$psake.use_exit_on_error' -eq $true
then runs exit(1) to set the DOS lastexitcode variable
otherwise set the '$psake.build_success variable' to $true or $false depend
ing
on whether an exception was thrown

NOTES

When the psake module is loaded a variabled called $psake is created it
is a hashtable
containing some variables that can be used to configure psake:

$psake.use_exit_on_error = $false # determines if psake uses the "exi
t()" function when an exception occurs
$psake.log_error = $false # determines if the exception detai
ls are written to a file
$psake.build_success = $false # indicates that the current build
was successful
$psake.version = "4.00" # contains the current version of p
sake
$psake.build_script_file = $null # contains a System.IO.FileInfo for
the current build file
$psake.framework_version = "" # contains the framework version #
for the current build

$psake.use_exit_on_error and $psake.log_error are boolean variables tha
t can be set before you call Invoke-Psake.

You should see the following when you display the contents of the $psak
e variable right after importing psake

PS projects:psake> Import-Module .psake.psm1
PS projects:psake> $psake

Name Value
---- -----
version 4.00
build_script_file
use_exit_on_error False
build_success False
log_error False
framework_version
.....
[/code]

I therefore modified the `build.ps1` psake script above set the $psake.use_exit_on_error variable to true and forced it to always use the 64 bit version of the .NET 3.5 framework:

[code language="powershell"]param([string]$task = "compile")
Import-Module '.libpsakepsake.psm1';
#$psake
$psake.use_exit_on_error = $true
Invoke-psake -t $task -framework '3.5x64';
if ($Error -ne '')
{
Write-Host "ERROR: $error" -fore RED;
exit $error.Count
}
#$psake
Remove-Module psake -ea 'SilentlyContinue';
[/code]

Executing the batch file on the command line within my development environment was now returning the correct results. Therefore I purposefully checked in a dodgy change into our Source control system (TFS) and watched a TeamCity build kick off……

The correct error code and the buildStatus from `build.bat` displaying nicely.

 Post2-TeamCityCorrectBuildStatus

Post2-TeamCityCorrectExitCode

….much to my delight the build failed with the correct error code this time

[code language="bash"]
[13:47:47]: ControllersAccountController.cs(73,62): error CS1002: ; expected
[13:47:48]: default.ps1:Error executing command: & msbuild $sln_file /t:Build /p:Configuration=Release /v:q
[13:47:48]: ##teamcity[buildStatus status='FAILURE' text='{build.status.text} in compilation']
[13:47:48]: Process exited with code 1
[/code]

No more fibbing build status’ for us.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>