owenG
home learn tableau about
divider
SDLC VS 2010 Coded UI Test Coded UI Test and Fitnesse MSBuild 4.0 MSBuild and IronPython MSBuild and IronPython - TFS checkins MSBuild and IronPython - Custom SQL Data








IronPython Example: Custom task for reporting on recent check-ins to TFS


Another example of the PythonClassFactory in use. See code samples in that article for background on the setup and general environment.

The goal hear is to informally log recent check-ins to Team Foundation Server, for reasons that might include: a) having an idea of what areas QA should concentrate on and, b) if the build breaks, have an idea why. Yes, there are more solid, ALM-friendly ways of accomplishing these tasks via an established workflow but if quick and dirty is what you are after...

Before looking at the code I wanted to point out that the location in the .xaml Workflow where the initiating MSBuild Activity has been inserted is different from that discussed in the MSBuild 4.0 Custom Tasks PythonClassFactory articles. In those scenarios I only wanted to run the Activity if the source files had compiled successfully. Since I want this new check-in log generated even when compilation fails, it needs to be kicked off much earlier. Specifically, it will go into the sequence that is run if the 'CreateLabel' Activity is set to true. That means it will wind up being run a majority of the time but in certain situations, e.g. a private build that includes a shelveset of potential check-ins, the new custom MSBuild Activity is skipped. And that makes sense, since a changeset related to the shelveset doesn't yet exist and wouldn't be reported in the check-in log anyway.

The MSBuild Activity goes by the name MSBuild_PyCheckins:

Properties for MSBuild workflow activity

Contents of the relevant .targets file section:

.targets
  <UsingTask TaskName="PyCheckins"  TaskFactory="PythonClassFactory" 
        AssemblyFile="..\_compiled\PythonClassFactory.dll" >
    <ParameterGroup>
      <inTfsProject ParameterType="System.String" Required="true" />
      <inMaxTfsHistoryCount ParameterType="System.Int32" Required="true" />
      <inBackHours ParameterType="System.Int32" Required="true" />
      <outData Output="true" />
    </ParameterGroup>
    <Task>
      <![CDATA[
import clr
clr.AddReference('Microsoft.TeamFoundation.Client')
clr.AddReference('Microsoft.TeamFoundation.VersionControl.Client')
from Microsoft.TeamFoundation.Client import *
from Microsoft.TeamFoundation.VersionControl.Client import *
from System import Uri
from System import DateTime

projectPath = '$/{0}'.format(inTfsProject)
serverUri = Uri('http://tfsbuild01:8080/tfs/defaultcollection')
projectCollection = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(serverUri)
versionControl = projectCollection.GetService(VersionControlServer)
outData = ('(Executed under Identity: {0})\n
    List: maximum {1} changesets checked in within last {2} hours\n'
    .format(projectCollection.AuthorizedIdentity.DisplayName, inMaxTfsHistoryCount, inBackHours))
dateSpec = DateVersionSpec(DateTime.Now.AddHours(-inBackHours))
changesets = versionControl.QueryHistory(projectPath, VersionSpec.Latest, 0, RecursionType.Full, None, 
          dateSpec, None, inMaxTfsHistoryCount, True, False)
for cs in changesets:
  outData += 'Changeset Id: {0} Date: {1} Comment: {2}\n'.format(cs.ChangesetId, 
      cs.CreationDate, cs.Comment)
  for ch in cs.Changes:
    outData += '\tItem: {0}\n\t\tChangeType: {1}\n'.format(ch.Item.ServerItem, ch.ChangeType)
      ]]>
    </Task>
  </UsingTask>

  <PropertyGroup>
    <TfsProject>$(TeamProject)</TfsProject>
    <MaxTfsHistoryCount>10</MaxTfsHistoryCount>
    <BackHours>25</BackHours>
  </PropertyGroup>
  
  <Target Name="PyTarget6">
    <PyCheckins inTfsProject="$(TfsProject)" inMaxTfsHistoryCount="$(MaxTfsHistoryCount)"
         inBackHours="$(BackHours)">
      <Output PropertyName="outData" TaskParameter="outData"  />
    </PyCheckins>
    <Message Text="outData: $(outData)" Importance="High"/>
  </Target>

The Task is named PyCheckins and takes in 3 parameters (outputting 1):

  • inTfsProject - set in a MSBuild PropertyGroup to the value of '$(TeamProject)', though I find that property does not resolve to the expected value in this workflow, having been initiated by a custom MSBuild Activity. Either way, MSBuild_PyCheckin in the .xaml has a Property for CommandLineArguments and it is there that I use /p argument to pass the value of BuildDetail.BuildDefinition.TeamProject as TfsProject. That variable is used by the Task and reassigned to inTfsProject.
  • inMaxTfsHistoryCount - an integer value, also set in the PropertyGroup. Has a default value of 10, which is on the low side and could also be overridden on the command line if desired. When the Team Project's history is queried, no more than inMaxTfsHistoryCount/10 changesets will be retrieved, even should that mean it only goes back for a few hours.
  • BackHours - also an integer, determines how many hours back the history will include. Currently set at 25, where that will include the last 24 hours + a little extra to cover the time it might take between build initiation and full workspace creation.

