Custom build activity for TFS 2010 to send email with build details – Part 1
Team Foundation Server 2010 build service can now be customized using .NET v4.0 workflow activities. I was recently working on a requirement to generate an email after the successful build which provides basic information about the contents of the build. Here are some basic requirements for the activity.
- Send Email after the compilation and test runs
- Include list of changesets in the email
- Include list of associated work items with a changeset
- Include list of associated files with a changeset
Here is a quick snapshot of the email that needs to be generated automatically.
Most of this information can be obtained from Team Foundation Server Object Model class Changeset. It gives you access to the associated work items and files (changes) which are part of the changeset. The default process template in VS includes a step to associate changesets and work items to the build, this step will give us access to the associated changesets. First step in the process is to create an activity class that inherits from System.Activities.CodeActivity. Second step is to add references to Microsoft.TeamFoundation.Build.Client.dll, Microsoft.TeamFoundation.Client.dll, Microsoft.TeamFoundation.VersionControl.Client.dll and Microsoft.TeamFoundation.WorkItemTracking.Client.dll. Most of these references will allow you to access, the build details, changeset information and work item information which can be found in C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0\ folder. You also should add reference to Microsoft Anti-XSS library to protect from XSS vulnerabilities. So here is how the class will look like so far.
using System.ComponentModel;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Activities;
using System.Net.Mail;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.Security.Application;
namespace ISTBuildActivityLibrary
{
[BuildActivity(HostEnvironmentOption.Agent)]
public sealed class SendEmailActivity : CodeActivity
{
#region Activity Execution Logic
protected override void Execute(CodeActivityContext context)
{
}
}
}
The next step is to define the properties that you need for the activity, these will help you retrieve the changeset details, smtp server and mail target information.
#region Message Properties
[BrowsableAttribute(true)]
[CategoryAttribute(MessagePropertiesCategory)]
public InArgument<IBuildDetail> BuildDetail { get; set; }
[BrowsableAttribute(true)]
[CategoryAttribute(MessagePropertiesCategory)]
public InArgument<IList<Changeset>> BuildAssociatedChangesets { get; set; }
[BrowsableAttribute(true)]
[CategoryAttribute(MessagePropertiesCategory)]
public InArgument<string> SmtpServer { get; set; }
[BrowsableAttribute(true)]
[CategoryAttribute(MessagePropertiesCategory)]
public InArgument<string> Subject { get; set; }
[BrowsableAttribute(true)]
[CategoryAttribute(MessagePropertiesCategory)]
public InArgument<string> MailFrom { get; set; }
[BrowsableAttribute(true)]
[CategoryAttribute(MessagePropertiesCategory)]
public InArgument<string> Mailto { get; set; }
#endregion
Please note the BuildDetail property when set in the process workflow gives you access to the build information such as build number, drop location, start time, team project, requested user, label name, and more which can be part of the email to provide the summary of the build. BuildAssociatedChangesets provides access to the changesets associated with the build, the default workflow process includes variables which store this information, thus the activity does not have to get it again from the TFS server. The rest of properties provide information such as SMTP Server, Subject, Mail From and Mail To which are self explanatory. Enough with the properties lets look at the activity execution code to see how the mail is constructed.
protected override voidExecute(CodeActivityContext context)
{
try
{
IBuildDetail buildInformation = context.GetValue(this.BuildDetail);
StringBuilder sbMailHtml = newStringBuilder();
sbMailHtml.Append(Resources.EmailHtml);
IList<Changeset> associatedChangesets = context.GetValue(this.BuildAssociatedChangesets);
StringBuilder changesetHtml = newStringBuilder();
foreach (Changeset changeset inassociatedChangesets)
{
changesetHtml.AppendLine("<table align=\"center\" cellpadding=\"4\" cellspacing=\"0\" class=\"style1\" style=\'border:solid #7BA0CD 1.0pt\'>');
changesetHtml.AppendLine("<tr><td bgcolor=\"#4F81BD\" class=\"style2\" colspan=\"2\">");
changesetHtml.AppendLine("<a target=\"_new\" href=\"https://server:8080/tfs/web/UI/Pages/Scc/ViewChangeset.aspx?cs="+ changeset.ChangesetId.ToString() + "\"><font color=\"white\">Changeset #"+ changeset.ChangesetId.ToString() + "</font></a>");
changesetHtml.AppendLine("<tr><td colspan=\"2\">Changeset checked in by <b>"+ AntiXss.HtmlEncode(changeset.Committer) + "</b> with comments:<br /><i>"+ AntiXss.HtmlEncode(changeset.Comment) + "</i></td></tr>");
changesetHtml.AppendLine("<tr><td colspan=\"2\">");
IList<WorkItem> workitems = changeset.WorkItems;
changesetHtml.AppendLine("<table align=\"center\" cellpadding=\"4\" cellspacing=\"0\" class=\"style1\" style=\'border:solid #7BA0CD 1.0pt\'><tr><td bgcolor=\'#4F81BD\' class=\'style3\' colspan=\'5\'>Associated Work Items: '+ workitems.Count.ToString() + "</td></tr>");
changesetHtml.AppendLine("<tr><td>Type</td><td>Id</td><td>Title</td><td>State</td><td>Reason</td></tr>");
foreach (WorkItem workitem in workitems)
{
changesetHtml.AppendLine("<tr><td>" + workitem.Type.Name + "</td>");
changesetHtml.AppendLine("<td><a target=\"_new\" href=\"https://server:8080/tfs/web/UI/Pages/WorkItems/WorkItemEdit.aspx?id=" + workitem.Id + "\">" + workitem.Id + "</a></td>");
changesetHtml.AppendLine("<td>" + AntiXss.HtmlEncode(workitem.Title) + "</td>");
changesetHtml.AppendLine("<td>" + workitem.State + "</td>");
changesetHtml.AppendLine("<td>" + workitem.Reason + "</td></tr>");
}
changesetHtml.AppendLine("</table></td></tr>");
changesetHtml.AppendLine("<tr><td colspan=\"2\"> </td></tr>");
List<Change> files = GetFilesAssociatedWithBuild(changeset.VersionControlServer, changeset.ChangesetId);
changesetHtml.AppendLine("<tr><td colspan=\"2\"><table align=\"center\" cellpadding=\"4\" cellspacing=\"0\" class=\"style1\" style=\'border:solid #7BA0CD 1.0pt\'><tr><td bgcolor=\'#4F81BD\' class=\'style3\' colspan=\'2\'>Associated Files: ' + files.Count + "</td></tr>");
foreach (Change file in files)
{
changesetHtml.AppendLine("<tr><td>" + AntiXss.HtmlEncode(file.Item.ServerItem) + "</td><td>" + file.ChangeType.ToString() + "</td></tr>");
}
changesetHtml.AppendLine("</table></td></tr>");
changesetHtml.AppendLine("</table>");
changesetHtml.AppendLine("<br />");
}
sbMailHtml.Replace("<@ChangesetsHtml>", changesetHtml.ToString());
sbMailHtml.Replace("<@DateTime>", buildInformation.FinishTime.ToString());
sbMailHtml.Replace("<@BuildNumber>", buildInformation.BuildNumber);
sbMailHtml.Replace("<@DropPath>", buildInformation.DropLocation);
SmtpClient objSmtp = new SmtpClient(this.SmtpServer.Get(context));
objSmtp.UseDefaultCredentials = true;
MailMessage objMsg = new MailMessage();
objMsg.From = new MailAddress(this.MailFrom.Get(context));
objMsg.To.Add(this.Mailto.Get(context));
objMsg.Subject = this.Subject.Get(context);
objMsg.IsBodyHtml = true;
objMsg.Body = sbMailHtml.ToString();
objSmtp.Send(objMsg);
sbMailHtml.Clear();
sbMailHtml = null;
}
catch
{
throw;
}
}
private static List<Change> GetFilesAssociatedWithBuild(VersionControlServer versionControlServer, int changesetId)
{
List<Change> files = new List<Change>();
Changeset changeset = versionControlServer.GetChangeset(changesetId);
if (changeset.Changes != null)
{
foreach (Change changedItem in changeset.Changes)
{
files.Add(changedItem);
}
}
changeset = null;
versionControlServer = null;
return files;
}
GetFilesAssociatedWIthBuild returns the list of files that have changed for a specific changeset. Execute is the main method that constructs the HTML from the changeset data and it encodes the string data that is coming from various sources to mitigate any XSS issues. Attached to this post is the full source code of the activity. In the next post I will outline the way to test and integrate the activity in to your build definition.
Thanks
Anil RV
Comments
- Anonymous
September 02, 2010
This is almost exactly what I am looking to do for our team. Currently TFS only notifies via alert subscription when a build completes. We would like to have a notification when a build is started so that testers can be aware that the deployment site will soon be reset.I'll be watching for your next post for testing and integrating the activity to a build definition. Hopefully this will be able to be integrated into a build definition that uses the upgrade template.Thanks so much for this information. - Anonymous
October 29, 2010
Thanks for posting this article. Exactly what i was looking for.Just a note for other folks:Had to down load the AntiXSSLibrary. Had to change the AntiXss.HtmlEncode to Microsoft.Security.Application.Encoder.HtmlEncode - Anonymous
January 19, 2011
Not a bad article but you could have pointed out the need to include antixss and there are a few syntax errors but thanks! - Anonymous
February 20, 2011
The comment has been removed - Anonymous
February 21, 2011
I've figured it out....Uri tfsUri = new Uri("http://tfs-server:8080/tfs/product");TfsTeamProjectCollection server;IBuildServer buildServer;server = new TfsTeamProjectCollection(tfsUri);server.EnsureAuthenticated();buildServer = (IBuildServer) server.GetService(typeof (IBuildServer));//IBuildDefinition[] buildDefinitions = buildServer.QueryBuildDefinitions("product");//foreach (IBuildDefinition buildDefinition in buildDefinitions)//{// Console.WriteLine(buildDefinition.Name);//}IBuildDetail[] nightlyBuilds = buildServer.QueryBuilds("product", "NightlyBuildAndTest");IBuildDetail mostRecent = nightlyBuilds[nightlyBuilds.GetUpperBound(0)];TestManagementService tcm = (TestManagementService)server.GetService(typeof(TestManagementService));IEnumerable<ITestRun> allTestRuns =
foreach (ITestRun testRun in allTestRuns){(IEnumerable<ITestRun>) (tcm.GetTeamProject("product").TestRuns.ByBuild(mostRecent.Uri));
}ITestCaseResultCollection testResults = testRun.QueryResults();foreach (ITestCaseResult testResult in testResults){ //ITestIterationResultCollection iterations = testResult.Iterations; //if (testResult.Outcome != TestOutcome.Passed) //{ // IAttachmentCollection tcAtts = testResult.Attachments; // foreach (ITestAttachment tcAtt in tcAtts) // { // if (tcAtt.Name.StartsWith("tmiResult", StringComparison.OrdinalIgnoreCase)) // { // //download the .trx file, you could also use ITestAttachment.DownloadToArray // string tmpFileName = Path.Combine(Environment.GetEnvironmentVariable("Temp"), testResult.TestCaseTitle + tcAtt.Name); // tcAtt.DownloadToFile(tmpFileName); // String tcAttContents = File.ReadAllText(tmpFileName); // //parse xml to get Owner // } // } //} Console.WriteLine("{0} : {1} - {2}", testResult.TestCaseTitle, testResult.Outcome, testResult.ErrorMessage);}