Third Shelf

Automating a ClickOnce Deployment – Part3

Posted in team foundation server by Sydney du Plooy on March 20, 2010

Continuing the series on Automating a ClickOnce Deployment it’s time to implement the deployment scheme in a build file.

After the build file compiled the solution we are ready to start the copying of the installation files to the deployment server. Before we start the copy process we have to create a new directory for the new application version. In the following snippet I simply generate a new version number and then create a directory where we will copy the installation files to:

<Target Name="DeployApplication">
  <Message Text="Deploying the client application to $(PublishServer)" />
  <MakeDir Directories="$(DeploymentServer)\Published\App\$(VersionNumber)" />

We are ready to copy the application files to the server:

<Message Text="Copying files to deployment directory [$(DeploymentServer)\Published\App\$(VersionNumber)]" />
<Copy DestinationFiles="@(ApplicationFiles->'$(DeploymentServer)\Published\App\$(VersionNumber)\%(Filename)%(Extension)')" SourceFiles="@(ApplicationFiles->'$(BinariesFolder)\%(Filename)%(Extension)')" />

The ApplicationFiles is an item group with the installation files that we are copying. Note: we do not transform the filename to include the .deploy extension because we first need to generate the deployment manifest with the files in the right location before changing the name of the files. (See Gotcha #3). Next, we create the application manifest with the Mage utility which will be stored in the application directory (not in the version directory). Mage is a manifest generator and is used to generate both application and deployment manifests. In the snippet below we are using Mage explicitly to generate the application manifest:

<CreateProperty Value="$(DeploymentServer)\Published\App\$(VersionNumber)\App.exe.manifest">
  <Output TaskParameter="Value" PropertyName="ApplicationManifestFile"/>
</CreateProperty>
<Message Text="Generating application manifest  [$(ApplicationManifestFile)] using  [$(DeploymentServer)\Published\App\$(VersionNumber)]" />
<Exec Command="mage.exe -New  Application -p msil -TrustLevel FullTrust -ToFile  $(ApplicationManifestFile) -Name  &quot;App&quot; -Version $(VersionNumber) -FromDirectory  $(DeploymentServer)\Published\App\$(VersionNumber)" />

So far so good, we have now managed to generate the deployment manifest from the files located in the …\Published\App directory and placed the file in the …\Published\App\$(VersionNumber) directory. We will now point the application manifest to this deployment manifest so that the correct files are installed on the client machine.

We now have everything that we need in order to generate the application manifest.

<CreateItem Include="$(ApplicationManifestFile)" AdditionalMetadata="TargetPath=$(VersionNumber)\App.exe.manifest">
  <Output TaskParameter="Include" ItemName="RelativeApplicationManifestFile"/>
</CreateItem>

<GenerateDeploymentManifest AssemblyName="App.exe.application"
  AssemblyVersion="$(VersionNumber)"
  DeploymentUrl="http://deployment-server/Published/App/App.exe.application"
  Product="App"
  Description="App"
  Publisher="thirdshelf.com"
  Install="true"
  UpdateEnabled="true"
  UpdateMode="Foreground"
  OutputManifest="$(DeploymentServer)\Published\App\App.exe.application"
  MapFileExtensions="true"
  EntryPoint="@(RelativeApplicationManifestFile)" />

If you read part 2 of this series you should be able to map these properties straight to their positions in the application manifest file and their functions. The only thing that remains is to rename the files to include their .deploy extensions. This can easily be done in the following way:

<Message Text="Replacing files in [$(DeploymentServer)\Published\App\$(VersionNumber)\] with [.deploy]" />
<Delete Files="@(ApplicationFiles->'$(DeploymentServer)\Published\App\$(VersionNumber)\%(Filename)%(Extension)')" />
<Copy SourceFiles="@(ApplicationFiles->'$(BinariesFolder)\%(Filename)%(Extension)')
  DestinationFiles="@(ApplicationFiles->'$(DeploymentServer)\Published\App\$(VersionNumber)\%(Filename)%(Extension).deploy')" />

Visiting the URL http://deployment-server/Published/App/App.exe.manifest and clicking on the Install button you should see the following dialog:

Gotchas

There are a few gotchas that you might find on your way to success and I’ve listed the most common ones here and their solutions for your convenience:

Gotcha #1: Reference in the manifest does not match the identity of the downloaded assembly …

This is easily resolved by adding the NoWin32Manifest property to solution in the SolutionsToBuild group:

<SolutionToBuild Include="$(SolutionRoot)\Application.sln">
  <Properties>NoWin32Manifest=true</Properties>
</SolutionToBuild>

Gotcha #2: XML files are marked as data files, by default.

This means that XML files are published to the data directory whose location can found by querying the System.Deployment.Application.ApplicationDeployment.CurrentDeployment.DataDirectory  property.

Gotcha #3: The customHostSpecified attribute is not supported for Windows Forms applications.

Not a very helpful error, but chances are that you generated the deployment manifest with the installation files having the .deploy extension and not on the original filenames.

Tagged with: , ,

Automating a ClickOnce Deployment – Part2

Posted in team foundation server by Sydney du Plooy on March 20, 2010

Continuing the series on Automating a ClickOnce Deployment it’s time to have a look at the manifest files. There are two required manifest files that give clickonce the necessary information to deploy the application:

  • an application manifest (.application);
  • a deployment manifest (.manifest).

Application Manifest

The application manifest describes the application that we are going to install. So, let’s have a look at the contents of this file:

xml version="1.0" encoding="utf-8"?>
<asmv1:assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1" xmlns="urn:schemas-microsoft-com:asm.v2" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xrml="urn:mpeg:mpeg21:2003:01-REL-R-NS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <assemblyIdentity name="App.application" version="1.0.903.18476" publicKeyToken="0000000000000000" language="neutral" processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" />
  asmv2:publisher="App Publishers Company" asmv2:product="App" xmlns="urn:schemas-microsoft-com:asm.v1">App
  mapFileExtensions="true">
    <subscription>
      <update>
        <beforeApplicationStartup />
      </update>
    </subscription>
    <deploymentProvider codebase="http://deployment-server/Published/App/app.exe.application" />
  </deployment>
  <dependency>
    <dependentAssembly dependencyType="install" codebase="1.0.903.18476\App.exe.manifest" size="33600">
      <assemblyIdentity name="App.exe" version="1.0.903.18476" publicKeyToken="0000000000000000" language="neutral" processorArchitecture="msil" type="win32" />
      <hash>
        <dsig:Transforms>
          <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
        </dsig:Transforms>
        <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <dsig:DigestValue>DrLiyVjtlKhOmtuzuXhFSTqi/9Q=
      </hash>
    </dependentAssembly>
  </dependency>
</asmv1:assembly>

One of the important things to note in this manifest is the mapFileExtensions attribute in the deployment tag. This attribute indicates that our installation files have a .deploy extension – MyApplication.exe is copied to the deployment server as MyApplication.exe.deploy and is renamed when installed on the client machine. In the deploymentProvider tag we specify where the application can find future updates and from where this application was installed.

The next interesting tag is dependentAssembly, which points to the deployment manifest to use when installing the application on the client machine. We specify the deployment manifest in the codeBase attribute which describes how the application installs. Note that the codebase attribute points to a version folder in which the deployment manifest resides – for every new release of the application this application manifest is either overwritten or updated to point to the new version of the installation files and deployment manifest.

Deployment Manifest

The deployment manifest describes the how the application installs and what files to install on the client. Once again, let’s have a look at the contents:

<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns="urn:schemas-microsoft-com:asm.v2" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1">
  <asmv1:assemblyIdentity name="App.exe" version="1.0.903.18476" publicKeyToken="0000000000000000" language="neutral" processorArchitecture="msil" type="win32" />
  <application />
  <entryPoint>
    <assemblyIdentity name="App" version="1.0.903.18476" language="neutral" processorArchitecture="msil" />
    <commandLine file="App.exe" parameters="" />
  </entryPoint>
  <trustInfo>
    <security>
      <applicationRequestMinimum>
        <PermissionSet Unrestricted="true" ID="Custom" SameSite="site" />
        <defaultAssemblyRequest permissionSetReference="Custom" />
      </applicationRequestMinimum>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
  <dependency>
    <dependentOS>
      <osVersionInfo>
        <os majorVersion="5" minorVersion="1" buildNumber="0" servicePackMajor="0" />
      </osVersionInfo>
    </dependentOS>
  </dependency>
  <dependency>
    <dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
      <assemblyIdentity name="Microsoft.Windows.CommonLanguageRuntime" version="2.0.50727.0" />
    </dependentAssembly>
  </dependency>
  <dependency>
    <dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="DevExpress.Data.v9.1.dll" size="855552">
      <assemblyIdentity name="App" version="9.1.5.0" publicKeyToken="B88D1754D700E49A" language="neutral" processorArchitecture="msil" />
      <hash>
        <dsig:Transforms>
          <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
        </dsig:Transforms>
        <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <dsig:DigestValue>CXehkPHON56TeTj7ZWsy++QsymE=
      </hash>
    </dependentAssembly>
  </dependency>
  <file name="GlobalConfiguration.xml" size="21074">
    <hash>
      <dsig:Transforms>
        <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
      </dsig:Transforms>
      <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
      <dsig:DigestValue>6hHtbL9F9sfgWmKLYrWRpYmILCE=
    </hash>
  </file>
</asmv1:assembly>

The first tag of interest here is the entryPoint tag that identifies the application to run once the installation completes. In order to start the application clickonce will execute the command with the parameters specified in the commandLine tag.

Every application has dependencies that must either be installed with the program or should have been installed. In the first dependency tag we see that the minimum operating system version required is Windows XP. In the second dependency tag we see that the dependencyType attribute instructs clickonce to verify that the .NET Framework 2.0 is already installed on the client.

In the third dependency tag we see that the dependencyType is install, which instructs clickonce to install this dependency on the client. Remember that the .deploy extension will automatically be removed during the installation process.

The next tag is a file tag that specifies a non-assembly file that is copied into the application installation directory. Note: by default when clickonce encounters a xml file it assumes it as a configuration file and will copy it to a data directory; along the application directory; instead of the application directory. You can find the data directory location by querying the System.Deployment.ApplicationApplicationDeployment.DataDirectory property.

In the next installment of this series we will show a build file example and generate a deployment manifest with the Mage tool.

Tagged with: , ,

Executing a task remotely with MsBuild

Posted in team foundation server by Sydney du Plooy on October 19, 2009

Recently, we were in need of executing a command on a remote server as part of our automated build process. For this we decided to make use of Windows Management Instrumentation (WMI). Before jumping to the code, copying and pasting it, there are always some security issues that need to be addressed. For this task there are also some service configurations that are required on the remote machine.

Note: All of the following settings are configured on the remote machine on which the command will be run remotely.

Security

Follow the steps below to set the WMI permissions for the build service account:

  1. Click on Start > Control Panel > Administrative Tools > Computer Management.
  2. Expand Service and Applications.
  3. Right-click on WMI Control and click on Properties.
  4. On the property dialog select the Security tab.
  5. Expand Root > CIMV2.
  6. Click on the Security button.
  7. Click on Add and add <TFSSERVICE> account.
  8. In the list of permissions, allow the following permissions:
    • Execute methods
    • Remote enable
  9. Click on OK.

Follow the step below to set the Local Security Policy for the build service account:

  1. Click on Start > Control Panel > Administrative Tools > Local Security Policy.
  2. Expand Local Policies > User Rights Assignment.
  3. Find and double-click on Log on as a service.
  4. Click on Add User or Group and add <TFSSERVICE> account.
  5. Click on OK.
  6. Find and double-click Log on as a batch job.
  7. Click on Add User or Group and add <TFSSERVICE> account.
  8. Click on OK and close the Local Security Policy window.

Services

Follow the steps below set the dependent services to automatically start:

  1. Click on Start > Control Panel > Administrative Tools.
  2. Double click on Services.
  3. Ensure that the following services are Started, and are set to start Automatic.
    • COM+ Event System
    • Remote Access Auto Connection Manager
    • Remote Access Connection Manager
    • Remote Procedure Call (RPC)
    • Remote Procedure Call (RPC) Locator
    • Remote Registry
    • Server
    • Windows Management Instrumentation
    • Windows Management Instrumentation Driver Extensions
    • WMI Performance Adapter
    • Workstation
  4. Close the services dialog.

Here is the code for the build task that will execute a command on a remote machine:

/// <summary>
/// Executes a command line on a remote machine.
/// </summary>
public class RemoteExec : Task
{
 /// <summary>
 /// Initialises a new instance of the <see cref="RemoteExec"/> class.
 /// </summary>
 public RemoteExec()
 {
   RemoteMachine = Environment.MachineName;
 }

 /// <summary>
 /// Gets or sets the name of the machine on which to execute the command.
 /// </summary>
 public string RemoteMachine
 {
   get;
   set;
 }

 /// <summary>
 /// Gets or sets the command to execute on the remote machine.
 /// </summary>
 [Required]
 public string Command
 {
   get;
   set;
 }

 /// <summary>
 /// Executes the task on the remote machine.
 /// </summary>
 /// <returns>true if the task succeeded, otherwise, false</returns>
 public override bool Execute()
 {
   if (string.IsNullOrEmpty(Command))
   {
     Log.LogError("Command property was not set.");
     throw new ArgumentNullException("Command");
   }

   var connOptions = new ConnectionOptions();
   connOptions.Impersonation = ImpersonationLevel.Impersonate;
   connOptions.EnablePrivileges = true;

   var managementScope = new ManagementScope(
     string.Format(@"\\{0}\ROOT\CIMV2", RemoteMachine), connOptions);
   managementScope.Connect();

   Log.LogMessage(string.Format(CultureInfo.CurrentCulture, "Connected to {0}: {1}",
   RemoteMachine, managementScope.IsConnected));

   if (!managementScope.IsConnected)
   return false;

   var objectGetOptions = new ObjectGetOptions();
   var managementPath = new ManagementPath("Win32_Process");
   var processClass = new ManagementClass(managementScope, managementPath, objectGetOptions);

   var processParameters = processClass.GetMethodParameters("Create");

   processParameters["CommandLine"] = Command;
   try
   {
     var outParams = processClass.InvokeMethod("Create", processParameters, null);
     var processID = Convert.ToUInt32(outParams["processId"]);

     Log.LogMessage(string.Format(CultureInfo.CurrentCulture,
       "Creation of the process {0} returned: {1}", processID, outParams["returnValue"]));

     var stopQuery = new WqlEventQuery(string.Format(CultureInfo.CurrentCulture,
       "select * from Win32_ProcessStopTrace where ProcessID={0}", processID));

     var processStopEvent = new ManagementEventWatcher(managementScope, stopQuery);
     processStopEvent.Options.Timeout = new TimeSpan(0, 1, 0);

     Log.LogMessage("Waiting for process to complete...");
     processStopEvent.WaitForNextEvent();
     processStopEvent.Stop();

     Log.LogMessage("Completed.");
   }
   catch (Exception ex)
   {
     Log.LogError(ex.Message);
   }

   return true;
   }
}

The task can then be used as follows:

<UsingTask TaskName="RemoteExec" AssemblyFile="BuildTasks.dll"/>
<RemoteExec RemoteMachine="$(Server)" Command="application.exe" />

There we have it, executing a command remotely from MsBuild!

Tagged with: ,

Creating JunctionPoints with MsBuild

Posted in team foundation server by Sydney du Plooy on October 10, 2009

With our build process we decided on a deployment folder structure that required us to create NTFS Junction Points. Junction Points are links to other folders, also known as soft links. Below is an example of how these links appear in Windows 7. It also shows our deployment folders for various build types:

JunctionPointFolderStructureAll the folders with shortcut arrows are junction points or links to folders in the Published folder. For each build, the binaries are copied to a build numbered folder in the Published folder. Only when a build is successful; meaning that all code compiled and unit tests passed; do we update the junction point to the relevant build numbered folder in Published.

By using the code written by Jeff Brown in this article, I was able to create a custom build task that enabled us to the create and delete junction points with relative ease. Here is a link to the source code for an MsBuild task that will enable you to manage junction points with MsBuild.

Tagged with: ,

Automating a ClickOnce Deployment – Part1

Posted in team foundation server by Sydney du Plooy on September 6, 2009

In our build process, using Team Foundation Build, we decided to use ClickOnce for application deployment. In this post I’ll try to set the lay of the land. In the following couple of posts I will take you through the process of setting up a clickonce deployment.

Choosing the deployment strategy

There are essentially two different ways to deploy clickonce applications. They are:

  1. Physical media such as CD or DVD
  2. Network share or web

Deciding on the strategy that will work for you is largely determined by the available bandwidth at the client site. If there is none to very little bandwidth, or no internet connectivity, choose CD / DVD. Otherwise choose Network share or web.

We opted for the web strategy and so I will base the series around this strategy.

Lay of the land

Assume that we have one build server and one deployment server. The build server will push the build artifacts to the deployment server from where clients will use clickonce to install the application. The application is pushed to the deployment server after every build with a new version number.

To make the explanations easier we will setup a hypothetical configuration and base the deployment configuration on it. Our hypothetical solution consists of a single application, which we will call App. Below is a diagram that demonstrates this configuration:

Note: To hide the published share simply name it with a trailing dollar sign i.e. “Published$”.

Initial configuration

First of all we need a folder on the deployment server to push the build artifacts to. For this, create a published folder on the deployment server and share it with modify permissions granted to the TFSSERVICE account. Remember the TFSSERVICE account that TFS uses for the build service?

Furthermore, we need to configure Internet Information Services (IIS) to interpret the clickonce files correctly according to their extension. For this, add the following MIME types in IIS:

.application    application/x-ms-application
.manifest       application/x-ms-manifest
.deploy         application/octet-stream

If you are using IIS 6.0, have a look at this article “IIS 6.0 Does Not Serve Unknown MIME Types”.

In the next installment I’ll discuss the different files that describe the application and deployment.

Tagged with: , ,

Team Foundation Server Build Notification

Posted in team foundation server by Sydney du Plooy on September 5, 2009

greentickIncluded in the Team Foundation Server Tools download is an absolute must-have, Team Foundation Build Notification. If you have are making use of continuous integration chances are that you are building often. Instead of keeping an eye out for the latest build report in your email or constantly checking it in Visual Studio, use this notification utility. It has little popups that appear when someone requested a build either via checkin or ad hoc, which is configurable. It has a notification icon in the system tray (one the few that I will tolerate) and changes whenever one of the builds succeeds, fails or starts. It gives immediate access to the build log which you would normally have to browse for. It is also able to monitor multiple builds. A definite win!

Tagged with: ,

Rollback Changesets

Posted in team foundation server by Sydney du Plooy on March 12, 2009

In Team Foundation Server 2008, there doesn’t seem an easy way to simply perform a rollback on a comitted changeset. Well, that is until you discover the power of the updated Team Foundation Server Power Toys.

rollback-changeset

Rollback command

Before rolling back a changeset, the local workspace must have no pending changes. After issuing the rollback command, tfpt rollback a dialog will appear asking whether you want the local workspace brought up to date with the repository. After affirming the dialog, a get command will be issued for your workspace and updated with the latest changes.

After the get operation completed successfully, another dialog will appear asking you to identify the changeset that you want to rollback. Note: Make sure that you have the correct changeset before continuing.

Select the files that you want to rollback in the changeset and click on “Roll Back”. After the rollback completed, you might have to resolve some conflicting changes. Now that the rollback is complete, you will still need to commit the new changeset, which is the latest code, without the changeset that was rolled back.

If you want to skip all of the GUI stuff, then you can simply execute the rollback command this way: tfpt rollback /changeset:4018.

Tagged with: , ,

Customising Work Item Templates

Posted in team foundation server, visual studio by Sydney du Plooy on October 26, 2008

Making use of either Team System Web Access or Team Explorer you might find it necessary to edit some of the work item templates. At first it might seem very confusing but is quite straight forward.

When editing work item templates there are two tools that you need to know about, witimport and witexport. They are used to import and export specific work item templates from an existing Team Foundation Server project.

First, you need to export the bug work item template by using the following command line:

witexport /f <work item templates folder>
    /t <tfs server>:8080 /p <project name> /n bug

This will create a bug.xml template file in the work item templates folder specified, which could be any folder that you have access to.

The first part of the XML file defines the fields (indicated by the fields tag) that describe the bug. A workflow process (indicated by the workflow tag) is described in the middle of the file and the bottom part of the file describes the layout (indicated by the form tag) of the bug submission form.

Let’s add an environment field to the template that will indicate the environment where the bug occurred with the following values, tip, pre-stage, stage and live:

<field name="Environment" refname="TfsProject.Environment" type="String">
    <required />
    <helptext>Was the bug found in tip, pre-stage, stage or live?</helptext>
    <allowedvalues>
	<listitem value="Tip"/>
	<listitem value="Pre-stage"/>
	<listitem value="Stage"/>
	<listitem value="Live"/>
    </allowedvalues>
    <default from="value" value="Pre-Stage"/>
</field>

The above piece of XML can be put anywhere after a closing field tag or the opening fields tag and must be somewhere before the workflow opening tag.

Name is obviously the name of the field that we are giving the additional piece of information. RefName is the name of this field that we are going to use to refer to when setting up the control in the layout section of the file. The presence of required tag says that a value must be specified for this field and will be validated automatically when a new bug is submitted. Next, we supply a list of values that will appear in the drop down list. Finally we provide a default value that will automatically be selected for each bug submission.

We now have to specify the new look of the form with our additional field and is done with the following XML definition:

<group>
    <column PercentWidth="50">
        <control Type="FieldControl" FieldName="TfsProject.Environment" Label="&Environment:" LabelPosition="Left" />
    </column>
    <column PercentWidth="50">
        ...
    </column>
</group>

A group defines a collection of controls that are related. The column tag has an attribute that states the percentage space that the control should occupy. Each group should add up to to 100% but do not have to. The fieldname should be set to the field that we have just created in the fields section of the template. The label attribute is set to the label that we want to show in front of the control. In this case it is “Environment:”.

After placing the control, all that is left is to save the bug.xml file and import it back into Team Foundation Server. This is accomplished by the following command line:

witimport /f <work item templates folder>\bug.xml
    /t <tfs server>:8080 /p <project name>

After a successful import the changes should be visible from both TSWA (Team System Web Access) and Team explorer, by double clicking on the Bug work item template.

Go customise those work item templates!

Tagged with: , ,

Source Code Promotion Model

Posted in team foundation server by Sydney du Plooy on September 24, 2008

At work we make use of a source code promotion model. What this means is that we promote source code through the various stages of product development.

All new development is done at the tip level. Once development is completed; and I’ll leave the definition up to you; the source code is promoted to the pre-stage level. It is at this level where our internal testers can begin testing the latest release of the product.

Only when the internal testers are satisfied, is the pre-stage source code promoted to the stage level. This is the level where our business users have access to the latest product release. They will in turn begin their testing. Once the business users are satisfied with the release, the stage source code will be promoted to the live or production level.

All this is accomplished by means of source code labels in Team Foundation Server Version Control and in a moment I’ll show you how to setup an MSBuild target that will take care of performing this type of promotion for you.

Note that although the source code is promoted, the labels in the version control system are in fact being shifted downwards, as shown in the picture. Also note that for every source code promotion there is a corresponding build process that kicks-off.

The first thing that we need is a set of label names that we will use to identify the various promotion levels. For this post I’m going to use the following label names: pre-stage, stage and live. You might wonder where the label is for tip – there isn’t one. We make use of the special version spec called T, which marks the latest version of the source code in Team Foundation Server Version Control by default and is always present.

I suggest creating a property group for these labels in case they need to change at some later stage.

<propertygroup>
<prestagelabel>pre-stage</prestagelabel>
  <stagelabel>stage</stagelabel>
	<livelabel>live</livelabel>
</propertygroup>

There are rules that we need to have in place before any source code promotion can happen. Here is a summarized list of rules:

  • A valid FromLabel parameter must be specified;
  • A valid ToLabel parameter must be specified;
  • The pre-stage label can only be moved to tip;
  • The stage label can only be moved to pre-stage;
  • The live label can only be moved to stage.

Here they are represented in the build file:

<error Condition="'$(FromLabel)'==''"
	Text="The FromLabel property has not been specified."
/>

<error Condition="'$(ToLabel)'==''"
	Text="The ToLabel property has not been specified."
/>

<!-- check that the label name we are going to move is valid -->
<error Condition="'$(FromLabel)'!='$(PreStageLabel)' and '$(FromLabel)'!='$(StageLabel)' and '$(FromLabel)'!='$(LiveLabel)'"
	Text="Valid FromLabel values are $(PreStageLabel), $(StageLabel) or $(LiveLabel)"
/>

<!-- check sanity : pre-stage label can only move to tip -->
<error Condition="'$(FromLabel)'=='$(PreStageLabel)' and '$(ToLabel)'!='T'"
	Text="$(PreStageLabel) can only be moved to tip (T)."
/>

<!-- check sanity : stage label can only move to pre-stage -->
<error Condition="'$(FromLabel)'=='$(StageLabel)' and '$(ToLabel)'!='$(PreStageLabel)'"
	Text="$(StageLabel) can only be moved to $(PreStageLabel)."
/>

<!-- check sanity : live label can only move to stage -->
<error Condition="'$(FromLabel)'=='$(LiveLabel)' and '$(ToLabel)'!='$(StageLabel)'"
	Text="$(LiveLabel) can only be moved to $(StageLabel)."
/>

At this point we can be fairly sure that we are going to move labels to where they are supposed to be. On with the promotion. The actual process of moving the labels is fairly straight forward.

There are three different cases to consider when moving labels:

Special case #1 – Moving the live label:

I have not yet spoken about the labels that are attached to previously live versions. It might be necessary to track the source code version that was previously live, just in case the product needs to be rolled back for some or other reason. I term this label the previous-live label. If you need to keep track of all the previous live source code, then I suggest creating a custom build task that simply returns a current date/timestamp and mangle this with the previous-live label. If you only need to keep track of the previous version, you can simply just add it to the property group with all the other label names.

If we are moving the live label which is indicated by the FromLabel property, we first need to label the version at the current live label with the previous-live label. This is done with the following command:

    <!-- Create a previous live label -->
    <exec Condition="'$(FromLabel)'=='$(LiveLabel)' and '$(ToLabel)'=='$(StageLabel)' and '$(LiveLabelExists)'=='true'"
        WorkingDirectory="$(SolutionRoot)"
        Command="&quot;$(TfCmd)&quot; label /server:$(TeamFoundationServerName) &quot;$(PreviousLiveLabel)@$(ProjectSourceRootPath)&quot; $(ProjectSourceRootPath) /version:L$(LiveLabel)@$(ProjectSourceRootPath) /recursive"
    />

Property values in the command line:

  • $(TfCmd) points to the location, including tf.exe.
  • $(TeamFoundationServerName) is machine name where Team Foundation Server is installed on.
  • $(ProjectSourceRootPath) is the source control folder of the project, for example $/MyProject.

Note the L in /version:L – which means that we are referring to the source code version labeled with the label that follows L.

Our next step is to move the live label to the stage label, which is done with the following command:

    <!-- Move Live to Stage -->
    <exec Condition="'$(FromLabel)'=='$(LiveLabel)' and '$(ToLabel)'=='$(StageLabel)'"
        WorkingDirectory="$(SolutionRoot)"
        Command="&quot;$(TfCmd)&quot; label /server:$(TeamFoundationServerName) &quot;$(PreviousLiveLabel)@$(ProjectSourceRootPath)&quot; $(ProjectSourceRootPath) /version:L$(LiveLabel)@$(ProjectSourceRootPath) /recursive"
    />

Special case #2 – Moving the pre-stage label to tip:

When moving the pre-stage label to the tip, there is a small change that has to be made to the command line. Referring to the tip version in the versionspec is simply a T and is not prefixed with L – which indicates a label.

Below is the command line that has to be executed when moving from pre-stage to tip:

    <exec Condition="'$(ToLabel)'=='T'"
        WorkingDirectory="$(SolutionRoot)"
        Command="&quot;$(TfCmd)&quot; label /server:$(TeamFoundationServerName) $(FromLabel)@$(ProjectSourceRootPath) $(ProjectSourceRootPath) /version:$(ToLabel) /child:replace /recursive"
    />

General case:

In all the other cases we simply move the source code labels based on the criteria given. The command line for this general case is shown below:

    <exec Condition="'$(FromLabel)'!='$(LiveLabel)' and '$(ToLabel)'!='T'"
        WorkingDirectory="$(SolutionRoot)"
        Command="&quot;$(TfCmd)&quot; label /server:$(TeamFoundationServerName) $(FromLabel)@$(ProjectSourceRootPath) $(ProjectSourceRootPath) /version:L$(ToLabel)@$(ProjectSourceRootPath) /child:replace /recursive"
    />

Note: The above command lines, with the exception of Special case #1, will overwrite previous applied labels. It will also perform a recursive labeling of all the items in the source path with the new label.

And that is all that there is to it!

Tagged with: ,

CopyDirectoryDifferences Build Task

Posted in team foundation server by Sydney du Plooy on September 1, 2008

In my attempt to optimize our build script work, I found myself needing to copy a list of files from one directory to another without overwriting existing files in the destination directory.

Scenario:

We need to gather code coverage information using multiple test containers (assemblies with unit tests inside them). This results in instrumenting all the assemblies that the test container depends on. This happens before the execution of unit tests in each test container.

Solution:

Use the MSBuild copy task to copy all the files from the source directory to the destination directory without overwriting the files that already exists in the destination directory. Simple enough, eh? Except, there is no support for this scenario to be found in the Copy Task that ships with MSBuild by default.

We are left no choice but to roll our own. Here is a code listing of the task that I wrote to do precisely that:

/// <summary>
/// Copies files from a source directory to a destination directory that
/// do not exist in the destination directory.
/// </summary>
public class CopyDirectoryDifference : Task
{
  private string _SourcePath = string.Empty;
  private string _DestinationPath = string.Empty;
  private ITaskItem[] _FilesCopied = {};

  /// <summary>
  /// Gets or sets the source path from where files will be copied.
  /// </summary>
  [Required]
  public string SourcePath
  {
    get { return _SourcePath; }
    set { _SourcePath = value; }
  }

  /// <summary>
  /// Gets or sets the destination path to where files will be copied.
  /// </summary>
  [Required]
  public string DestinationPath
  {
    get { return _DestinationPath; }
    set { _DestinationPath = value; }
  }

  /// <summary>
  /// Gets the files that were copied to the destination path.
  /// </summary>
  [Output]
  public ITaskItem[] FilesCopied
  {
    get { return _FilesCopied; }
  }

  /// <summary>
  /// Finds the files that are different between two directories and copies them from
  /// the source directory to the target directory.
  /// </summary>
  /// <returns>true if the task completed successfully, otherwise false.</returns>
  public override bool Execute()
  {
    if (!Directory.Exists(_SourcePath))
    {
      Log.LogError("Source path does not exist.");
      return false;
    }

    if (!Directory.Exists(_DestinationPath))
    {
      Log.LogError("Destination path does not exist.");
      return false;
    }

    string[] sourceFiles = Directory.GetFiles(_SourcePath);
    string[] destinationFiles = Directory.GetFiles(_DestinationPath);
    List<itaskitem> copiedFiles = new List</itaskitem><itaskitem>();

    foreach (string file in sourceFiles)
    {
      string fileName = Path.GetFileName(file);

      if (!File.Exists(Path.Combine(_DestinationPath, fileName)))
      {
        Log.LogMessage("Copying file {0} from {1} to {2}.", fileName, _SourcePath, _DestinationPath);

	try
	{
	  File.Copy(Path.Combine(_SourcePath, fileName), Path.Combine(_DestinationPath, fileName), false);
	  copiedFiles.Add(new TaskItem(Path.Combine(_SourcePath, fileName)));
	}
	catch (IOException ex)
	{
	  Log.LogError("Failed to copy the file {0}. Exception: {1}", Path.Combine(_SourcePath, fileName), ex.ToString());
	  return false;
	}
      }
    }

    if(copiedFiles.Count > 0)
      _FilesCopied = copiedFiles.ToArray();

    Log.LogMessage("Done.");

    return true;
  }
}

Tagged with: , ,