Next I'll look at PyTarget6 the actual MSBuild Target invoked by MSBuild_PyCheckin. It is responsible for calling the PyCheckins task with the appropriate parameter values, setting the output parameter to a local MSBuild property, and then calling the Message task that writes the output data to the log. I'll note here that the LogFileDropLocation property of MSBuild_PyCheckin, in the .xaml, had to be modified from what was used in the initial PythonClassFactory articles. It had been set to the standard log file location but that value is no longer available due to the change in workflow sequencing - logFileDropLocation just doesn't exist 'yet'. Instead I used a known network share, which happens to work out fine since log file names are auto-incremented, or will be the first 100 times before. After that I'm guessing they will be progressively overwritten.

Now the IronPython code, beginning with the AddReferences. My plan had been to check the two TeamFoundation dll's in to the same TFS directory that held the IronPython binaries, .../BuildProcessTemplates/CustomTasks/_compiled. In my test environment however, the TFS server is the build server and apparently the check-in wasn't necessary and the assemblies are being found in, and loaded from, the GAC. (Actually I remember IronPython initially not being able to AddReference them during standalone MSBuild testing until I had copied the two dll's to \_compiled - either way check-in approach should work for a standalone build server.) The code imports everything from those TeamFoundation namespaces, which is not the most efficient method but serves well during testing. The only additional .Net classes that are needed: System.Uri and System.DateTime.

The projectPath variable is set to a TFS-friendly format by prepending the inTfsProject input parameter with '$/'. The serverUri is hard-coded, though of course there are other options that would make the script more re-usable. The next few lines illustrate the deprecation of TeamFoundationServer in favor of TfsTeamProjectCollection (and/or TfsConfigurationServer). If a 2k5/2k8 TFS server can be thought of containing a single set of team projects, 2010 can now hold multiple sets, i.e. a collection, of team projects. Once an instance of VersionControlServer is obtained from the TfsTeamProjectCollection we are more or less back to 2k5/2k8 TFS API syntax.

The header I constructed for the log begins with a confirmation of the identity under which the task is being executed, helpful for debugging problems e.g. the TFS Build Service account doesn't have sufficient permissions to perform the necessary API actions. Following that are details on the values that were passed into the Task.

A .Net DateVersionSpec is constructed and set to a time 25 hours in the past and, in combination with the inMaxTfsHistoryCount parameter, limits the processing TFS has to do. The second to last parameter on the call to QueryHistory is set to True, so that the resulting Changeset collection will include details on the contents of each changeset member. To get that info there is an inner For loop within the outer loop responsible for going through the Changeset collection itself. Details are appended to the main outData string variable.

And that is pretty much it, though you can see the C# code below for comparison purposes. The recorded information is purposefully vague, in that it doesn't try to match up anything with existing TFS labels. Instead, this task would be most appropriate for a Build Definition set to run once per day. There are code samples on the web that try to get a set of changsets that occur specifically between two labels and could definitely make sense when the labels in question have been set by Team Build and are unlikely to have been modified post-facto. Otherwise, you need to be aware that even if Labels represent a point in time when they are applied, they are not guaranteed to remain that way - see MSDN blog post Finding the changes between two labels in TFS version control for an overview of the issues and concerns.


divider element

C# version of same code:

C#
    string projectPath = string.Format("$/{0}", inTfsProject);
    var serverUri = new Uri("http://tfsbuild01:8080/tfs/defaultcollection"); 
    var projectCollection = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(serverUri);
    var versionControl = (VersionControlServer)projectCollection.GetService(typeof(VersionControlServer));
    outData = string.Format("(Executed under Identity: {0})\r\nList: maximum {1} changesets checked 
        in within last {2} hours\r\n",
        projectCollection.AuthorizedIdentity.DisplayName, inMaxTfsHistoryCount, inBackHours);
    var fromSpec = new DateVersionSpec(DateTime.Now.Subtract(TimeSpan.FromHours(inBackHours)));
    var changesets = versionControl.QueryHistory(projectPath, VersionSpec.Latest, 0, RecursionType.Full, 
        null, fromSpec, null, inMaxTfsHistoryCount, true, false);
    foreach (Changeset cs in changesets)
    {
        outData += string.Format("Changeset Id: {0} Date: {1} Comment: {2}\r\n", cs.ChangesetId, 
            cs.CreationDate, cs.Comment);
        foreach (Change ch in cs.Changes)
        {
            outData += string.Format("\tItem: {0}\n\t\tChangeType: {1}\r\n", ch.Item.ServerItem, 
                ch.ChangeType);
        }
    }