13

Just wondering what's the best approach to versioning of .NET builds?

I use:

  • TFS 2013 for version control
  • TFS gated check-ins
  • Wix 3.8 to package code to MSI files

I want to set version of:

  • assemblies (AssemblyInfo.cs, or a shared one referenced in all projects)
  • MSI packages (in Wix code)
  • documentation (for example inside readme.txt file in the final output of the build)
  • etc

Ideal version number would allow tracing installed software back to the exact source code.
Something like:

<Major>.<Minor>.<TFS_changeset_number>

First 2 parts of the version I want to store in some simple text \ XML file in version control near the solution, as I believe they should live together. Developers will update this file manually (for example following Semantic Versioning approach). Each build will read this version file, get 3d part of the version from the calling CI tool, and update all the necessary files with the version.

What's the best way to implement this?

I've used a few approaches in the past:

1) NAnt \ MsBuild wrapper that does this version work, then calls MsBuild for the solution. It could be called from CI tool (Jenkins \ TeamCity \ etc).

Problem - integration with TFS gated check-in is ugly as I build solution twice.

2) customize TFS build process template

Problem - it's not that simple, and causes some merge work on TFS upgrades. Also changeset number doesn't exist yet in gated check-ins, so we can only use the previous changeset id.

3) A separate MsBuild project in solution, which does only this versioning task, and is configured to run first in Project Build Order of the VS solution.

Problem - need to reference this meta-project in all other projects (including all future ones) which feel ugly

I know different MsBuild and TFS extension packs that can simplify updates. This topic is not about which one is the best. The question is more methodological than technical.

I also think that it would be ideal if Microsoft include something for versioning in their standard TFS build template. Other CI tools already have this functionality (AssemblyInfo patcher).




UPDATE 11/09/2014

I've decided to clearly express the Versioning Principles that will conform to the best practices of Agile \ Continuous Delivery:

1) Ability to reproduce any historic build

2) As a consequence of 1) and according to CD principles everything (source code, tests, app configs, env configs, build\package\deploy scripts, etc) is stored under version control and so has a version assigned to it

3) Version number is stored tightly together with the source code it applies to

4) People are able to update version according to their business\marketing logics

5) There is only 1 master copy of the version, which is used in all parts of automated build\packaging process

6) You can easily say which Version of the software is currently installed on the target system

7) Version of the installed software must unambiguously identify the source code that was used to build it

8) It's very simple to compare versions to say which is lower and which is higher - to control which upgrade\downgrade scenarios are allowed and implementation specifics of them




UPDATE 15/09/2014

See my own answer below.
I was lucky to find the solution that meets all my requirements!

Community
  • 1
  • 1
Ivan
  • 9,089
  • 4
  • 61
  • 74
  • Have you looked into T4 text templates? A central template can provide default and generated values. A project-local file would provide assembly specific attributes. – Darcara Sep 10 '14 at 15:10
  • Darcara, thanks for the interesting idea. Just read about T4 - do you mean to use build-time templates for plugging in version number? I'll need to read more about this and think over )) – Ivan Sep 10 '14 at 15:39

8 Answers8

6

Good grief, all those complicated answers.

In TFS 2013 this is simple. The Community TFS Build Extensions site offers a simple PowerShell to pull the build number from the build name that is assigned by TFS.

enter image description here

You configure your build number format to be "$(BuildDefinitionName)_6.0.0$(Rev:.r)" which will result in something like "6.0.0.1" where the "1" is incremented for each build.

