Checking out and editing files during build using TeamCity and TFS 2010

If you are using TeamCity with MSBuild definition files you might be up for a challenge if you want to check out and check-in files during build. At first this might seem like a simple task? All you have to do is execute some TF.EXE commands? WRONG!

Unfortunately this is all too complicated. As MSBuild has no support for TF commands built into it the first challenge is to actually be able to execute TF.EXE

Here is how I do it in MSBuild.

<PropertyGroup>
  <TfCommand>"$(VS100COMNTOOLS)..\IDE\tf.exe"</TfCommand>
</PropertyGroup>
<Target Name="foo">
  <Exec Command="$(TfCommand) [your command here]" WorkingDirectory="$(MyWorkingDir)" ContinueOnError="false" />
</Target>

This is all well, but so far the commands are pretty useless. Let’s say we have a file called Director.xml that is under source control ($/Product/Trunk/Install/). Every time we build we want to check out this file, update it and check it back in. We cannot simply call TF Checkout etc. because TF requires a workspace. TeamCity uses a temporary workspace while getting files and we cannot use this during our msbuild script.              

So to be able to get the file and update it we need to create a temporary workspace. That just means calling TF workspace /new and then TF workfold /map. But wait… there is more. If we do this we will most likely get an error message from TF saying that the folder has already been mapped. Come on, give me a break! To get around this we create a temporary folder for our workspace.

1. Update our PropertyGroup to contain everything we need 

<PropertyGroup>
   <TfCommand>"$(VS100COMNTOOLS)..\IDE\tf.exe"</TfCommand>
   <ServerName>http://mytfs:8080/tfs/DefaultCollection</ServerName>
   <WorkspaceName>DummyWorkspace;DOMAIN\USER</WorkspaceName>
   <TempWorkspaceDir>C:\MYTEMPCHECKOUTFOLDER\</TempWorkspaceDir>
   <TFSUser>DOMAIN\USER,password</TFSUser>
</PropertyGroup>

We now have a reference to our TFS server, workspace name and the temporary folder the workspace will be stored in. We have also defined TFSUser which will be used to specify /login to tf. You might be able to skip this if the current user has the required access in TFS.

 2. Define a target that creates the workspace and checks out the file(s)

<Target Name="BeforeBuildInstaller">
  <Exec Command="$(TfCommand) workspace /new $(WorkSpaceName) /server:$(ServerName) /noprompt /login:$(TFSUser)" WorkingDirectory="$(TempWorkspaceDir)" ContinueOnError="false"/>
  <Exec Command="$(TfCommand) workfold /map "$/Product/Trunk/Install/" "$(TempWorkspaceDir)" /workspace:$(WorkSpaceName) /collection:$(ServerName) /login:$(TFSUser)" WorkingDirectory="$(TempWorkspaceDir)" ContinueOnError="false"/>
  <Exec WorkingDirectory="$(TempWorkspaceDir)" Command="$(TfCommand) get /version:T /force /login:$(TFSUser)"/>
  <Exec WorkingDirectory="$(TempWorkspaceDir)" Command="$(TfCommand) checkout Director.xml /login:$(TFSUser)"/>
  <Copy DestinationFolder="$(TeamBuildOutDir)" SourceFiles="$(TempWorkspaceDir)\Director.xml" />
  <OnError ExecuteTargets="UndoCheckout"/>
</Target>
<Target Name="UndoCheckout">
  <Exec WorkingDirectory="$(TempWorkspaceDir)" Command="$(TFCommand) undo /noprompt /recursive Director.xml /login:$(TFSUser)"/>
  <Exec Command="$(TfCommand) workspace /delete $(WorkSpaceName) /server:$(ServerName) /noprompt /login:$(TFSUser)" WorkingDirectory="$(TempWorkspaceDir)" ContinueOnError="true" />
</Target>
The above targets do the following:

a. Create a new workspace
b. Map the new workspace to a local folder
c. Get the latest version of the file(s) that we want
d. Checkout the file(s)
e. Copy the checkout file(s) to the directory where it will be changed. In my case it was the build output directory.
f. IMPORTANT! Rollback everything if anything fails.

3. Check-in and cleanup

Now all we have to do is check-in the file(s) and delete the temporary workspace. The following target will do that.

<Target Name="AfterBuildUpdate">
  <Copy DestinationFolder="$(TempWorkspaceDir)" SourceFiles="@(TeamBuildOutDir)Director.xml" />
  <Exec WorkingDirectory="$(TempWorkspaceDir)" Command="$(TFCommand) checkin /comment:"Auto-Build: Director.xml updated." /noprompt /override:"Auto-Build: Director.xml updated." /recursive Director.xml /login:$(TFSUser)"/>
  <Exec Command="$(TfCommand) workspace /delete $(WorkSpaceName) /server:$(ServerName) /noprompt /login:$(TFSUser)" WorkingDirectory="$(TempWorkspaceDir)" ContinueOnError="false" />
  <OnError ExecuteTargets="UndoCheckout"/>
</Target>
The above targets do the following:

a. Copy file(s) back to the temporary workspace
b. Check-in file(s)
c. Delete the temporary workspace
d. Again we make sure everything is rolled back if an error occurs

4. Putting it all together

<Target Name="BuildUpdate" />
  <Exec Command="$(BuildMyUpdate)" ContinueOnError="false" />
  <OnError ExecuteTargets="UndoCheckout"/>
</Target>

<Target Name="CreateInstaller" DependsOnTargets="BeforeBuildUpdate;BuildUpdate;AfterBuildUpdate" />

The above target set up the temporary workspace and checks out files, builds the update and finally checks files back in. If anything fails the temporary workspace and checkout will be rolled back. The rollback is vital as the build would fail the next time if proper cleanup of the workspace is not performed.