Automatically create bug resolution task using the TFS 2010 API

Posted by Bob Hardister on Geeks with Blogs See other posts from Geeks with Blogs or by Bob Hardister
Published on Mon, 08 Oct 2012 09:33:16 GMT Indexed on 2012/10/09 15:40 UTC
Read the original article Hit count: 308

Filed under:

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");
        }
    }
}

© Geeks with Blogs or respective owner