# Tuesday, 23 April 2013

A while a go I wrote an article on how I managed to get the test results from a build that was using PSAKE to surface in TFS’s summary view. I provided one solution to this problem in that article that would “just get you by” and have since been reminded that I said I would write another article on my second solution to the problem which was the PSAKE Incubator.

Its been a long time so its taken me a while to recollect what I did and I no longer have the old dev TFS server I originally got this working on so I will provide what I have done below in order to help anyone else who is looking for a similar solution and needs a steer in the right direction.

Basically I had removed the need for a InvokeProcess in the build XAML workflow and created a new custom activity called PSAKE Incubator. This custom activity would invoke the PSAKE powershell script natively allowing you to make use of the TFS code activity context directly in your PSAKE power shell scripts. In addition the PSAKE Incubator takes its parameters as an array and passes this through to PSAKE.

Below is the code I used to create the custom workflow activity to compile it you will need to create a custom workflow activity, Ewald Hofman has a great example here . Please also note that if you do not want to modify PSAKE if memory serves correctly you may need to create a custom activity host to handle the console output from PSAKE, hope this will be of use to someone, when I get some more time I’ll try and get a TFS box setup to test this out again.

Please note the code below is used at your own risk and I would advise testing on a dev environment before using on your production system.

If you do find the code below useful, please drop me a line and let me know how it went.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.IO;
namespace TfsWorkflowActivities
    using System.Activities;
    using System.Collections;
    using System.Globalization;
    using Microsoft.TeamFoundation.Build.Client;
    using Microsoft.TeamFoundation.Build.Workflow.Activities;
    using Microsoft.TeamFoundation.Build.Workflow.Services;
    public sealed class PSAKEIncubator : CodeActivity
        public InArgument<string> TFSURL { get; set; }
        public InArgument<string> TFSTeamProject { get; set; }
        public InArgument<string> TFSBuildURI { get; set; }
        public InArgument<Hashtable> LogTargets { get; set; }
        public InArgument<string> SourceDirectory { get; set; }
        public InArgument<string> BuildFile { get; set; }
        public InArgument<string[]> TaskList { get; set; }
        public InArgument<Hashtable> Parameters { get; set; }
        public InArgument<Hashtable> Properties { get; set; }
        public InArgument<string> PSAKEModuleDIR { get; set; }
        public InArgument<string> LogName { get; set; }
        public InArgument<string> OutPutDir { get; set; }
        public OutArgument<int> ExitCode { get; set; }
        protected override void Execute(CodeActivityContext context)
            IActivityTracking activityTracking = context.GetExtension<IBuildLoggingExtension>().GetActivityTracking((ActivityContext) context);
            context.TrackBuildMessage("PSAKEIncubator Started", BuildMessageImportance.Low);
            string tfsURL = context.GetValue(this.TFSURL);
            string tfsTeamProject = context.GetValue(this.TFSTeamProject);
            string tfsBuildURI = context.GetValue(this.TFSBuildURI);
            string outPutDir = context.GetValue(this.OutPutDir);
            string sourceDirectory = context.GetValue(this.SourceDirectory);
            string buildFile = context.GetValue(this.BuildFile);
            string[] taskList = context.GetValue(this.TaskList);
            Hashtable properties = context.GetValue(this.Properties);
            Hashtable parameters = context.GetValue(this.Parameters);
            string pSAKEModuleDIR = context.GetValue(this.PSAKEModuleDIR);
            string logName = context.GetValue(this.LogName);
            string arguments = String.Format("$global:tfsUrl = '{0}'\n",tfsURL);
                   arguments+= String.Format("$global:tfsTeamProject = '{0}'\n", tfsTeamProject);
                   arguments+= String.Format("$global:TfsBuildUri = '{0}'\n", tfsBuildURI);
                   arguments+= String.Format("$global:logTargets = @{{}}\n");
                   arguments+= String.Format("$global:logTargets.Add('TfsBuild', @{{verbosity='Progress'}})\n");
                   arguments += String.Format("$global:logTargets.Add('LogFile', @{{verbosity='Debug'; logDir='{0}\\_Logs'; logFilename='build-output.log'}});\n", outPutDir);
            context.TrackBuildMessage("Source Directory:" + sourceDirectory, BuildMessageImportance.Low);
            // Replace the line below with the location and name of the powershell script you use to kick off your PSAKE Build
            string scriptPath = string.Format("{0}\\run-psake.ps1", Path.Combine(sourceDirectory, "BuildAndDeploy\\buildframework"));
            context.TrackBuildMessage("Script Path:" + scriptPath, BuildMessageImportance.Low);
            List<CommandParameter> parametersArgument = new List<CommandParameter>();
            Hashtable parametersCleaned = new Hashtable();
            foreach (DictionaryEntry valHash in parameters)
                if (valHash.Value == null)
                    parametersCleaned.Add(valHash.Key, string.Empty);
                    parametersCleaned.Add(valHash.Key, valHash.Value);
            Hashtable propertiesCleaned = new Hashtable();
            foreach (DictionaryEntry valHash in properties)
                if (valHash.Value == null)
                    propertiesCleaned.Add(valHash.Key, string.Empty);
                    propertiesCleaned.Add(valHash.Key, valHash.Value);
            parametersArgument.Add(new CommandParameter("buildFile",buildFile));
            parametersArgument.Add(new CommandParameter("tasklist",taskList));
            parametersArgument.Add(new CommandParameter("outputDir",outPutDir));
            parametersArgument.Add(new CommandParameter("parameters", parametersCleaned));
            parametersArgument.Add(new CommandParameter("properties", propertiesCleaned));
            parametersArgument.Add(new CommandParameter("psakeModuleDir",pSAKEModuleDIR));
            parametersArgument.Add(new CommandParameter("logName",logName));
            context.TrackBuildMessage("PSAKEIncubator passing arguments to RunScript", BuildMessageImportance.Low);
            int exitResult = RunScript(scriptPath,arguments,context,parametersArgument.ToArray());
            context.TrackBuildMessage("PSAKEIncubator exit code " + exitResult, BuildMessageImportance.Low);
            context.SetValue<int>(this.ExitCode, exitResult);
        public string InformationNodeId { get; set; }
        private int RunScript(string scriptFile, string scriptText, CodeActivityContext context, CommandParameter[] parameters)
            // create Powershell runspace
            string encodedCommand = scriptText;
            //BespokePSHost bespokePSHost = new BespokePSHost();
            Runspace runspace = RunspaceFactory.CreateRunspace();
            IActivityTracking activityTracking = context.GetExtension<IBuildLoggingExtension>().GetActivityTracking((ActivityContext)context);
            InformationNodeId = activityTracking.Node.Id.ToString("D", (IFormatProvider)CultureInfo.InvariantCulture);
            // open it
            context.TrackBuildMessage("PSAKEIncubator Opening runspace", BuildMessageImportance.Low);
            //We set these variables so they become available in our PSAKE powershell scripts.
            // You will need to ensure these variables exist in your scripts to make use of them.
            runspace.SessionStateProxy.SetVariable("InformationNodeId", InformationNodeId);
            //This enables us to make use of the CodeContext directly from within PSAKE
            runspace.SessionStateProxy.SetVariable("CodeContext", context);
            // create a pipeline and feed it the script text
            Pipeline pipeline = runspace.CreatePipeline();
            context.TrackBuildMessage("PSAKEIncubator adding encoded script to pipeline", BuildMessageImportance.Low);
            Command parameterCommands = new Command(scriptFile);
            context.TrackBuildMessage("PSAKEIncubator adding parameters to pipeline", BuildMessageImportance.Low);
            foreach (CommandParameter item in parameters)
                context.TrackBuildMessage("processing param name:" + item.Name, BuildMessageImportance.Low);
                context.TrackBuildMessage("processing param value:" + item.Value, BuildMessageImportance.Low);
                if ("System.Collections.Hashtable" == item.Value.GetType().ToString())
                    Hashtable hashTable = (Hashtable)item.Value;                  
                    foreach (DictionaryEntry valHash in hashTable)
                        string valueType = "";
                        if (valHash.Value != null)
                            valueType = valHash.Value.GetType().ToString();
                            valueType = "NULL";
                            //valueType = valHash.Value.GetType().ToString();
                            //valHash.Value = string.Empty;
                        context.TrackBuildMessage("HashVal:" + valHash.Key + ":" + valHash.Value + "(" + valueType + ")", BuildMessageImportance.Low);
                    context.TrackBuildMessage("---End of HASH---", BuildMessageImportance.Low);
            //pipeline.Commands.Add("exit $LastExitCode;");
            // execute the script
            Collection<PSObject> results = pipeline.Invoke();
            // close the runspace
            context.TrackBuildMessage("PSAKEIncubator closing runspace", BuildMessageImportance.Low);
            // convert the script result into a single string
            context.TrackBuildMessage("PSAKEIncubator iterating through results", BuildMessageImportance.Low);
            StringBuilder stringBuilder = new StringBuilder();
            foreach (PSObject obj in results)
                //return Convert.ToInt32(obj);
                context.TrackBuildMessage(obj.ToString(), BuildMessageImportance.Low);
            return 0;


