Custom Versioning Strategy on TFS 2010 for Windows Phone
Join the DZone community and get the full member experience.
Join For FreeRead this tutorial that shows you how to implement the major.minor.build.revision format in Team Foundation Server while developing Windows Phone apps.
Recently, I started to build many Windows Phone 7 applications and I
decided to take the next step and start implementing build automation for Windows
Phone 7 projects. One thing I struggled with is versioning. I wanted to
be able to use a major.minor.build.revision format and I wanted this to be
used on labels and also on drop location folder names.
I find it extremely hard to believe that there are almost no blogs on this particular topic of custom versioning.; although there are a couple of blogs that do this, they still fail to mention the important part of using BuildNumber to create DropLocation and Set LabelName using buildNumber which was my main motivation for this blog. Every organization I work with to help migrate or implement TFS 2008 and TFS 2010 seems to always ask the question of using custom versioning.
Custom versioning is nothing more then being able to control the versioning using the format of {major}.{minor}.{build}.{revision}. The roblem with TFS 2008 and TFS 2010 is that by default it uses $(Date:yyyyMMdd)$(Rev:.r) which almost nobody wants to use at least from my experience working with my clients.
In TFS 2008, it was easy enough to simply override BuildNumberOverrideTarget using a custom MSBuild task. But in TFS 2010 if you decide to use the Workflow approach it is not so obvious and needs little more work. It took me a while to come up with something that was a good solution.
Your final goal after this blog is to create something similar to the below Workflow diagram. You will be adding custom activities after Get Workspace activity as shown below.
So here are your expected final results.
1) Custom build number using major.minor.build.revision.
2) Set LabelName using custom version.
3) Create DropLocation using custom version.
4) Windows Phone 7 Xap file.
It seems like a lot but there will be a reason for all these things. In the following sections, you will learn to 1) customize your build number using custom Workflow Activity, 2) integrate it to Workflow, and 3) build Windows Phone 7 bits.
Preparing the Build Server
Make sure to read my blog on this if you are planning to set up a build server specific to Windows Phone 7.
Assumes
I will assume following for the sake of making this blog more simpler.
1) At least created build definition once before.
2) Has general idea about TFS 2010 and Workflow or glimpsed through following helpful blogs that I enjoy reading:
- Martin Woodward’s TFS 2010 related blogs
- Ewald Hofman’s TFS 2010 Workflow related tutorials
Creating Custom Activities
You can learn about preparing to create custom activities by reading blogs above they cover various best practices on how to do this. You will need four custom activities. And following section will describe what they do and their short snippets of codes.
Checkout
I don’t remember where I got this code originally from but I made some modification of adding search patters to allow to checkout files that matches specific search pattern.
using System.Activities; using System.IO; using Microsoft.TeamFoundation.Build.Client; using Microsoft.TeamFoundation.VersionControl.Client; namespace TfsBuildActivitiesLib.Activities { [BuildActivity(HostEnvironmentOption.Agent)] public sealed class Checkout : CodeActivity { // The file mask of all files for which the buildnumber of the // AssemblyVersion must be increased [RequiredArgument] public InArgument<string> SearchPatterns { get; set; } // The workspace that is used by the build [RequiredArgument] public InArgument<Workspace> Workspace { get; set; } protected override void Execute(CodeActivityContext context) { // Obtain the runtime value of the input arguments string[] searchPatterns = context.GetValue(this.SearchPatterns).Split(‘;’); Workspace workspace = context.GetValue(this.Workspace); // Checks all files out in the workspace that apply to the file mask // For every workspace folder (mapping) foreach (var folder in workspace.Folders) { foreach (var searchPattern in searchPatterns) { // Get the files that apply to the mask on the local system foreach (var file in Directory.GetFiles(folder.LocalItem, searchPattern, SearchOption.AllDirectories)) { // Check all those file out workspace.PendEdit(file); } } } } } }
SetCustomVersion
In order to use your own custom version, you would need to have file where you can store version that you know it will be there so that you can use it to increment or control the version by changing major or minor version. I noticed from experience that the companies like to control either major or minor or both of the numbers.
I typically put the version.txt at the root of the folder where the solution file is where I want that product to have specific version and I track these version using version.txt as shown below.
To manipulate version.txt and get new version custom activity you will use an OutArgument called NewVersion as shown in below code. NewVersion will be accessible to the Workflow and you will be using it to change the BuildNumber and DropLocation found in BuildDetail using SetBuildPropties TFS activity.
using System; using System.Activities; using System.IO; using Microsoft.TeamFoundation.Build.Client; namespace TfsBuildActivitiesLib.Activities { [BuildActivity(HostEnvironmentOption.Agent)] public sealed class SetCustomVersion : CodeActivity { [RequiredArgument] public InArgument<IBuildDetail> BuildDetail { get; set; } [RequiredArgument] public InArgument<string> VersionText { get; set; } // i.e. wp7\duckcaller\version.txt [RequiredArgument] public InArgument<string> SourceDir { get; set; } // i.e. wp7\duckcaller\version.txt public OutArgument<string> NewVersion { get; set; } // i.e. wp7\duckcaller\version.txt protected override void Execute(CodeActivityContext context) { var srcDir = SourceDir.Get(context); var versionText = VersionText.Get(context); var versionFile = Path.Combine(srcDir, versionText); if (File.Exists(versionFile)) { Version version = new Version(File.ReadAllText(versionFile)); Version newVersion = new Version(version.Major, version.Minor, version.Build + 1, 0); var buildDetail = BuildDetail.Get(context); File.WriteAllText(versionFile, newVersion.ToString()); this.NewVersion.Set(context, newVersion.ToString()); } else { throw new ArgumentException(versionFile + “Does not exist!”); } } } }
SetAssemblyVersion
This custom activity will search through DirectoryList (contains semicolon delimited list of directories relative to the source directory) and stamp AssemblyInfo.cs files with the version as show in below code.
using System.Activities; using Microsoft.TeamFoundation.Build.Client; using Microsoft.TeamFoundation.Build.Workflow.Activities; using System.ComponentModel; using System.IO; using System.Text.RegularExpressions; namespace TfsBuildActivitiesLib.Activities { [BuildActivity(HostEnvironmentOption.All)] [DisplayName("Set Assembly Version Task")] public sealed class SetAssemblyVersion : CodeActivity { [RequiredArgument] public InArgument<string> BuildNumber { get; set; } [RequiredArgument] public InArgument<string> DirectoryList { get; set; } // ; delimited (i.e root\dir3;c:\root\dir4) [RequiredArgument] public InArgument<string> SearchPatterns { get; set; } // ; delimited (i.e assemblyinfo.*;assembly.cs) [RequiredArgument] public InArgument<string> SourcesDirector { get; set; } protected override void Execute(CodeActivityContext context) { // Build Number string buildNumber = context.GetValue<string>(BuildNumber); context.TrackBuildMessage(“Preparing For Setting Assembly Version: “ + buildNumber, BuildMessageImportance.High); string sourceDir = SourcesDirector.Get(context); // Get directories to search string dirList = DirectoryList.Get(context); string[] directories = string.IsNullOrEmpty(dirList) ? null : dirList.Split(‘;’); context.TrackBuildMessage(“Searching Directories: “ + dirList, BuildMessageImportance.High); // Get Assembly File Names string assemblyInfoFileMasks = SearchPatterns.Get(context); if (!string.IsNullOrEmpty(assemblyInfoFileMasks)) { context.TrackBuildMessage(“Setting Assemblies: “ + assemblyInfoFileMasks, BuildMessageImportance.High); foreach (string dir in directories) { DirectoryInfo dirInfo = new DirectoryInfo(Path.Combine(sourceDir, dir)); foreach (string assemblyInfoFileMask in assemblyInfoFileMasks.Split(‘;’)) { foreach (FileInfo file in dirInfo.GetFiles(assemblyInfoFileMask, SearchOption.AllDirectories)) { context.TrackBuildMessage(string.Format(“Setting version on {0}”, file.FullName), BuildMessageImportance.High); ChangeAssemblyVersion(file, buildNumber); } } } } } private void ChangeAssemblyVersion(FileInfo assemblyFile, string buildNumber) { string contents = string.Empty; using (StreamReader reader = assemblyFile.OpenText()) { contents = reader.ReadToEnd(); reader.Close(); } string newAssemblyVersion; string newAssemblyFileVersion; if (assemblyFile.Extension.ToLower().Equals(“.cs”)) { // c# newAssemblyVersion = “[assembly: AssemblyVersion(\"" + buildNumber + "\")]“; newAssemblyFileVersion = “[assembly: AssemblyFileVersion(\"" + buildNumber + "\")]“; contents = Regex.Replace(contents, @”\[assembly: AssemblyVersion\("".*""\)\]“, newAssemblyVersion); contents = Regex.Replace(contents, @”\[assembly: AssemblyFileVersion\("".*""\)\]“, newAssemblyFileVersion); } else { // vb newAssemblyVersion = “<Assembly: AssemblyVersion(\”" + buildNumber + “\”)>”; newAssemblyFileVersion = “<Assembly: AssemblyFileVersion(\”" + buildNumber + “\”)>”; contents = Regex.Replace(contents, @”\<Assembly: AssemblyVersion\(“”.*”"\)\>”, newAssemblyVersion); contents = Regex.Replace(contents, @”\<Assembly: AssemblyFileVersion\(“”.*”"\)\>”, newAssemblyFileVersion); } using (StreamWriter writer = new StreamWriter(assemblyFile.FullName, false)) { writer.Write(contents); writer.Close(); } } } }
Checkin
I also got this code from somewhere I don’t remember where I got them so please write a comment below to give credit for this code. This task will check in all the files that are check out in current Workspace and code is shown below. Always keep in mind to put “***NO_CI***” comment if you are doing continuous integration.
using System.Activities; using Microsoft.TeamFoundation.Build.Client; using Microsoft.TeamFoundation.VersionControl.Client; using Microsoft.TeamFoundation.Build.Workflow.Activities; namespace TfsBuildActivitiesLib.Activities { [BuildActivity(HostEnvironmentOption.Agent)] public sealed class Checkin : CodeActivity { // The workspace that is used by the build [RequiredArgument] public InArgument<Workspace> Workspace { get; set; } protected override void Execute(CodeActivityContext context) { // Obtain the runtime value of the input arguments Workspace workspace = context.GetValue(this.Workspace); context.TrackBuildMessage(workspace.Name, BuildMessageImportance.High); // Checks all files in in the workspace that have pending changes // The ***NO_CI*** comment ensures that the CI build is not triggered (and that // you end in an endless loop) workspace.CheckIn(workspace.GetPendingChanges(), “Build Agent”, “***NO_CI***”, null, null, new PolicyOverrideInfo(“Auto checkin”, null), CheckinOptions.SuppressEvent); } } }
Using BuildNumber to Set DropLocation
There are blogs that will show you how to customize the build but not with specific case of using major, minor, build and revision. And most importantly how to make sure DropLocation uses the BuildNumber.
If you look at the default build template creation of DropLocation happens after Update Build Number task as shown in below figure.
Here is the problem. You have to read version.txt (whatever mechanism you use to read previous version so you can increment the version number) has to happen before you can Set Drop Location and Create the Drop Location activities as shown in above.
To over come this problem you need to do two things.
1. First move three activities: Set Drop Location, Create Drop Location and If Drop Build and Build Reason is validatShelvset activities to Custom Version sequence shown in very first figure shown at the top.
2. After you moved the shapes down you need to modify Set Drop Location and Set Drop Location Private to include BuildNumber as shown in below diagram.
And then Set the BuildNumber using NewVersion that is the output of the SetAssemblyVersion activity and change DropLocation to following.
BuildDetail.DropLocationRoot + “\” + BuildDetail.BuildDefinition.Name + “\” + NewVersion
Define Workflow Arguments and Create Custom Build Properties and Build Definition
In above custom activities you coded you will notice that the public properties with RequiredArgument attributes. These properties are expected to pass into the custom activities from the Workflow and you must define the arguments and then create Process Parameter Metadata so that it can be set when you are creating build definition. See below for process of creating arguments to be passed into the activities and creating metadata to be passed in from the build definition.
Not metadata is exposed to the build definition level where you can customize with greater flexibility as show below.
Final Result
So here is the final result as shown in figure below.
1) Custom build number using major.minor.build.revision.
2) Set LabelName using custom version.
3) Create DropLocation using custom version.
4) Windows Phone 7 Xap file. if you properly prepared Windows Phone 7 build server you will see Xap file in DropLocation when you build the solution that contains Windows Phone 7 project.
Download Code
TFS XAML
Conclusion
In this blog you built a Windows Phone 7 project in TFS 2010 using
Workflow. As part of the process you learned to create custom version
(major.minor.build.revision), set custom version as label, and use
custom version to create drop location.
Source: http://blog.toetapz.com/2010/12/02/custom-versioning-strategy-on-tfs-2010-using-workflow-for-windows-phone-7-application/
Opinions expressed by DZone contributors are their own.
Comments