You then add the PowerShell versioning script to your build and it automagically scrapes the version number above and applies it to all AssemblyInfo.* files in the build folder. You can add additional file types by updating the script.

  • 1
    MrHinsh, if you rebuild old code with the current build definition it will get a newer version which is bad. Versions should be aligned with the source code revision history, not build history. I want to be able to reproduce any historic build and get the same result. – Ivan Sep 11 '14 at 08:21
  • IvanL If you want to rebuild an old code then you have to change the build definition to get a specific code. This should be save as a different build definition – ds19 Sep 11 '14 at 10:08
  • Even better would be to go back to the original Build you want to 're-run' and, as you marked it as 'retain indefinably' you can just run it again. – MrHinsh - Martin Hinshelwood Sep 11 '14 at 10:48
  • +1 for using community build extensions. Especially if you can devise a way to ensure the local build behaves similarly to the TFS build. These scripts didn't exist (or we didn't come across them) when we built our solution for versioning 3+ years ago. I will investigate replacing our bespoke solution with this one as soon as time permits - I would love to remove the dependency on the build number format from my build scripts and have definition name in the build number. – d3r3kk Sep 11 '14 at 16:55
  • ds19, in TFS 2013 to rebuild the old code - Queue new build... > Parameters tab > Get version. No need to edit\clone build definition. – Ivan Sep 12 '14 at 10:02
  • MrHinsh, I don't want to retain builds indefinitely. In active projects it will use too much space. Hence rebuilding from source code. Branches vs Contiunous Delivery is a separate religious discussion, I don't wanna go there. Ability to reproduce a build can be beneficial for both ways. – Ivan Sep 12 '14 at 10:06
  • You only retain build indefinitely if you ship them – MrHinsh - Martin Hinshelwood Sep 12 '14 at 13:32
  • Do you manually click "Retail indefinitely"? What if you go and your substitute forget to do this? Is this step documented? The way I see Build Manager role is to develop a system that delivers software to production automatically and reliably, efficiently connecting developers and users. I hate support type of work - for systems with lots of hard-coded things, manual error-prone steps, etc. I better create something that works by itself, get my money and move on to an even more interesting job. – Ivan Sep 12 '14 at 15:24
  • I usually use Release Manager for Visual Studio and have it automatically set the retain indefinably. You can do it with PowerShell so it would support DeployIt, Octopus or whatever... – MrHinsh - Martin Hinshelwood Sep 12 '14 at 15:35
  • That's another unnecessary customization. In my case I don't use Release Manager and not planning. Your answer is NOT simple, it relies on a lot of assumptions that you didn't even describe. – Ivan Sep 16 '14 at 10:18
  • If you want a consistent and reliable deployment of your product you should use a Release Management tool. Any release management tool would be able to update the retain indefinably in the event that the build output flowing down the release pipeline is deamed viable. RM is part of Visual Studio and included in your MSDN. – MrHinsh - Martin Hinshelwood Sep 16 '14 at 17:58
3

I came up with a solution that meets all my requirements, and surprisingly quite simple one!

IDEA

Put all custom Versioning work into a custom Version.proj MsBuild script and call it in TFS build definition before the .sln. The script injects Version into source code (SharedAssemblyInfo.cs, Wix code, readme.txt), and then solution build builds that source code.

Version is formed from Major and Minor numbers living in Version.xml file stored in TFS together with the source codes; and from Changeset Number supplied as TF_BUILD_SOURCEGETVERSION env var by parent TFS Build process

enter image description here

Thanks Microsoft for this:

  • TFS 2013 - passes TF_BUILD environment variables to the build process, this is how I get changeset number of the current code being built
  • MsBuild allows inline tasks in C# - to replace version in source files using Regex C# class

So there is no need to use any MsBuild or TFS community\extension packs\addons\whatever. And there is no need to modify standard TFS build process template. Simple solution leads to high maintainability!

IMPLEMENTATION

