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:
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.
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);
}
}