Set assembly and app version to a matching build name in TFS 2015 or VSO Build.VNext

One of the most common customizations in TFS XAML build templates was to automatically update the assembly version number. This can also be done in build vNext using a small power shell script. I also added the option to apply the version to SharePoint / Office 365 apps.

The out of the box build name for VNext builds is not very descriptive. It’s just a number that consists of the current date and a revision number: $(date:yyyyMMdd)$(rev:.r)
If you have many build this is not very helpful.  So its always a good thing to customize you build name and at least add the build definition name (you can use the variable  $(BuildDefinitionName) for this) to it. You can do this on the “General” tab under “Build Number Format”.

image

What you can do is to add variables that you define on the variable tab – like major and minor version.

image

You can then use the variables in the name of the build. Here is an example that consists of the build definition name, a major and minor version that are configured via build variables and a revision number that is build from the year and “dayofyear” to ensure a valid sorting of the builds:

$(BuildDefinitionName)_$(MajorVersion).$(MinorVersion).$(Year:yy)$(DayOfYear)$(rev:.rr)

You can then use a PowerShell script Set-AssemblyVersion.ps1 in a VNext build to set all assembly versions – and optionally the version of SharePoint / O365 Apps – to a build number that is extracted from the build name using a regular expression.

<#
.Synopsis
   Sets the assembly version of all assemblies in the source directory.
.DESCRIPTION
   A build script that can be included in TFS 2015 or Visual Studio Online (VSO) vNevt builds that update the version of all assemblies in a workspace.
   It uses the name of the build to extract the version number and updates all AssemblyInfo.cs files to use the new version.
.EXAMPLE
   Set-AssemblyVersion
.EXAMPLE
   Set-AssemblyVersion -SourceDirectory $Env:BUILD_SOURCESDIRECTORY -BuildNumber $Env:BUILD_BUILDNUMBER
.EXAMPLE
   Set-AssemblyVersion -SourceDirectory ".\SourceDir" -BuildNumber "Dev_1.0.20150922.01" -VersionFormat "\d+\.\d+\.\d+\.\d+"
#>

[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
[Alias()]
[OutputType([int])]
Param
(
    # The path to the source directory. Default $Env:BUILD_SOURCESDIRECTORY is set by TFS.
    [Parameter(Mandatory=$false, Position=0)]
    [ValidateNotNullOrEmpty()]
    [string]$SourceDirectory = $Env:BUILD_SOURCESDIRECTORY,

    # The build number. Default $Env:BUILD_BUILDNUMBER is set by TFS and must be configured according your regex.
    [Parameter(Mandatory=$false, Position=1)]
    [ValidateNotNullOrEmpty()]
    [string]$BuildNumber = $Env:BUILD_BUILDNUMBER,

    # The build number. Default $Env:BUILD_BUILDNUMBER is set by TFS and must be configured according your regex.
    [Parameter(Mandatory=$false, Position=2)]
    [ValidateNotNullOrEmpty()]
    [string]$VersionFormat = "\d+\.\d+\.\d+\.\d+",

    # Set the version number also in all AppManifest.xml files.
    [Parameter(Mandatory=$false)]
    [switch]$SetAppVersion
)

<#
.Synopsis
   Sets the assembly version of all assemblies in the source directory.
.DESCRIPTION
   A build script that can be included in TFS 2015 or Visual Studio Online (VSO) vNevt builds that update the version of all assemblies in a workspace.
   It uses the name of the build to extract the version number and updates all AssemblyInfo.cs files to use the new version.
.EXAMPLE
   Set-AssemblyVersion
.EXAMPLE
   Set-AssemblyVersion -SourceDirectory $Env:BUILD_SOURCESDIRECTORY -BuildNumber $Env:BUILD_BUILDNUMBER
.EXAMPLE
   Set-AssemblyVersion -SourceDirectory ".\SourceDir" -BuildNumber "Dev_1.0.20150922.01" -VersionFormat "\d+\.\d+\.\d+\.\d+"
#>
function Set-AssemblyVersion
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    [Alias()]
    [OutputType([int])]
    Param
    (
        # The path to the source directory. Default $Env:BUILD_SOURCESDIRECTORY is set by TFS.
        [Parameter(Mandatory=$false, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$SourceDirectory = $Env:BUILD_SOURCESDIRECTORY,

        # The build number. Default $Env:BUILD_BUILDNUMBER is set by TFS and must be configured according your regex.
        [Parameter(Mandatory=$false, Position=1)]
        [ValidateNotNullOrEmpty()]
        [string]$BuildNumber = $Env:BUILD_BUILDNUMBER,

        # The build number. Default $Env:BUILD_BUILDNUMBER is set by TFS and must be configured according your regex.
        [Parameter(Mandatory=$false, Position=2)]
        [ValidateNotNullOrEmpty()]
        [string]$VersionFormat = "\d+\.\d+\.\d+\.\d+",

        # Set the version number also in all AppManifest.xml files.
        [Parameter(Mandatory=$false)]
        [switch]$SetAppVersion
    )

    if (-not (Test-Path $SourceDirectory)) {
        throw "The directory '$SourceDirectory' does not exist."
    }

    $Version = Get-Version -BuildNumber $BuildNumber -VersionFormat $VersionFormat

    $files = Get-Files -SourceDirectory $SourceDirectory

    Set-FileContent -Files $files -Version $Version -VersionFormat $VersionFormat

    if ($SetAppVersion.IsPresent)
    {
        $files = Get-AppManifest -SourceDirectory $SourceDirectory
        Set-AppManifest -Files $files -Version $Version
    }
}

function Get-Version
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([string])]
    Param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$BuildNumber,

        [Parameter(Mandatory=$true, Position=1)]
        [ValidateNotNullOrEmpty()]
        [string]$VersionFormat
    )

    $VersionData = [regex]::matches($BuildNumber,$VersionFormat)

    if ($VersionData.Count -eq 0){
        throw "Could not find version number with format '$VersionFormat' in BUILD_BUILDNUMBER '$BuildNumber'."
    }

    return $VersionData[0]
}

