Prerequisites

   ·      .NET Framework 2.0 SP1 or greater

   ·      Web Deployment Tool

   ·      IIS 7.0

How it began?

I was working with a team of developers on automation of build deployment. We had decided to go with Microsoft’s web deployment tool to host our web application and web services directly in to IIS on remote server on successful completion of our build.

In the middle of the discussion on deployment process, one of our team members raised his concern over the quality of build. He just mentioned what if our build quality doesn’t meet our expectations and still the build is succeeded and the application will get deployed, but in this case we don’t want the build to get deployed.  Here by the term expectations, I wanted all unit test cases in the project should be passed and the Code Coverage result should be above the minimum value decided for the project.

We agreed to his points and we have decided that we will deploy the build only when we changed the quality of the build to ‘Ready for Deployment’ if it meets all the expectations.

How it is Implemented?

Before starting please make sure that Web Deploy 2.0 is installed, Web Deployment Agent Service and Web Management Service is started on the deployment server.

For auto deployment on successful build, we have to pass the following parameters in build arguments of our build definition. As shown in Fig. 1.

/p:DeployOnBuild=True /p:Configuration=Debug /p:DeployTarget=MSDeployPublish /p:MSDeployPublishMethod=RemoteAgent  /p:MsDeployServiceUrl=MachineName /p:DeployIisAppPath=TestSite/MySite  /p:UserName=UserName /p:Password=Password 

 

Fig.1

When we queue the build, the above mentioned build arguments will be passed to the MS Build and MS Build will call the MS Web Deploy with these arguments to deploy the application in IIS.

But our requirement is to deploy on build quality change, so just remove the parameter called /p:DeployTarget=MSDeployPublish from the above mentioned parameter list. This actually enables the MS Build to create a deployment package but it will not deploy it to the server. As we will see later in the topic, we need other parameters left out to deploy on build quality change. This is the end of first step.

Now we will actually start our main stuff. As we know that we have to deploy our build on build quality change, somehow we have to subscribe to Build Quality Change Event of Microsoft Team Foundation Server 2010 (TFS 2010).

There is one interesting thing in Microsoft Team Foundation Server 2010 which is hard to find in the documentation and that thing is, actually you can write event subscriber for TFS 2010 which will be executed within the context of TFS 2010. This means now you don’t need to use TFS client object model to go back to the TFS server and get the required information. As now you are executing the event subscriber within the context of TFS, all the information will be available to you.

So let’s see how we have written our event subscriber for Build Quality Change Event.

For writing an event subscriber to handle TFS 2010 event, we have to implement ISubscriber Interface which is found in Microsoft.TeamFoundation.Framework.Server. When we implement ISubscriber Interface, there is one method called ProcessEvent of ISubscriber interface where actually we have to   write our logic of deployment. For further explanation, let’s see the code below.

			

        public string Name
        {
            get
            {
                return "BuildQualityChangedNotificationEventHandler";
            }
        }

        public SubscriberPriority Priority
        {
            get
            {
                return SubscriberPriority.High;
            }
        } 
The Process Event method will actually do the deployment at deployment server. When Web   Deployment tool creates a package containing the deployment content which includes config       files,      html files, Asp.net files etc., it also create one cmd file appended with the text        “deploy.cmd”.   When we execute this file through command line, it calls the MS Web deploy to deploy the          content of the package in IIS of deployment server. The machine name and                       other information            for authentication will be taken from the command line arguments         passed while executing the    “*.deploy.cmd” file. So in the ProcessEvent method, we are       actually retrieving the parameters                 passed in the MS Build arguments in the build definition               through the build definition object.         Once we have the parameters with us, we will pass           those parameters to the “*.deploy.cmd” file and we will execute the command using                ProcessStartInfo and Process class as shown in the code below.

 For handling the Build Quality Change Event, we have to use the              BuildQualityChangedNotificationEvent class which is present in          Microsoft.TeamFoundation.Build.Server Assembly. The BuildQualityChangedNotificationEvent object will contain the information about build quality and build definition.

