P3.NET

Create a Build Task for TFBuild, Part 2

In the last post I demonstrated how to create a build task for TFBuild that showed the build variables. I also demonstrated how to wrap the task in a build extension that could be installed in TFS, on-premise. In this post we’ll add another task to the extension. This serves two purposes. Firstly it demonstrates hosting multiple tasks in a single extension. Secondly it demonstrates a more common build task, versioning assemblies.

UPDATE: There is an updated version that uses the VSTS SDK and hashtables. The issues I originally had trying to get hashtables to work with VSTS SDK have seemed to been cleared up.

Versioning Assemblies

In general most companies like to version all the assemblies in a build with the same number.Furthermore the version tends to follow a major.minor pattern. The remaining two values (generally known as build and revision) vary by company. For our purposes the build number will be the Julian date. The revision will be the build attempt for that day. TFS already provides this information in the default build format so we do not need to calculate it.

To properly version assemblies in .NET we need to set a series of attributes on the assembly. The task should set the following.

AssemblyCompany
The company name.
AssemblyConfiguration
The build configuration (i.e. Debug, Beta, etc.).
AssemblyCopyright
Copyright information.
AssemblyFileVersion
Full file version (i.e. 4.5.17004.2).
AssemblyInformationalVersion
Informational version number (i.e. 4.5.17004.2).
AssemblyProduct
The product name.
AssemblyTrademark
Trademark information.
AssemblyVersion
Product version (i.e. 4.5.0.0)

Almost all of these attributes are generated in the standard AssemblyInfo.cs file of a C# project. Therefore the versioning process can look for all files that match this pattern and replace the existing attribute values with the values calculated by the build.

Creating the New Task

Like last time, we’ll create a new task using tfx. This task we’ll call VersionAssemblies. We’ll place it in a subfolder (called VersionAssemblies) where the previous task resides. Like the other task we’ll do the following.

  • Remove the JavaScript file.
  • Rename the sample.ps1 to versionAssemblies.ps1.
  • Update the task.json file to contain the relevant information about the new task.
    • Change instanceNameFormat to “Version Assemblies”.
    • Remove the inputs, for now.
    • Add the execution value set to use Powershell and the Powershell script file.
"execution": {
   "PowerShell3": {
      "target": "versionAssemblies.ps1"	
   }  
}

Defining the Parameters

For this task we’ll want parameters for each of the values we’ll be generating. Most of them are defined per build definition. The major and minor version numbers will change as products are released. The build number should be calculated but sometimes it is useful to build a specific version (i.e. hotfix) so we’ll allow setting this value as well. We’ll break the parameters up into those that are commonly changed for each definition and those that are rarely changed (i.e. company name, etc.). Here’s the final inputs and groups. Adjust these to suit your needs.

"groups": [
   {
      "name": "product",
      "displayName": "Product",
      "isExpanded": true
   },
   {
      "name": "advanced",
      "displayName": "Advanced",
      "isExpanded": false
   }  
],
"inputs": [
  {
     "name": "product",
     "type": "string",
     "label": "Product",
     "defaultValue": "",
     "required": true,
     "helpMarkDown": "The product name.",
     "groupName" : "product"    
 },
 {
     "name": "major", 
     "type": "string",   
     "label": "Major Version",     
     "defaultValue": "1",      
     "required": true,
     "helpMarkDown": "The major version number. Update this value for each major release.",
     "groupName" : "product"
 },   
 {
     "name": "minor",
     "type": "string",
     "label": "Minor Version",
     "defaultValue": "0",
     "required": true,   
     "helpMarkDown": "The minor version number. Update this value for any new release. Reset to 0 when the major version is changed.",
     "groupName" : "product"
 },
 {
      "name": "build",
      "type": "string",
      "label": "Build Version",
      "defaultValue": "",
      "required": false,
      "helpMarkDown": "The build version number. This is managed by TFS but can be overridden if needed.",
      "groupName" : "advanced"
 },
 {
      "name": "assemblyInfoFilePattern",
      "type": "string",
      "label": "Assembly Info File Pattern",
      "defaultValue": "AssemblyInfo.*",
      "required": true,
      "helpMarkDown": "The file pattern for files containing the assembly information to be updated.",
      "groupName" : "advanced"
 },
 {  
      "name": "configuration",
      "type": "string",
      "label": "Configuration",
      "defaultValue": "",
      "required": false,
      "helpMarkDown": "The configuration for this release.",
      "groupName" : "advanced"
 },
 {
      "name": "company",
      "type": "string",
      "label": "Company",
      "defaultValue": "P3Net",
      "required": true,
      "helpMarkDown": "The company name.",
      "groupName" : "advanced"
 },
 {
      "name": "trademark",
      "type": "string",
      "label": "Trademark",
      "defaultValue": "",
      "required": false,
      "helpMarkDown": "The trademark notice.",
      "groupName" : "advanced"
 }
],