Tuesday, 23 April 2013 14:07:51 (GMT Daylight Time, UTC+01:00)  #    Comments [0]

# Monday, 28 January 2013

I was recently brought into a client site where they had made use of PSAKE to handle their build process. The build would be kicked off from the traditional Workflow in TFS using an Invoke Process. Everything was working perfectly until they spotted that when the build failed there was no way of viewing which unit tests had failed from within TFS. In short PowerShell was giving precious little to the TFS summary view.

The question was how could we get that rich logging information you got in the build summary when doing a traditional build using Workflow? Setting up a traditional build and observing how MSBUILD is called from TFS starts to shed some light on the situation

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe /nologo /noconsolelogger "C:\Builds\1\Scratch\Test Build\Sources\user\Test\Build.proj" /m:1 /fl /p:SkipInvalidConfigurations=true  /p:OutDir="C:\Builds\1\Scratch\Test Build\Binaries\\" /p:VCBuildOverride="C:\Builds\1\Scratch\Test Build\Sources\user\Test\Build.proj.vsprops" /dl:WorkflowCentralLogger,"C:\Program Files\Microsoft Team Foundation Server 2010\Tools\Microsoft.TeamFoundation.Build.Server.Logger.dll";"Verbosity=Normal;BuildUri=vstfs:///Build/Build/111;InformationNodeId=6570;
http://mytfshost:8080/tfs/Test%20Collection;"*WorkflowForwardingLogger,"C:\Program Files\Microsoft Team Foundation Server 2010\Tools\Microsoft.TeamFoundation.Build.Server.Logger.dll";"Verbosity=Normal;"