Version.proj

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<!--
Run this script for every build in CI tool before building the main solution
If you build in TFS, simply add script as the first item in a list of projects under Process tab > Build > Projects
-->

  <PropertyGroup>
    <VersionFile>..\Version.xml</VersionFile>
    <MainProjectDir>... set this to main solution directory ...</MainProjectDir>
  </PropertyGroup>

  <Import Project="$(VersionFile)"/>
  <Import Project="Common.proj"/>

  <Target Name="GetMajorMinorNumbers">
    <Error Text="ERROR: MajorVersion is not set in $(VersionFile)" Condition="'$(MajorVersion)' == ''" />
    <Message Text="MajorVersion: $(MajorVersion)" />

    <Error Text="ERROR: MinorVersion is not set in $(VersionFile)" Condition="'$(MinorVersion)' == ''" />
    <Message Text="MinorVersion: $(MinorVersion)" />
  </Target>


  <Target Name="GetChangesetNumber">
    <Error Text="ERROR: env var TF_BUILD_SOURCEGETVERSION is not set, see http://msdn.microsoft.com/en-us/library/hh850448.aspx" Condition="'$(TF_BUILD_SOURCEGETVERSION)' == ''" />
    <Message Text="TF_BUILD_SOURCEGETVERSION: $(TF_BUILD_SOURCEGETVERSION)" />
  </Target>


  <Target Name="FormFullVersion">
    <PropertyGroup>
        <FullVersion>$(MajorVersion).$(MinorVersion).$(TF_BUILD_SOURCEGETVERSION.Substring(1))</FullVersion>
    </PropertyGroup>
    <Message Text="FullVersion: $(FullVersion)" />
  </Target>


  <Target Name="UpdateVersionInFilesByRegex">
    <ItemGroup>
        <!-- could have simplified regex as Assembly(File)?Version to include both items, but this can update only one of them if another is not found and operation will still finish successfully which is bad -->
        <FilesToUpdate Include="$(MainProjectDir)\**\AssemblyInfo.cs">
            <Regex>(?&lt;=\[assembly:\s*Assembly?Version\(["'])(\d+\.){2,3}\d+(?=["']\)\])</Regex>
            <Replacement>$(FullVersion)</Replacement>
        </FilesToUpdate>
        <FilesToUpdate Include="$(MainProjectDir)\**\AssemblyInfo.cs">
            <Regex>(?&lt;=\[assembly:\s*AssemblyFileVersion\(["'])(\d+\.){2,3}\d+(?=["']\)\])</Regex>
            <Replacement>$(FullVersion)</Replacement>
        </FilesToUpdate>
        <FilesToUpdate Include="CommonProperties.wxi">
            <Regex>(?&lt;=&lt;\?define\s+ProductVersion\s*=\s*['"])(\d+\.){2,3}\d+(?=["']\s*\?&gt;)</Regex>
            <Replacement>$(FullVersion)</Replacement>
        </FilesToUpdate>
    </ItemGroup>

    <Exec Command="attrib -r %(FilesToUpdate.Identity)" />
    <Message Text="Updating version in %(FilesToUpdate.Identity)" />
    <RegexReplace Path="%(FilesToUpdate.Identity)" Regex="%(Regex)" Replacement="%(Replacement)"/>
  </Target>



  <Target Name="WriteReadmeFile">
    <Error Text="ERROR: env var TF_BUILD_BINARIESDIRECTORY is not set, see http://msdn.microsoft.com/en-us/library/hh850448.aspx" Condition="'$(TF_BUILD_BINARIESDIRECTORY)' == ''" />
    <WriteLinesToFile
        File="$(TF_BUILD_BINARIESDIRECTORY)\readme.txt"
        Lines="This is version $(FullVersion)"
        Overwrite="true"
        Encoding="Unicode"/>
  </Target>

  <Target Name="Build">
    <CallTarget Targets="GetMajorMinorNumbers" />
    <CallTarget Targets="GetChangesetNumber" />
    <CallTarget Targets="FormFullVersion" />
    <CallTarget Targets="UpdateVersionInFilesByRegex" />
    <CallTarget Targets="WriteReadmeFile" />
  </Target>

</Project>

Common.proj

<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003' ToolsVersion="12.0">
<!-- based on example from http://msdn.microsoft.com/en-us/library/dd722601.aspx -->
  <UsingTask TaskName="RegexReplace" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll">
    <ParameterGroup>
      <Path ParameterType="System.String" Required="true" />
      <Regex ParameterType="System.String" Required="true" />
      <Replacement ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.Core" />
      <Using Namespace="System" />
      <Using Namespace="System.IO" />
      <Using Namespace="System.Text.RegularExpressions" />
      <Code Type="Fragment" Language="cs"><![CDATA[
            string content = File.ReadAllText(Path);
            if (! System.Text.RegularExpressions.Regex.IsMatch(content, Regex)) {
                Log.LogError("ERROR: file does not match pattern");
            }
            content = System.Text.RegularExpressions.Regex.Replace(content, Regex, Replacement);
            File.WriteAllText(Path, content);
            return !Log.HasLoggedErrors;
]]></Code>
    </Task>
  </UsingTask>

  <Target Name='Demo' >
    <RegexReplace Path="C:\Project\Target.config" Regex="$MyRegex$" Replacement="MyValue"/>
  </Target>
</Project>

Version.xml

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <MajorVersion>1</MajorVersion>
    <MinorVersion>1</MinorVersion>
  </PropertyGroup>
</Project>
Ivan
  • 9,089
  • 4
  • 61
  • 74
2

Injecting the Changeset number is great, but it really doesn't do everything I need it to. For instance, if I know the changeset number of a build produced by my build system, do I really know what is in that executable? No I do not, as the build could have been a private build, or even a failed build.

We put our trust in the Build number (BuildID, really) instead, so that we can get as much data about that build as possible by querying the TFS system after the fact. This way we can determine if the build was a private build, was a build that had special command line parameters passed to it, and other pertinent details.

We use the following setup:

  1. Set the build format in the build definition to be something like: 1.0.0.$(BuildId)

  2. In the build process template, in the MSBuild task, inject the following to the MSBuildArguments item

    String.format("/p:BuildNumber={0}", BuildDetail.BuildNumber)
    ...ensure you leave what was already there.
  3. In your projects (or ideally, a common props file included in all your projects) defined a property called build number that defaults to something like 0.0.0.1.

    <PropertyGroup><BuildNumber Condition="'$(BuildNumber)'==''">0.0.0.1</BuildNumber></PropertyGroup>
    note that you can further break this down however you like using property functions. We use this to get the Major number for instance:
    <MajorVersionNumber>$(BuildNumber.Split('.')[0])</MajorVersionNumber>
    and yes, this does put a dependency on the build number format in our builds!
  4. In your project, you can make use of the build number property to inject into various files during build time. You can use custom build tasks (I use 'sed' and this macro to inject a version number into a .h file for instance - the same could be done with any text-based file type).

  5. If you have more complex versioning requirements you can make use of custom MSBuild targets that inject the build number into other file types. I have done exactly that with versioning for NuGet packages that our builds automatically create for our common-library CS projects for example.

To query a build by its build number then, you can do the following in PowerShell (with the Visual Studio Team Foundation Server Power Tools installed):

Add-PSSnapin Microsoft.TeamFoundation.PowerShell # you must install the VS TFS Power tools with the powershell option enabled to get this... a must have IMHO
$tfs = Get-TfsServer http://yourtfsserver:8080/tfs/YourProjectCollectionName
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation.Build.Client')
$buildserver = $tfs.GetService([Microsoft.TeamFoundation.Build.Client.IBuildServer])
$buildQuerySpec = $buildserver.CreateBuildDetailSpec("YourTFSProjectName","Your-Build-Definition-Name")
$buildQuerySpec.BuildNumber = '1.0.0.12345' # put your build number here.
$buildQuerySpec.QueryDeletedOption = 'IncludeDeleted'
$bld = $buildserver.QueryBuilds($buildQuerySpec)

With the '$bld' you can now query all the properties of that particular build. For instance, to see what changeset the build was based on, the status of the build, who instigated the build, and if there was a shelveset for the build:

$bld.Builds[0] | Ft -Property BuildFinished,RequestedFor,ShelvesetName,Status,SourceGetVersion -AutoSize

Edit: Correct typo in Powershell script

d3r3kk
  • 3,465
  • 3
  • 18
  • 22
  • d3r3kk, I've been thinking too about using BuildId instead of Changeset number. I agree with you why it could be better, especially in case of gated check-ins. The problem with builds is that they get deleted according to Retention Policy and so you lose the traceability link! – Ivan Sep 10 '14 at 17:14
  • 1
    The build artifacts do get deleted, but the TFS build metadata does not. Note the line in my Powershell script: $buildQuerySpec.QueryDeletedOption='IncludeDeleted' – d3r3kk Sep 10 '14 at 17:52
  • Ah! I got you! Thanks for the explanation. – Ivan Sep 11 '14 at 08:15
  • If you label the source code during the build then you can re-create any deleted build – ds19 Apr 17 '15 at 09:35
2

I downloaded this script (thanks to MrHinsh's answer), checked-in the script in source control and specified it in the build definition pre-build script path:

BuildDefPreBuild

Then I configured build number format as "$(BuildDefinitionName)_1.0.0$(Rev:.r)" (see MrHinsh's answer for details).

And it works to my surprise.

Community
  • 1
  • 1
Stanislav
  • 4,389
  • 2
  • 33
  • 35
0

I'm working on a project that has similar, but not identical requirements. Th major and minor versions are kept in AssemblyInfo as any standard .net project would have them. On our build server, we have a wrapper MsBuild script that invokes the .sln build, but it also does some setup tasks including generating additional build information. This build file is only executed on our build server. Developers building through Visual Studio will only build the .sln, and not get that additional behavior.

recursive
  • 83,943
  • 34
  • 151
  • 241
  • Yes, that's perfectly fine way of doing things. And I've done it myself in the past. In my current case I use TFS Builds to have Gated Check-ins which I'm really fond of, especially in case of lots of developers and TDD. – Ivan Sep 10 '14 at 15:13
0

We have a similar requirement and make use of the NANT ASMINFO TASK. During the TFS build we invoke this additional NANT target which creates a new AssemblyVersion.cs file.

<asminfo output="AssemblyInfo.cs" language="CSharp">
<imports>
    <import namespace="System" />
    <import namespace="System.Reflection" />
    <import namespace="System.EnterpriseServices" />
    <import namespace="System.Runtime.InteropServices" />
</imports>
<attributes>
    <attribute type="ComVisibleAttribute" value="false" />
    <attribute type="CLSCompliantAttribute" value="true" />
    <attribute type="AssemblyVersionAttribute" value="${version.number}" />
    <attribute type="AssemblyTitleAttribute" value="My fun assembly" />
    <attribute type="AssemblyDescriptionAttribute" value="More fun than a barrel of monkeys" />
    <attribute type="AssemblyCopyrightAttribute" value="Copyright (c) 2002, Monkeyboy, Inc." />
    <attribute type="ApplicationNameAttribute" value="FunAssembly" />
</attributes>
<references>
    <include name="System.EnterpriseServices.dll" />
</references>

Please make note of the property ${version.number}, which is actually set based on your requirement. Then we loop through the exisiting Assemblyversion.cs files and make them read only and then replace it with the new file which we created.

<attrib readonly="false" file="${project}\AssemblyVersion.cs"/>

As you might know, this target gets executed before compilation.

Isaiah4110
  • 9,855
  • 1
  • 40
  • 56
  • Isaiah4110, how do you call task in TFS build? – Ivan Sep 10 '14 at 15:18
  • In our case, we use NANT script for the actual compilation and not msbuild/tfs. TFS is just a medium for us to kick off the build. The build template internally turns over and calls the NANT script. Basically an invokeprocess activity which calls the nant.exe along with the arguments (.build file, target name along with other options). – Isaiah4110 Sep 10 '14 at 16:40
  • I see - a customized build process - this is what I'm trying to avoid )) – Ivan Sep 10 '14 at 17:10
  • 2) customize TFS build process template Problem - it's not that simple, and causes some merge work on TFS upgrades. Also changeset number doesn't exist yet in gated check-ins, so we can only use the previous changeset id. 1. Adding InvokeProcess is SIMPLE. 2. No merge process is required for TFS upgrades, we just upgraded from TFS2010 to TFS 2013. 3. Not sure about changeset number for gated checkins. – Isaiah4110 Sep 10 '14 at 18:18
  • This means you will keep using your old customized process, and not a new once from the new TFS version, which can contain many new features and bug fixes. And - as an example - editing and debugging MsBuild scripts is SIMPLER. I'm saying this because I've done both. Checkout 12 Agile principles and especially this one: "Simplicity--the art of maximizing the amount of work not done--is essential." – Ivan Sep 11 '14 at 08:31
  • Regardless of which version you use, I dont understand how InvokeProcess activity is going to change between 2010 and 2013, I dont think that activity will ever change. – Isaiah4110 Sep 11 '14 at 14:18
  • Activity won't change, but process template might – Ivan Sep 11 '14 at 14:26
0

I use [Major].[Minor].[BuildNumber].[revision]

I can then trace back to a build, which will give a changeset, which will give a work item etc.

You can use the community build tasks or I roll my own.

I do the same for MSI's and DacPac's

basically attrib the assemblyinfo file and then update the number using a regex, on a daily build leave the net version at the same value and just update the file version, so you can maintain compatability

the same method for the MSI's and the Dacapac's just different locations. in the MSI i have a Buildparams.wxi which has the following structure

<?xml version="1.0" encoding="utf-8"?>
<Include>
    <?define ProductVersion="1.2.3.4"?>  
</Include>

Productversion is then used as var.Productversion in the wix scripts. pre build i update the 1.2.3.4 with the build number i want to use

Just TFS
  • 4,697
  • 1
  • 18
  • 22
  • How is it implemented? What build tools do you use? – Ivan Sep 10 '14 at 15:28
  • purely TFS Custom Activities tasks written in C#. – Just TFS Sep 10 '14 at 15:42
  • Yes, done that too in the past. Next TFS upgrade you will need to merge your process template with the upgraded template or keep using your custom one. Also, process customization is tedious process, especially when you need to recompile C# custom activities, deploy them etc. Imagine you need to change something in the logics of versioning. It's much simpler to do in MsBuild or similar tools. – Ivan Sep 10 '14 at 15:57
0
 <Target Name="GetTFSVersion" >  
    <Exec Command="$(TF) history /server:[tfs]\DefaultCollection&quot;$/FP4WebServices/Staging&quot; /stopafter:1 /recursive /login:domain\user,password /noprompt | find /V &quot;Changeset&quot; | find /V > $(LatestChangeSetTxtFile).tmp"/>
    <Exec Command="FOR /F &quot;eol=; tokens=1 delims=, &quot; $(PERCENT_SIGN)$(PERCENT_SIGN)i in ($(LatestChangeSetTxtFile).tmp) do $(AT_SIGN)echo $(PERCENT_SIGN)$(PERCENT_SIGN)i > $(LatestChangeSetTxtFile)"/>
    <ReadLinesFromFile File="$(LatestChangeSetTxtFile)">
      <Output TaskParameter="lines" PropertyName="ChangeSet"/>
    </ReadLinesFromFile>
    <Message Text="TFS ChangeSet: $(ChangeSet)"/>        
  </Target>

  <Target Name="SetVersionInfo" DependsOnTargets="GetTFSVersion">
    <Attrib Files="@(AssemblyInfoFiles)" Normal="true"/>
     <FileUpdate Files="@(AssemblyInfoFiles)" Regex="AssemblyFileVersion\(&quot;.*&quot;\)\]" ReplacementText="AssemblyFileVersion(&quot;$(Major).$(Minor).$(Build).$(ChangeSet)&quot;)]" />
   </Target>
  • 1
    The problem with this approach is that you get the latest changeset number, and not necessarily the one you are building. Also you have to securely deal with login\password somehow.. – Ivan Sep 11 '14 at 10:55