In many of my projects I will create a new release branch when our code is ready for production. This gives me a snapshot of the code when it went to production. This allows us to make any bug fixes in the production code base but continue adding new features to dev and test branches. This has always been a manual step to create the branch until i was talking to a client and suggested that I could automate this process. I hadn’t done a branch from within Team Build but I knew it could be done via the command line so I figured we could automate it. I used the Exec task to create the branch like this:
<Exec WorkingDirectory="$(SolutionRoot)"
Command="$(TF) branch $/MyTeamProject/Main/ $/MyTeamProject/Releases/2.1.0.0 /noget"
Next the build needed to check in the branch:
<Exec WorkingDirectory="$(SolutionRoot)"
Command="$(TF) checkin /comment:"***NO_CI***New Release Created" /noprompt
/override:"New Release Created" /recursive"
IgnoreExitCode="true" />
This was the easy part. One of the challenging parts was the workspace mappings. Even though I could get this to work by creating a mapping at the root of the team project, I didn’t want to pull down all of the branches under source control. If I didn’t have the mappings at a high enough level to cover the target, the build will generate an error like this:
No appropriate mapping exists for $/MyTeamProject/Releases/2.1.0.0
Since with each release this problem would compound, I wanted to only pull down the necessary source control. To solve this problem I created a custom task that takes a parent folder, in this case $/MyTeamProject/Releases/ and either cloaks all of the subfolders to only add a new row, or it will cloak all of the folders except for the one specified. The second option is beneficial if the source folder for the branch is dynamic also. This will also create a dynamic mapping between the WorkspaceServerItem and WorkspaceLocalItem.
The task is called CreateDynamicMapping and here is the call:
<CreateDynamicMapping
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
WorkspaceServerItem="$/MyTeamProject/Releases/"
WorkspaceLocalItem="$(SolutionRoot)\Releases"
WorkspaceName="$(WorkspaceName)"
WorkspaceOwner="$(WorkspaceOwner)"
ParentFolder="$/MyTeamProject/Releases/"
</CreateDynamicMapping>
This example uses the ParentFolder to get all of the child folders, loops through each of them and if it finds a match to the WorkspaceServerItem (It won’t in this example because it isn’t a child folder of the ParentFolder) it will keep that folder, otherwise it cloaks the folder so it is not pulled down.
Creating this task was my first opportunity to use the TFS API. With the help of some great posts by Shai Raiten, this ended up straight forward and even wrapped some unit tests around make sure it worked. Here’s the main part of the code for the task:
TeamFoundationServer tfs = new TeamFoundationServer(teamFoundationServerUrl);
// Get a reference to Version Control.
VersionControlServer versionControl = (VersionControlServer)tfs.GetService(typeof(VersionControlServer));
Workspace[] workspaces = versionControl.QueryWorkspaces(workspaceName, workspaceOwner, null);
if (!parentFolder.Contains("*.*"))
{
parentFolder += "/*.*";
parentFolder = parentFolder.Replace("//", "/");
}
RecursionType recursion = RecursionType.OneLevel;
ItemSet itemSet = versionControl.GetItems(parentFolder, recursion);
foreach (Item item in itemSet.Items)
{
if (workspaceServerItem.ToUpper() != item.ServerItem.ToUpper())
{
workspaces[0].Cloak(item.ServerItem);
}
}
workspaces[0].CreateMapping(new WorkingFolder(workspaceServerItem, workspaceLocalItem));
This is what you need to automate the branching process from with a TFS Build. I didn’t talk about how to get your version number to branch to. This will depend on your projects requirements. You may want to use get the assembly version of one of the assemblies you compiled to use this as the folder or in this instance the source folder was dynamic too.
Mike