function Get-Files
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([System.IO.FileSystemInfo[]])]
    Param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$SourceDirectory
    )

    $folders = Get-ChildItem $SourceDirectory -Recurse -Include "*Properties*" | Where-Object { $_.PSIsContainer }

    return $folders | ForEach-Object { Get-ChildItem -Path $_.FullName -Recurse -include AssemblyInfo.* }
}

function Get-AppManifest
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([System.IO.FileSystemInfo[]])]
    Param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$SourceDirectory
    )

    return Get-ChildItem -Path $SourceDirectory -Recurse -Filter "AppManifest.xml"
}

function Set-FileContent
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    [OutputType([int])]
    Param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [System.IO.FileSystemInfo[]]$Files,

        [Parameter(Mandatory=$true, Position=1)]
        [string]$Version,

        [Parameter(Mandatory=$true, Position=2)]
        [string]$VersionFormat
    )

    foreach ($file in $Files)
    {
        $filecontent = Get-Content $file

        if ($PSCmdlet.ShouldProcess("$file", "Set-AssemblyVersion"))
        {
            attrib $file -r
            $filecontent -replace $VersionFormat, $Version | Out-File $file
            Write-Verbose -Message "Applied Version '$Version' $($file.FullName) - version applied"
        }
    }
}

function Set-AppManifest
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    [OutputType([int])]
    Param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [System.IO.FileSystemInfo[]]$Files,

        [Parameter(Mandatory=$true, Position=1)]
        [string]$Version
    )

    foreach ($file in $Files)
    {
        [xml]$xml = Get-Content $file

        $xml.App.Version = $Version

        if ($PSCmdlet.ShouldProcess("$file", "Set-AppManifest")){
            $xml.Save($file.FullName)
        }
    }
}

if (-not ($myinvocation.line.Contains("`$here\`$sut"))) {
    Set-AssemblyVersion -SourceDirectory $SourceDirectory -BuildNumber $BuildNumber -VersionFormat $VersionFormat
}

Just add a “PowerShell Script” build step for your actual build step and reference the script in your repository.

image

I really love the new build system. It’s so easy and customizable. With a little script as the starting point you can configure everything according to the needs of your customers.

15 comments

  1. Man.
    I would like to thank you for that post. But there is something that make me lose my mind for some time.
    in the script the regex you show is “\d+\.\d+\.\d+\.\d+” but in the examples the build number is build with that line
    $(BuildDefinitionName)_$(MajorVersion).$(MinorVersion)_$(Year:yy)$(DayOfYear)$(rev:.rr) that definition will not work with the regex because there is an “_” insted of a dot (“.”) that make me craaazy for a while 🙂 but i eventualy findout the issue and change that for $(BuildDefinitionName)_$(MajorVersion).$(MinorVersion).$(Year:yy)$(DayOfYear)$(rev:.rr) and it work perfectly.
    Thanks.

  2. Hi,

    Can I set the product version to a string? lets said “My software Beta 1 [1234]”.

    Thanks & Best Regards,
    Ken

    1. What do you mean with product version? The build name can be a string – as long as you can extract the version numbers for the assemblies. The assembly version must match the pattern Major.Minor.Build.Revision like 1.0.0.0

  3. I am trying to assign the build number from TFS build definition, but the assembly is a ASP.NET Core project. Then the version is gather from project.json. So your script doesn’t apply ¿right?

    1. You have to modify Get-Files to return the Project.json instead of the AssemblyInfo.cs. Then check that your regex in $VersionFormat is correct that the Set-FileContent does not mess up your file.

  4. I tried this, and the Script writes that the Version is applied.
    (Applied Version ‘1.3.16244.01’ C:\Users\Josma\Downloads\agent(1)\_work\1\s\source\mbit.merpa.common\Properties\AssemblyInfo.cs – version applied)
    The ‘Visual Studio-Build’ Task starts after the PowerShell Task

    But in my case there is almost the old version Number in the *.dll and *.exe files. All AssemblyVersion Files have there Originial Versionnumber.

    What mistake i had made?
    Must i also define a Variable in the Assemblyversion.cs file?
    in my case there stands:
    [assembly: AssemblyVersion(“1.2.*”)]

  5. I am able to run the script in TFS 2015 successfully and make the changes locally. I added code to open connection to TFS and check in the code, but changes are not applied into TFS.

    $url = “$($env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI)$env:SYSTEM_TEAMPROJECTID/_apis/build/definitions/$($env:SYSTEM_DEFINITIONID)?api-version=2.0”
    Write-Host “URL: $url”
    $definition = Invoke-RestMethod -Uri $url -Headers @{
    Authorization = “Bearer $env:SYSTEM_ACCESSTOKEN”
    }
    Write-Host “Definition = $($definition | ConvertTo-Json -Depth 1000)”

    1. What do you mean with changes are not applied? If you want your repository to reflect the versions you have to do a “git commit” “git push” or use git version (see link above).

  6. Thanks Mike for your reply. I was trying to say I want the changes reflected in repo, our repo is Team Foundation Version Control.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s