In the above example I have highlighted the section I discovered is responsible for the summary view you usually see when kicking off a build from TFS. I discovered this with a bit of guesswork and some reflector usage to see what was going on inside MSBUILD. Googling for the WorkflowCentralLogger gives precious little back about how it works and more about the errors people have encountered with it.

Getting to the solution
You will be forgiven for thinking the answer to the problem is just adding the missing WorkflowCentralLogger switch (with arguments) to your MSBUILD command line in PowerShell/PSAKE. Sadly its not that simple. See the InformationNodeId in the above command line? This appears to tell the WorkFlowCentralLogger where it needs to append its logging information. Passing it into the Invoke Process was my first thought, the problem is you’re not going to find anything that will give it to you, I wasn’t able to find it anywhere.

So how do you get it to work then?
The answer is, you need to build a Custom Workflow Activity. A custom workflow activity will have access to the current Context. To use this you need to inherit the class “CodeActivity”. Its up to you how you use this Custom Workflow Activity, you have one of two ways.

  • Place it above the Invoke Process in your workflow, get the InformationNodeId and pass this as an OutArgument to the Invoke Process below it (not tested fully)
  • Or invoke Powershell from within the Custom Activity using a runspace and pass it the code context. (fully tested)
   3:  namespace MyWorkflowActivities
   4:  {
   5:      using System;
   6:      using System.Collections.Generic;
   7:      using System.Linq;
   8:      using System.Text;
   9:      using System.Collections.ObjectModel;
  10:      using System.Management.Automation;
  11:      using System.Management.Automation.Runspaces;
  12:      using System.IO;
  13:      using System.Activities;
  14:      using System.Collections;
  15:      using System.Globalization;
  17:      using Microsoft.TeamFoundation.Build.Client;
  18:      using Microsoft.TeamFoundation.Build.Workflow.Activities;
  19:      using Microsoft.TeamFoundation.Build.Workflow.Services;
  21:      public OutArgument<string> InformationNodeIdOut { get; set; }
  23:      [BuildActivity(HostEnvironmentOption.All)]
  24:      public sealed class GetInformationNodeId : CodeActivity
  25:      {
  26:          protected override void Execute(CodeActivityContext context)
  27:          {
  29:              context.TrackBuildMessage("Getting the Information Node Id", BuildMessageImportance.Low);
  30:              IActivityTracking activityTracking = context.GetExtension<IBuildLoggingExtension>().GetActivityTracking((ActivityContext) context);
  31:              string informationNodeId = activityTracking.Node.Id.ToString("D", (IFormatProvider)CultureInfo.InvariantCulture);
  33:              context.SetValue<string>(this.InformationNodeIdOut, informationNodeId);
  34:          }
  35:      }
  37:  }

The code above illustrates the first solution. Its a lot simpler and you’ll have to pass that node id to MSBUILD when you construct its command line in PowerShell. Line 30 and 31 is where all the magic takes place, I managed to find this line using reflector in MSBUILD. If you have never written a custom activity before Ewald Hofman has a short summary of one here

The diagram below illustrates where GetInformationNodeId (code above) sits just above the InvokeProcess which calls PowerShell.



The second solution, which I actually went with is slightly more complex and I’ll blog about how I did that in another article. You might be wondering what are the immediate benefits of one over the other? The beauty of going with the second solution is you can make use of the code activity context within your PowerShell scripts. So for example instead of writing your PowerShell events out to the host you could wrap that call in context.TrackBuildMessage (as illustrated on line 29 above).

I’d be interested to hear about other peoples experiences.


Monday, 28 January 2013 15:51:49 (GMT Standard Time, UTC+00:00)  #    Comments [0]