Versioning .NET builds
.net, msbuild, tfs, versioning, wix
Solution
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
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>(?<=\[assembly:\s*Assembly?Version\(["'])(\d+\.){2,3}\d+(?=["']\)\])</Regex>
<Replacement>$(FullVersion)</Replacement>
</FilesToUpdate>
<FilesToUpdate Include="$(MainProjectDir)\**\AssemblyInfo.cs">
<Regex>(?<=\[assembly:\s*AssemblyFileVersion\(["'])(\d+\.){2,3}\d+(?=["']\)\])</Regex>
<Replacement>$(FullVersion)</Replacement>
</FilesToUpdate>
<FilesToUpdate Include="CommonProperties.wxi">
<Regex>(?<=<\?define\s+ProductVersion\s*=\s*['"])(\d+\.){2,3}\d+(?=["']\s*\?>)</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>
Problem
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!