My customer requires bug resolution to be approved and tracked. To minimize the overhead for developers I implemented a TFS 2010 server-side plug-in to automatically create a child resolution task for the bug when the “CCB” field is set to approved. The CCB field is a custom field. I also added the story points field to the bug WIT for sizing purposes. Redundant tasks will not be created unless the bug title is changed or the prior task is closed. The program writes an audit trail to a log file visible in the TFS Admin Console Log view. Here’s the code. BugAutoTask.cs /* SPECIFICATION
* When the CCB field on the bug is set to approved, create a child task where the task:
* name = Resolve bug [ID] - [Title of bug]
* assigned to = same as assigned to field on the bug
* same area path
* same iteration path
* activity = Bug Resolution
* original estimate = bug points
*
* The source code is used to build a dll (Ows.TeamFoundation.BugAutoTaskCreation.PlugIns.dll),
* which needs to be copied to
* C:\Program Files\Microsoft Team Foundation Server 2010\Application Tier\Web Services\bin\Plugins
* on ALL TFS application-tier servers.
*
* Author: Bob Hardister.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Text;
using System.Diagnostics;
using System.Linq;
using Microsoft.TeamFoundation.Common;
using Microsoft.TeamFoundation.Framework.Server;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Server;
using Microsoft.TeamFoundation.Client;
using System.Collections;
namespace BugAutoTaskCreation
{
public class BugAutoTask : ISubscriber
{
public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext requestContext,
NotificationType notificationType,
object notificationEventArgs,
out int statusCode,
out string statusMessage,
out ExceptionPropertyCollection properties)
{
statusCode = 0;
properties = null;
statusMessage = String.Empty;
// Error message for for tracing last code executed and optional fields
string lastStep = "No field values found or set ";
try
{
if ((notificationType == NotificationType.Notification) &&
(notificationEventArgs.GetType() == typeof(WorkItemChangedEvent)))
{
WorkItemChangedEvent workItemChange = (WorkItemChangedEvent)notificationEventArgs;
// see ConnectToTFS() method below to select which TFS instance/collection
// to connect to
TfsTeamProjectCollection tfs = ConnectToTFS();
WorkItemStore wiStore = tfs.GetService<WorkItemStore>();
lastStep = lastStep + ": connection to TFS successful ";
// Get the work item that was just changed by the user.
WorkItem witem = wiStore.GetWorkItem(workItemChange.CoreFields.IntegerFields[0].NewValue);
lastStep = lastStep + ": retrieved changed work item, ID:" + witem.Id + " ";
// Filter for Bug work items only
if (witem.Type.Name == "Bug")
{
// DEBUG
lastStep = lastStep + ": changed work item is a bug ";
// Filter for CCB (i.e. Baseline Status) field set to approved only
bool BaselineStatusChange = false;
if (workItemChange.ChangedFields != null)
{
ProcessBugRevision(ref lastStep, workItemChange,
wiStore, ref witem, ref BaselineStatusChange);
}
}
}
}
catch (Exception e)
{
Trace.WriteLine(e.Message);
Logger log = new Logger();
log.WriteLineToLog(MsgLevel.Error, "Application error: " + lastStep + " - " +
e.Message + " - " + e.InnerException);
}
statusCode = 1;
statusMessage = "Bug Auto Task Evaluation Completed";
properties = null;
return EventNotificationStatus.ActionApproved;
}
// PRIVATE METHODS
private static void ProcessBugRevision(ref string lastStep,
WorkItemChangedEvent workItemChange,
WorkItemStore wiStore, ref WorkItem witem,
ref bool BaselineStatusChange)
{
foreach (StringField field in workItemChange.ChangedFields.StringFields)
{
// DEBUG
lastStep = lastStep + ": last changed field is - " + field.Name + " ";
if (field.Name == "Baseline Status")
{
lastStep = lastStep + ": retrieved bug baseline status field value, bug ID:" +
witem.Id + " ";
BaselineStatusChange = (field.NewValue != field.OldValue);
if ((BaselineStatusChange) && (field.NewValue == "Approved"))
{
// Instanciate logger
Logger log = new Logger();
// *** Create resolution task for this bug ***
// *******************************************
// Get the team project and selected field values of the bug work item
Project teamProject = witem.Project;
int bugID = witem.Id;
string bugTitle = witem.Fields["System.Title"].Value.ToString();
string bugAssignedTo = witem.Fields["System.AssignedTo"].Value.ToString();
string bugAreaPath = witem.Fields["System.AreaPath"].Value.ToString();
string bugIterationPath = witem.Fields["System.IterationPath"].Value.ToString();
string bugChangedBy = witem.Fields["System.ChangedBy"].OriginalValue.ToString();
string bugTeamProject = witem.Project.Name;
lastStep = lastStep + ": all mandatory bug field values found ";
// Optional fields
Field bugPoints = witem.Fields["Microsoft.VSTS.Scheduling.StoryPoints"];
if (bugPoints.Value != null)
{
lastStep = lastStep + ": all mandatory and optional bug field values found ";
}
// Initialize child resolution task title
string childTaskTitle = "Resolve bug " + bugID + " - " + bugTitle;
// At this point I can check if a resolution task (of the same name)
// for the bug already exist
// If so, do not create a new resolution task
bool createResolutionTask = true;
WorkItem parentBug = wiStore.GetWorkItem(bugID);
WorkItemLinkCollection links = parentBug.WorkItemLinks;
foreach (WorkItemLink wil in links)
{
if (wil.LinkTypeEnd.Name == "Child")
{
WorkItem childTask = wiStore.GetWorkItem(wil.TargetId);
if ((childTask.Title == childTaskTitle) && (childTask.State != "Closed"))
{
createResolutionTask = false;
log.WriteLineToLog(MsgLevel.Info, "Team project " + bugTeamProject + ": "
+ bugChangedBy + " - set the CCB field to \"Approved\" for bug, ID: "
+ bugID + ". Task not created as open one of the same name already exist, ID:"
+ childTask.Id);
}
}
}
if (createResolutionTask)
{
// Define the work item type of the new work item
WorkItemTypeCollection workItemTypes = wiStore.Projects[teamProject.Name].WorkItemTypes;
WorkItemType wiType = workItemTypes["Task"];
// Setup the new task and assign field values
witem = new WorkItem(wiType);
witem.Fields["System.Title"].Value = "Resolve bug " + bugID + " - " + bugTitle;
witem.Fields["System.AssignedTo"].Value = bugAssignedTo;
witem.Fields["System.AreaPath"].Value = bugAreaPath;
witem.Fields["System.IterationPath"].Value = bugIterationPath;
witem.Fields["Microsoft.VSTS.Common.Activity"].Value = "Bug Resolution";
lastStep = lastStep + ": all mandatory task field values set ";
// Optional fields
if (bugPoints.Value != null)
{
witem.Fields["Microsoft.VSTS.Scheduling.OriginalEstimate"].Value = bugPoints.Value;
lastStep = lastStep + ": all mandatory and optional task field values set ";
}
// Check for validation errors before saving the new task and linking it to the bug
ArrayList validationErrors = witem.Validate();
if (validationErrors.Count == 0)
{
witem.Save();
// Link the new task (child) to the bug (parent)
var linkType = wiStore.WorkItemLinkTypes[CoreLinkTypeReferenceNames.Hierarchy];
// Fetch the work items to be linked
var parentWorkItem = wiStore.GetWorkItem(bugID);
int taskID = witem.Id;
var childWorkItem = wiStore.GetWorkItem(taskID);
// Add a new link to the parent relating the child and save it
parentWorkItem.Links.Add(new WorkItemLink(linkType.ForwardEnd, childWorkItem.Id));
parentWorkItem.Save();
log.WriteLineToLog(MsgLevel.Info, "Team project " + bugTeamProject + ": "
+ bugChangedBy + " - set the CCB field to \"Approved\" for bug, ID:"
+ bugID + ", which automatically created child resolution task, ID:"
+ taskID);
}
else
{
log.WriteLineToLog(MsgLevel.Error, "Error in creating bug resolution child task for bug ID:" + bugID);
foreach (Field taskField in validationErrors)
{
log.WriteLineToLog(MsgLevel.Error, " - Validation Error in task field: "
+ taskField.ReferenceName);
}
}
}
}
}
}
}
private TfsTeamProjectCollection ConnectToTFS()
{
// Connect to TFS
string tfsUri = string.Empty;
// Production TFS instance production collection
tfsUri = @"xxxx";
// Production TFS instance admin collection
//tfsUri = @"xxxxx";
// Local TFS testing instance default collection
//tfsUri = @"xxxxx";
TfsTeamProjectCollection tfs = new TfsTeamProjectCollection(new System.Uri(tfsUri));
tfs.EnsureAuthenticated();
return tfs;
}
// HELPERS
public string Name
{
get
{
return "Bug Auto Task Creation Event Handler";
}
}
public SubscriberPriority Priority
{
get
{
return SubscriberPriority.Normal;
}
}
public enum MsgLevel { Info, Warning, Error };
public Type[] SubscribedTypes()
{
return new Type[1] { typeof(WorkItemChangedEvent) };
}
}
}
Logger.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace BugAutoTaskCreation
{
class Logger
{
// fields
private string _ApplicationDirectory = @"C:\ProgramData\Microsoft\Team Foundation\Server Configuration\Logs";
private string _LogFileName = @"\CFG_ACCT_AT_OWS_BugAutoTaskCreation.log";
private string _LogFile;
private string _LogTimestamp = DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss");
private string _MsgLevelText = string.Empty;
// default constructor
public Logger()
{
// check for a prior log file
FileInfo logFile = new FileInfo(_ApplicationDirectory + _LogFileName);
if (!logFile.Exists)
{
CreateNewLogFile(ref logFile);
}
}
// properties
public string ApplicationDirectory
{
get
{ return _ApplicationDirectory; }
set
{ _ApplicationDirectory = value; }
}
public string LogFile
{
get
{
_LogFile = _ApplicationDirectory + _LogFileName;
return _LogFile;
}
set
{ _LogFile = value; }
}
// PUBLIC METHODS
public void WriteLineToLog(BugAutoTask.MsgLevel msgLevel, string logRecord)
{
try
{
// set msgLevel text
if (msgLevel == BugAutoTask.MsgLevel.Info)
{
_MsgLevelText = "[Info @" + MsgTimeStamp() + "] ";
}
else if (msgLevel == BugAutoTask.MsgLevel.Warning)
{
_MsgLevelText = "[Warning @" + MsgTimeStamp() + "] ";
}
else if (msgLevel == BugAutoTask.MsgLevel.Error)
{
_MsgLevelText = "[Error @" + MsgTimeStamp() + "] ";
}
else
{
_MsgLevelText = "[Error: unsupported message level @" + MsgTimeStamp() + "] ";
}
// write a line to the log file
StreamWriter logFile = new StreamWriter(_ApplicationDirectory + _LogFileName, true);
logFile.WriteLine(_MsgLevelText + logRecord);
logFile.Close();
}
catch (Exception)
{
throw;
}
}
// PRIVATE METHODS
private void CreateNewLogFile(ref FileInfo logFile)
{
try
{
string logFilePath = logFile.FullName;
// write the log file header
_MsgLevelText = "[Info @" + MsgTimeStamp() + "] ";
string cpu = string.Empty;
if (Environment.Is64BitOperatingSystem)
{
cpu = " (x64)";
}
StreamWriter newLog = new StreamWriter(logFilePath, false);
newLog.Flush();
newLog.WriteLine(_MsgLevelText + "====================================================================");
newLog.WriteLine(_MsgLevelText + "Team Foundation Server Administration Log");
newLog.WriteLine(_MsgLevelText + "Version : " + "1.0.0 Author: Bob Hardister");
newLog.WriteLine(_MsgLevelText + "DateTime : " + _LogTimestamp);
newLog.WriteLine(_MsgLevelText + "Type : " + "OWS Custom TFS API Plug-in");
newLog.WriteLine(_MsgLevelText + "Activity : " + "Bug Auto Task Creation for CCB Approved Bugs");
newLog.WriteLine(_MsgLevelText + "Area : " + "Build Explorer");
newLog.WriteLine(_MsgLevelText + "Assembly : " + "Ows.TeamFoundation.BugAutoTaskCreation.PlugIns.dll");
newLog.WriteLine(_MsgLevelText + "Location : " + @"C:\Program Files\Microsoft Team Foundation Server 2010\Application Tier\Web Services\bin\Plugins");
newLog.WriteLine(_MsgLevelText + "User : " + Environment.UserDomainName + @"\" + Environment.UserName);
newLog.WriteLine(_MsgLevelText + "Machine : " + Environment.MachineName);
newLog.WriteLine(_MsgLevelText + "System : " + Environment.OSVersion + cpu);
newLog.WriteLine(_MsgLevelText + "====================================================================");
newLog.WriteLine(_MsgLevelText);
newLog.Close();
}
catch (Exception)
{
throw;
}
}
private string MsgTimeStamp()
{
string msgTimestamp = string.Empty;
return msgTimestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff");
}
}
}