The “*.deploy.cmd “file will be present in build drop location. We will get the build drop location from the build definition property in the BuildQualityChangedNotificationEvent class. So using      the build drop location, we will find the “*.deploy.cmd” file and execute it.

        public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext requestContext, NotificationType notificationType, object notificationEventArgs, out int statusCode, out string statusMessage, out ExceptionPropertyCollection properties)
        {
            BuildQualityChangedNotificationEvent buildEvent = notificationEventArgs as BuildQualityChangedNotificationEvent;
            if (notificationType == NotificationType.Notification && buildEvent != null && buildEvent.Build.Status == BuildStatus.Succeeded)
            {
                if (buildEvent.NewValue.Equals("Ready for Deployment", StringComparison.OrdinalIgnoreCase))
                {
                    DirectoryInfo directory = new DirectoryInfo(buildEvent.Build.DropLocation);

                    FileInfo[] deployFiles = directory.GetFiles("*.deploy.cmd", SearchOption.AllDirectories);
                    XmlDocument doc = new XmlDocument();
                    doc.LoadXml(buildEvent.Build.Definition.ProcessParameters);
                    string msDeployParameters = GetMSDeployParameters(doc);
                    if (deployFiles.Length > 0)
                    {
                        ProcessStartInfo processStartInfo = null;
                        for (int count = 0; count < deployFiles.Length; count++)
                        {
                            processStartInfo = new ProcessStartInfo("cmd.exe", @"/C " + deployFiles[count].FullName + msDeployParameters);
                            processStartInfo.CreateNoWindow = false;
                            processStartInfo.UseShellExecute = false;

                            Process process = Process.Start(processStartInfo);
                        }
                    }
                }
            }
            statusCode = 1;
            statusMessage = "Deployed";
            properties = null;
            return EventNotificationStatus.ActionApproved;
        }

        private string GetMSDeployParameters(XmlDocument xmlDoc)
        {
            StringBuilder msDeployParameters = new StringBuilder(@" /Y");

            string[] msBuildArgumentsArr = GetMSBuildArguments(xmlDoc);
            if (msBuildArgumentsArr != null && msBuildArgumentsArr.Length > 0)
            {
                for (int count = 0; count < msBuildArgumentsArr.Length; count++)
                {
                    string[] msBuildArgsKeyValueArr = msBuildArgumentsArr[count].Split('=');
                    if (msBuildArgsKeyValueArr.Length > 1)
                    {
                        if (msBuildArgsKeyValueArr[0].Equals("MsDeployServiceUrl", StringComparison.OrdinalIgnoreCase))
                        {
                            msDeployParameters.Append(@" /M:" + msBuildArgsKeyValueArr[1]);
                        }
                        if (msBuildArgsKeyValueArr[0].Equals("UserName", StringComparison.OrdinalIgnoreCase))
                        {
                            msDeployParameters.Append(@" /U:" + msBuildArgsKeyValueArr[1]);
                        }
                        if (msBuildArgsKeyValueArr[0].Equals("Password", StringComparison.OrdinalIgnoreCase))
                        {
                            msDeployParameters.Append(@" /P:" + msBuildArgsKeyValueArr[1]);
                        }
                    }
                }
            }
            return msDeployParameters.ToString();
        }
        private string[] GetMSBuildArguments(XmlDocument xmlDoc)
        {
            string[] msBuildArgumentsArr = null;
            string msBuildArguments = string.Empty;
            XmlNode msBuildArgumentNode = GetMSBuildArgumentNode(xmlDoc);
            if (msBuildArgumentNode != null)
            {
                msBuildArguments = msBuildArgumentNode.InnerText;
                if (!string.IsNullOrEmpty(msBuildArguments))
                {
                    msBuildArgumentsArr = msBuildArguments.Split(new string[] { "/p:" }, StringSplitOptions.RemoveEmptyEntries);
                }
            }
            return msBuildArgumentsArr;
        }

        private XmlNode GetMSBuildArgumentNode(XmlDocument xmlDoc)
        {
            XmlNode msBuildArgumentNode = null;
            XmlNodeList xmlNodeList = xmlDoc.GetElementsByTagName("String", @"http://schemas.microsoft.com/winfx/2006/xaml");
            if (xmlNodeList.Count > 0)
            {
                foreach (XmlNode xmlNode in xmlNodeList)
                {
                    XmlNode attributeNode = xmlNode.Attributes.GetNamedItem("Key", @"http://schemas.microsoft.com/winfx/2006/xaml");
                    if (attributeNode.Value.Equals("MSBuildArguments", StringComparison.OrdinalIgnoreCase))
                    {
                        msBuildArgumentNode = xmlNode;
                        break;
                    }
                }
            }
            return msBuildArgumentNode;
        }

        public Type[] SubscribedTypes()
        {
            return new Type[1] { typeof(BuildQualityChangedNotificationEvent) };
        }
		 

Just copy the above give code in your class and compile it. To compile it successfully you have to add three assemblies Microsoft.TeamFoundation.Common, Microsoft.TeamFoundation.Build.Server and Microsoft.TeamFoundation.Framework.Server.  Microsoft.TeamFoundation.Build.Server and Microsoft.TeamFoundation.Framework.Server assembly will be found in the path C:\Program Files\Microsoft Team Foundation Server 2010\Application Tier\Web Services\bin.

Now you have your compiled assembly with you which will handle the Build Quality Change Event of the TFS 2010 but how TFS 2010 will come to know where to look up for this assembly. As we know TFS 2010 is a collection of Web Services, so you have to put this assembly in C:\Program Files\Microsoft Team Foundation Server 2010\Application Tier\Web Services\bin\Plugins folder.  TFS 2010 will take this assembly from this location and will execute the event accordingly. Any changes to the Plugins folder will restart the TFS so your assembly will loaded automatically once it is dropped to the plugins folder. If in case it is not loaded then just restarts the IIS and it will be loaded.

Now we have everything in place, just queue a new build and change its build quality to “Ready for Deployment”, and our web application will be deployed at the deployed server.

 

Conclusion

Download the zipped project attached with the article  and compile it. After compilation, copy the dll to C:\Program Files\Microsoft Team Foundation Server 2010\Application Tier\Web Services\bin\Plugins folder. Restart the IIS, your Auto deployment on build quality change will start working. 

All is Well :) 

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架