The Powershell Script

The script itself is too long to put here so we’ll just highlight some of the details. It uses the VSTS SDK like the other task to simplify some of the work. The flow is as follows.

  1. Calculate each of the attribute values based upon the inputs to the script.
  2. Calculate the version information.
    1. Major and minor are inputs.
    2. If build is specified then use it otherwise calculate the Julian date.
    3. Revision is obtained by extracting the revision number from the build format of TFS (assumes you are using the default).
    4. Store the full/long version (major.minor.build.revision) and the product/short version (major.minor.0.0) for later use.
  3. For each file that matches the assembly info file pattern.
    1. Remove any read-only attributes from the file.
    2. Enumerate each of the lines looking for a matching attribute.
    3. If the attribute is found then replace the attribute with the new value.
    4. If the attribute is not found then insert it into the file.
    5. Save the file.
  4. Store the full (version_full) and product (version_product) versions into build variable in case any other tasks need them.

One caveat to this solution is with storing the value mappings that are needed for replacement. Powershell supports hash tables out of the box using a simple syntax. But for some reason when you use VSTS SDK in the script you get a runtime error as Powershell now sees the hash table as an array. To work around this issue I had to resort to using a fully qualified dictionary type when declaring the variable and remove any hash table references when passing as a parameter.

Adding to the Extension

Now that the tasks is written we need to add it to the extension. Open the vss-extension.json file.

  1. Increment the version number.
  2. Add a new path under files.
  3. Add a new contribution under contributions.
"files": [
   {
       "path": "ShowVariables"
   },    
   {
       "path": "VersionAssemblies"
   }
 ],
"contributions": [
  {
      "id": "p3net-showvariables-task",
      "type": "ms.vss-distributed-task.task",
      "targets": [
           "ms.vss-distributed-task.tasks" 
       ],
       "properties": {
            "name": "ShowVariables"
       }
   },
   {
       "id": "p3net-versionassemblies-task",
       "type": "ms.vss-distributed-task.task",
       "targets": [
             "ms.vss-distributed-task.tasks"
        ],
        "properties": {
              "name": "VersionAssemblies"
        }
    }
]

Now build the extension using tfx extension create –-manifest-globs vss-extension.json.

Testing

Deploy the extension into TFS by installing or updating it. Then go to a build definition and test it out. Editing a build definition should show the new task. Add it to the build (after the NuGet restore but before any build process). Set the parameters based upon your needs. Then build the solution. You should be able to see any messages in the output. If something goes wrong then turn on verbose or debug messages.

If you need to make any changes then remember to do the following.

  1. Increment the task version.
  2. Increment the extension version.
  3. Build the extension using tfx.
  4. Deploy the extension to TFS.

Enhancements

The versioning task is just a starter task. There are some enhancements that can be made.

  1. The current implementation enumerates through each attribute and then each line of the file. This is done as a convenience because each attribute has to be inserted if it is not found. Ideally however we would enumerate each line once and apply any changes as needed.
  2. The attributes are replaced by using regular expression matching. This doesn’t work for some cases (such as non-C#) nor will it honor attributes that are defined in a non-standard way. It replaces the entire attribute value. Ideally it should instead just replace the value in the attribute.
  3. More than just attributes need the version and product information. It would be useful to generate a set of compile-time constants containing the calculated values that can be used in things like other attributes. However the code would need to compile without running through the task so any constants would need to be replaced just like the attributes are today.
  4. It would be nice to be able replace version information in things other than source files. Nuspec files, pkgdef files, templates and other files come to mind. That isn’t possible today although the build variables allow for this replacement in a custom task. It would be nice if we could do the replacement using pre-defined values, again, needing to ensure the code will compile without running through the custom task.

Download the code.

Comments