Just wondering what\'s the best approach to versioning of .NET builds?
I use:
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:
Set the build format in the build definition to be something like: 1.0.0.$(BuildId)
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.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!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).
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
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:
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>
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.
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:
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.
<Target Name="GetTFSVersion" >
<Exec Command="$(TF) history /server:[tfs]\DefaultCollection"$/FP4WebServices/Staging" /stopafter:1 /recursive /login:domain\user,password /noprompt | find /V "Changeset" | find /V > $(LatestChangeSetTxtFile).tmp"/>
<Exec Command="FOR /F "eol=; tokens=1 delims=, " $(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\(".*"\)\]" ReplacementText="AssemblyFileVersion("$(Major).$(Minor).$(Build).$(ChangeSet)")]" />
</Target>
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