Monday, April 6, 2009

CI - Failed tfs team build watcher. Part II - the code.

Code follows, as promised in the first installment on this subject.

What is required to develop?

1. Resolve the "windows" name of the build requestor in CI to the Display Name and email address. This requires a call to the TFS API, as name comes in the format "domain\windowsusername".
That is done by the ResolveUser build task: http://code.google.com/p/toolsdotnet/source/browse/trunk/Tools.Net/src/Tools.TeamBuild.Tasks/ResolveUser.cs

2. Keep a build and requestor names (email address) for the first broken build following the successful one. Again I decided not to use build API for that because of its chatty character of retrieving all build records and only then filtering through them. This is achieved by the BuildGateKeeper task: http://code.google.com/p/toolsdotnet/source/browse/trunk/Tools.Net/src/Tools.TeamBuild.Tasks/BuildGateKeeper.cs

Pardon me all the surrounding files for achieving the better test coverage, you can get the dll (.exe) straight here: http://cid-c651f6a9f36fb87d.skydrive.live.com/self.aspx/Public/tools.tfs.utils.exe. Reason for having it as an exe was ability to use ResolveUser task as a console utility.

First, get those tasks imported (change path to yours):

<UsingTask AssemblyFile="$(MSBuildExtensionsPath)\SitronicsTeamBuild.Quick\tools.tfs.utils.exe" TaskName="Tools.TeamBuild.Tasks.ResolveUser" />   
<UsingTask AssemblyFile="$(MSBuildExtensionsPath)\SitronicsTeamBuild.Quick\tools.tfs.utils.exe" TaskName="Tools.TeamBuild.Tasks.BuildGateKeeper" /> 

Then use ResolveUser in some place before the build:

<ResolveUser WindowsAccountName="$(RequestedFor)"> 
  <Output TaskParameter="DisplayName" PropertyName="Requestor" />
  <Output TaskParameter="MailAddress" PropertyName="MailAddress" />
</ResolveUser>

I used a default for our own tfs, but there is an extra parameter TfsUrl that is the best to set to the full url as http://tfsname:port.

Note that it is $(RequestedFor) that is used as $(RequestedBy) gives something like "System Build Account", while $(RequestedFor) gives user windows name. This way we are getting from mine domain\sdvoychenko to Stanislav Dvoychenko and "yours truly" email address.

If build is successful build I reward the lucky guy with the grateful email:

<CreateProperty          Value="Success" >
 <Output TaskParameter="Value" PropertyName="BuildStatus" />
</CreateProperty>
<BuildGateKeeper BuildStatus="$(BuildStatus)" StateFilePath="c:\Build\$(BuildDefinition).txt" RequestorMailAddress="$(MailAddress)" RequestorDisplayName="$(Requestor)">
</BuildGateKeeper>
<Mail SmtpServer="YourSmtpServer" IsBodyHtml="true" Username="xxx\xxx" Password="xxx" To="$(MailAddress)" CC="controlfreak@controlfreakdomain.com" From="build@controlfreakdomain.com" Body="Successful check-in! Your checkin to SP5 Dev branch was built successfuly. Check-in number and comments to be provided soon. Build log: %3CBR%2F%3E $(DropLocation)\$(BuildNumber)\BuildLog.txt;" Subject="Your checkin to SP5 Dev branch was built successfuly."></Mail>           

What BuildGateKeeper does at this moment is to delete the state file (if any), that keeps exactly who was the first to break the build.
After that, email sending follows for which you might need to add the following usage if you have not been using community tasks (http://msbuildtasks.tigris.org/) in your project already:

<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />

Now, lets see what to do if build breaks.

<Copy Condition="Exists('$(MSBuildProjectDirectory)\build.errors.txt')"                SourceFiles="$(MSBuildProjectDirectory)\build.errors.txt" DestinationFolder="$(DropLocation)\$(BuildNumber)\" />
<ReadLinesFromFile Condition="Exists('$(DropLocation)\$(BuildNumber)\build.errors.txt')"            File="$(DropLocation)\$(BuildNumber)\build.errors.txt" >
  <Output TaskParameter="Lines"  ItemName="BuildErrorLines"/>
</ReadLinesFromFile>
<CreateProperty Value="Failure" >
  <Output TaskParameter="Value" PropertyName="BuildStatus" />
</CreateProperty>
<BuildGateKeeper BuildStatus="$(BuildStatus)" StateFilePath="c:\Build\$(BuildDefinition).txt" RequestorMailAddress="$(MailAddress)" RequestorDisplayName="$(Requestor)">
  <Output TaskParameter="BreakerMailAddress" PropertyName="BreakerMailAddress" />
  <Output TaskParameter="BreakerDisplayName" PropertyName="BreakerDisplayName" />
  <Output TaskParameter="BreakTimeStamp" PropertyName="BreakTimeStamp" />
</BuildGateKeeper>
<Mail SmtpServer="YourSmtpServer" IsBodyHtml="true" Username="xxx\xxx" Password="xxx" To="$(MailAddress)" CC="controlfreak@controlfreakdomain.com;$(BreakerMailAddress)" From="build@controlfreakdomain.com" Body="You got this email message because the code you checked in recently was not compiled successfully during verification build. Changeset number and comments to be provided soon. &lt;br/&gt; The build was broken first by $(BreakerDisplayName) at $(BreakTimeStamp). &lt;br/&gt;&lt;br/&gt; Errors log: &lt;br/&gt; &lt;b&gt; @(BuildErrorLines) &lt;/b&gt; &lt;br/&gt; &lt;br/&gt; More detailed info: &lt;a href='file:///$(DropLocation)\$(BuildNumber)\BuildLog.txt' &gt; $(DropLocation)\$(BuildNumber)\BuildLog.txt &lt;/a &gt;. &lt;br/&gt; If you believe this informatin is false positive or need help with fixing check-in, please reply to the Dvoychenko Stanislav." Subject="FAILURE! Build - $(BuildNumber) - changes your checked in were not compiled successfuly."></Mail>

The line I owe here is an update to the .rsp build file in the BuildTypes folder. That is to log build errors only to the separate file. I'll add this snippet asap.
What we do then is to copy that error file to the drop location and then we read its lines into the $(BuildErrorLines) property which we use then in the body of our email.

We then use BuildGateKeeper to create a break state file if there is no such yet. If there is already one, then task uses it to populate Breaker details which we use again in our Mail community task to send the email to the check-in initiator and CC to the guy who First Broke The Build (and the control freak who is me by the way:).

This is very spartan and I'll share the improvements as I progress in the evenings :).

Happy hunting my fellow control freaks!

P.S. Full solution including nunit tests is available here:  http://code.google.com/p/toolsdotnet/source/browse/trunk/Tools.Net/Tools.Build.sln

No comments: