Introduction
This article provides a method for enabling a Team System build process to undo checked-out files in TFS. It requires the Build Service account to have the "unlock other users' changes" permission. If you're not cool with that,
nothing to see here.
Background
We have a very large code base with many shared binary assemblies, and we love our continuous integration. There are a number of solutions in this code base that
rely on the shared binaries being built in a certain sequence. Moreover, when the solutions that produce these shared binaries are run, the build process needs to check
those binaries into TFS so that the latest binaries are available to subsequent builds. This allows our nightly builds to have all the latest and greatest binaries,
and produces a solid build for our QA team to start out their day. Of course, that's simple enough, until someone checks out one or more of these binary files, and the
build process fails to check in those files because of it. That means the build of the system won't have the latest changes! Therefore,
the need to undo another user's checkout by the Build Service account has given rise to a custom MSBuild task.
This article will focus on the undo checkout. I'll follow with another article on the process of checking out from TFS, copying the output from the build, and checking the new files in.
Using the code
Creating a custom task is a relatively simple matter of creating a C# class, and adding a few references:
Likewise, add the appropriate using statements:
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.IO;
The objective of the custom task is to extend the Task
object from MSBuild. Therefore, the Execute
method is overridden.
namespace Epsilon.BuildTask
{
public class UndoCheckoutTask : Task
{
public override bool Execute()
{
try
{
}
catch (Exception ex)
{
Log.LogError("Exception caught: " + ex.Message.ToString() + ex.StackTrace.ToString());
return false;
}
}
}
}
One of the arguments that will be passed in from the calling MSBuild .proj file is the server path location of the binaries to be checked in (for example,
$/Project/Folder/MyBinaries). We've already pre-determined that there's a changeset pending for the binaries in this path, so it's safe to undo the check-out.
We also need to provide the TFS server's URI. That, too is passed in from the calling project.
private string _binaryPath;
public string binaryPath
{
set { _binaryPath = value; }
}
private string _collectionURI;
public string collectionURI
{
set { _collectionURI = value; }
}
We can return a checkin ID back from the process, if desired. Notice the [Output]
decoration.
private int _checkinID;
[Output]
public int CheckinID
{
get { return _checkinID; }
}
Now to the meat of the matter. The idea here is to find any Pending Sets for the server path we passed in, and then to iterate through the Pending Changes
for those Pending Sets. Yes, there might be more than one Pending Set, because more than one user could conceivably have binaries checked out. Once the Pending Changes are found,
the workspace.Undo
can be invoked for the itemSpecs
that we've derived from the binary path. Here's the good stuff:
try
{
_checkinID = 0;
Uri serverUri = new Uri(_collectionURI);
TfsTeamProjectCollection collection = new TfsTeamProjectCollection(serverUri);
VersionControlServer versionControl = (VersionControlServer)collection.GetService(typeof(VersionControlServer));
ItemSpec[] itemSpecs = new ItemSpec[1];
itemSpecs[0] = new ItemSpec(@_binaryPath, RecursionType.Full);
PendingSet[] pendingSet = versionControl.QueryPendingSets(itemSpecs, null, null, includeDownloadInfo: true);
Log.LogMessage("Number of pending sets: " + pendingSet.Length.ToString());
foreach (PendingSet ps in pendingSet)
{
Log.LogMessage("====PS====>" + ps.Computer + "\t" + ps.Name +
"\t" + ps.OwnerName + "\t" + ps.PendingChanges + "\t" + ps.Type);
PendingChange[] pendingChanges = ps.PendingChanges;
foreach (PendingChange pc in pendingChanges)
{
Log.LogMessage("====PC====>" + pc.ChangeTypeName + "\t" +
pc.FileName + "\t" + pc.ServerItem + "\t" + pc.Version);
}
Workspace[] wsList = versionControl.QueryWorkspaces(ps.Name, ps.OwnerName, null);
if (wsList.Length != 0)
{
Workspace ws = wsList[0];
_checkinID = ws.Undo(itemSpecs);
Log.LogMessage("Check-in ID = " + _checkinID.ToString());
}
}
return true;
}
catch (Exception ex)
{
Log.LogError("Exception caught: " + ex.Message.ToString() + ex.StackTrace.ToString());
return false;
}
}
The very important part of all this is to make sure that the ws.Undo(itemSpecs)
is correctly specified. That's what limits the undo check-in process
to the binary path specified.
You've got options within all this to do useful things like sending email messages, or returning certain information back to the calling project file. The sky's the limit.
How to Use the Task
In order to consume this task from your MSBuild .proj file, you'll need to make the
.dll from this project available to the build server.
We use the MSBuild Extensions already, so we just placed the .dll into the same directory as the MSBuild project. Here's the
UndoCheckOut.proj file:
="1.0" ="utf-8"
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0" DefaultTargets="UndoCheckOut">
<UsingTask AssemblyFile="$(ExtensionTasksPath)Epsilon.BuildTask.dll" TaskName="Epsilon.BuildTask.UndoCheckoutTask"/>
<PropertyGroup>
<TPath>$(MSBuildExtensionsPath)\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks</TPath>
<TPath Condition="Exists('$(MSBuildExtensionsPath)\ExtensionPack\4.0\
MSBuild.ExtensionPack.tasks')">$(MSBuildExtensionsPath)\
ExtensionPack\4.0\MSBuild.ExtensionPack.tasks</TPath>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
<Import Project="$(TPath)"/>
<Target Name="UndoCheckOut">
-->
<Epsilon.BuildTask.UndoCheckoutTask collectionURI="$(MyURI)" binaryPath="$(MyBinaryPath)">
<Output TaskParameter="CheckinID" PropertyName="ID"/>
</Epsilon.BuildTask.UndoCheckoutTask>
<Message Text="CheckinID = $(ID)" />
</Target>
</Project>
The <UsingTask
directive provides the path to the .dll, and the invocation of the task can be found within the <Target>
.
Note how the output parameter is consumed and used.
Points of Interest
As mentioned at the beginning of this article, the Build Service account needs to be able to undo other account's changes. Since these binaries are in folders by themselves,
it's not a risk to grant that permission to the Build Service account. If you don't grant that permission, this will still run, but the checkouts obviously won't be undone.
Check the inline comments in the code above for little hints, too. There are some important tidbits in there.
History