One aim of continuous integration should be to automate as much of the build process as possible. If something is left to human intervention then inevitably it will at some point be completed incorrectly or inconsistently or forgotten completely.
Part of a good build process is to update the version number of the application so that each build (triggered by changes in the source) will have a unique number and the source control system should label the source so that the code that created that build can be identified. This is my solution to updating the version number when using NAnt, Draco.Net and SourceGear Vault.
The build process consists of the following:
Draco.Net monitors the project folder in the SourceGear Vault repository.
A developer checks in some changes which causes Vault to update the folder version number (based on the changeset).
Draco.Net detects the changes and gets the latest version of the project to a temporary build folder
The NAnt .build file (part of the project) is started by Draco.Net
Within NAnt a custom function gets the folder version from Vault and sets the revision part of the version number based on it
The project folder in Vault is labeled with the build revision number
The assembly is compiled using the version number by overriding the AssemblyInfo.cs file (using the standard <adminfo> task)
The NUnit tests are run and reports produced, MSI files created etc.
Everything is pretty straightforward using the standard functions of NAnt, Draco.Net and the SourceGear Vault NAnt Tasks other than step #5 which required a custom NAnt function to return the folder tree version number. The code for this is below which I added to the Vault NAnt Tasks project (I'm not sure yet of the process for feeding this into the project as an update):
using NAnt.Core;
using NAnt.Core.Attributes;
using VaultClientNetLib;
using VaultClientOperationsLib;
using VaultLib;
namespace NAnt.Contrib.Tasks.SourceGearVault
{
/// <summary>
/// Vault functions for NAnt
/// </summary>
/// <example>
/// <code>
/// <![CDATA[<property name="build.version.revision" value="${vault::getversion(vault.url, vault.repository, vault.path, vault.username, vault.password)}"/>
/// ]]></code>
/// </example>
[FunctionSet("vault", "Vault")]
public class VaultFunctions : NAnt.Core.FunctionSetBase
{
public VaultFunctions(Project project, PropertyDictionary properties) : base(project, properties) { }
#region getversion
/// <summary>
/// Gets the version of the object at the path specified.
/// </summary>
/// <param name="url">SourceGear Vault url.</param>
/// <param name="repository">Name of repository.</param>
/// <param name="path">Path to object.</param>
/// <param name="username">Username.</param>
/// <param name="password">Password.</param>
/// <returns></returns>
[Function("getversion")]
public static string GetVersion(string url, string repository, string path, string username, string password)
{
string version = string.Empty;
ClientInstance clientInstance = VaultFunctions.Login(url, VaultConnection.AccessLevelType.Client, username, password);
VaultRepositoryInfo currentRepository = new VaultRepositoryInfo();
try
{
if (VaultFunctions.SelectRepository( clientInstance, repository, username ))
{
string normalizedPath = RepositoryPath.NormalizeFolder( path );
VaultClientTreeObject vaultClientTreeObject = clientInstance.TreeCache.Repository.Root.FindTreeObjectRecursive( path );
version = vaultClientTreeObject.Version.ToString();
}
}
finally
{
clientInstance.Logout();
}
return version;
}
#endregion
#region Support functions.
/// <summary>
/// Login to Vault.
/// </summary>
/// <param name="url">URL.</param>
/// <param name="accessLevel">Access level.</param>
/// <param name="username">Username.</param>
/// <param name="password">Password.</param>
/// <returns></returns>
public static ClientInstance Login(string url, VaultConnection.AccessLevelType accessLevel, string username, string password)
{
ClientInstance clientInstance = new ClientInstance();
if( url.Length != 0 && username.Length != 0 )
{
clientInstance.Init( accessLevel );
if (url.ToLower().EndsWith("/VaultService") == false)
url += "/VaultService";
if (url.ToLower().StartsWith("http://") == false && url.ToLower().StartsWith("https://") == false)
url = "http://" + url;
clientInstance.Login( url, username, password );
if( clientInstance.ConnectionStateType != ConnectionStateType.Connected )
{
throw new BuildException( "Login to Vault repository failed." );
}
}
return clientInstance;
}
/// <summary>
/// Selects the repository.
/// </summary>
/// <param name="clientInstance">Client instance.</param>
/// <param name="repositoryName">Name of the repository.</param>
/// <param name="username">Username.</param>
/// <returns></returns>
public static bool SelectRepository( ClientInstance clientInstance, string repositoryName, string username )
{
if( clientInstance.ConnectionStateType != ConnectionStateType.Connected )
{
return false;
}
VaultRepositoryInfo[] vaultRepositories = null;
clientInstance.ListRepositories( ref vaultRepositories );
foreach( VaultRepositoryInfo vaultRepositoryInfo in vaultRepositories )
{
if( vaultRepositoryInfo.RepName.ToLower() == repositoryName.ToLower() )
{
VaultRepositoryInfo currentRepository = vaultRepositoryInfo;
clientInstance.SetActiveRepositoryID( currentRepository.RepID, username, currentRepository.UniqueRepID, true, true );
return true;
}
}
return false;
}
#endregion
}
}
This is called from the NAnt build script
<?xml version="1.0"?>
<project name="NUnitAspInjector" default="build" basedir=".">
<!-- set by Draco.Net for automated builds -->
<property name="draco" value="false"/>
<property name="build.id" value="0"/>
<!-- project properties -->
<property name="project.name" value="NUnitAspInjector"/>
<!-- base project version for this trunk / branch -->
<property name="build.version.major" value="1"/>
<property name="build.version.minor" value="0"/>
<property name="build.version.build" value="5000"/>
<property name="build.version.revision" value="0"/>
<!-- if automated build then set revision number based on version in vault -->
<if test="${draco}">
<!-- vault parameters (set in Draco.Net project properties) -->
<!-- <property name="vault.url" value="http://localhost"/> <property name="vault.repository" value="default"/> <property name="vault.path" value="$/NUnitAspInjector/trunk"/> <property name="vault.username" value="build"/> <property name="vault.password" value="password"/> -->
<!-- set revision number based on tree version from vault -->
<property name="build.version.revision" value="${vault::getversion(vault.url, vault.repository, vault.path, vault.username, vault.password)}"/>
<!-- label the build -->
<!-- in case a build is forced, this avoids a duplicate label error -->
<vaultdeletelabel url="${vault.url}" username="${vault.username}" password="${vault.password}" repository="${vault.repository}" path="${vault.path}" labelstring="build ${build.version.revision}"/>
<!-- apply the label to the source folder -->
<vaultlabel url="${vault.url}" username="${vault.username}" password="${vault.password}" repository="${vault.repository}" path="${vault.path}" labelstring="build ${build.version.revision}"/>
</if>
<!-- set the full build version based on major.minor.build.revision -->
<property name="build.version" value="${build.version.major}.${build.version.minor}.${build.version.build}.${build.version.revision}"/>
<!-- output directory -->
<property name="build.outdir" value="C:\working\buildoutput\${project.name}\${build.version}"/>
<!-- target .NET framework and corresponding compiler directives -->
<property name="nant.settings.currentframework" value="net-1.1"/>
<property name="csc.define" value="NET11"/>
<target name="build">
<echo message="building: ${project.name} ${build.version}"/>
<echo message="build id: ${build.id}" if="${draco}"/>
<!-- build the solution in debugh mode so that the test are created -->
<!-- this is a shortcut until a proper test project is created -->
<solution solutionfile="src\NUnitAspInjector.sln" configuration="debug">
<webmap>
<map url="http://localhost/NUnitAspInjector.Sample/" path="src\NUnitAspInjector.Sample\"/>
</webmap>
</solution>
<!-- make a folder for the assembly -->
<mkdir dir="bin"/>
<!-- if automated build then create assembly version info -->
<if test="${draco}">
<attrib file="src\NUnitAspInjector\AssemblyInfo.cs" readonly="false"/>
<asminfo output="src\NUnitAspInjector\AssemblyInfo.cs" language="CSharp">
<imports>
<import name="System" />
<import name="System.Reflection" />
<import name="System.EnterpriseServices" />
<import name="System.Runtime.InteropServices" />
</imports>
<attributes>
<attribute type="ApplicationNameAttribute" value="${project.name}" />
<attribute type="AssemblyProductAttribute" value="${project.name}" />
<attribute type="AssemblyTitleAttribute" value="InteSoft ${project.name} ${build.version.major}.${build.version.minor} for .NET 1.1" />
<attribute type="AssemblyDescriptionAttribute" value="${project.name} for ASP.NET" />
<attribute type="AssemblyCompanyAttribute" value="InteSoft IT Ltd." />
<attribute type="AssemblyCopyrightAttribute" value="Copyright © 2005 InteSoft IT Ltd." />
<!-- the version stays at x.x.x.0 for binding -->
<attribute type="AssemblyVersionAttribute" value="${build.version.major}.${build.version.minor}.${build.version.build}.0" />
<!-- the information versions show the revision number -->
<attribute type="AssemblyFileVersionAttribute" value="${build.version}" />
<attribute type="AssemblyInformationalVersionAttribute" value="${build.version}" />
<attribute type="AssemblyDelaySignAttribute" value="false" />
<attribute type="AssemblyKeyFileAttribute" value="" />
<attribute type="AssemblyKeyNameAttribute" value="" />
<attribute type="CLSCompliantAttribute" value="true" />
<attribute type="ComVisibleAttribute" value="false" />
</attributes>
<references>
<include name="System.EnterpriseServices.dll" />
</references>
</asminfo>
</if>
<!-- now build the proper release version of our component -->
<csc target="library" output="bin\NUnitAspInjector.dll" debug="false" define="${csc.define};" optimize="true" rebuild="true">
<sources basedir="src\NUnitAspInjector">
<include name="AssemblyInfo.cs"/>
<include name="Gdi32.cs"/>
<include name="Handler.cs"/>
<include name="Html2Image.cs"/>
<include name="InjectorModule.cs"/>
<include name="InjectorWebFormTestCase.cs"/>
<include name="RenderAssertion.cs"/>
<include name="TestHostAttribute.cs"/>
<include name="TestInjectorAttribute.cs"/>
<include name="TestRenderAttribute.cs"/>
<include name="TestServer.cs"/>
<include name="cassini\ByteParser.cs"/>
<include name="cassini\ByteString.cs"/>
<include name="cassini\Connection.cs"/>
<include name="cassini\Host.cs"/>
<include name="cassini\Messages.cs"/>
<include name="cassini\Request.cs"/>
<include name="cassini\Server.cs"/>
</sources>
<resources prefix="NUnit.Extensions.Asp" basedir="src\NUnitAspInjector">
<include name="Html2Image.resx"/>
</resources>
<references>
<include asis="true" name="System.Web.dll"/>
<include asis="true" name="System.Web.Services.dll"/>
<include asis="true" name="System.Runtime.Remoting.dll"/>
<include asis="true" name="System.XML.dll"/>
<include asis="true" name="System.Windows.Forms.dll"/>
<include asis="true" name="System.Drawing.dll"/>
<include asis="true" name="lib\AForge.Imaging.dll"/>
<include asis="true" name="lib\AForge.Math.dll"/>
<include asis="true" name="lib\nmock.dll"/>
<include asis="true" name="lib\nunit.framework.dll"/>
<include asis="true" name="lib\NUnitAsp.dll"/>
<include asis="true" name="lib\Interop.SHDocVw.dll"/>
<include asis="true" name="lib\AxInterop.SHDocVw.dll"/>
<include asis="true" name="lib\Microsoft.mshtml.dll"/>
</references>
</csc>
<!-- copy this release version to the test app -->
<copy file="bin\NUnitAspInjector.dll" todir="src\NUnitAspInjector.Sample\bin" overwrite="true"/>
<!-- run the NUnit tests -->
<nunit2 failonerror="true">
<formatter type="Xml" usefile="true" extension=".xml" outputdir="${build.outdir}"/>
<test assemblyname="src\NUnitAspInjector.Sample\bin\NUnitAspInjector.Sample.dll" appconfig="src\NUnitAspInjector.Sample\web.config">
<categories>
<exclude name="IIS"/>
</categories>
</test>
</nunit2>
<!-- create NUnit report -->
<nunit2report out="NUnitReport.htm" todir="${build.outdir}" opendesc="true">
<fileset>
<include name="${build.outdir}\NUnitAspInjector.Sample.dll-results.xml"/>
</fileset>
</nunit2report>
<!-- copy this release version to the output folder -->
<copy file="bin\NUnitAspInjector.dll" todir="${build.outdir}" overwrite="true"/>
</target>
</project>
The key line is this:
<property name="build.version.revision" value="${vault::getversion(vault.url, vault.repository, vault.path, vault.username, vault.password)}"/>
The vault url, repository, path, username and password are passed into the NAnt build script from the Draco.Net project properties and the version and labeling parts of the script only run when the script is invoked on the build server (so developers can run the script locally without creating rogue labels).
Incidentally, I did start by using a revision number based on the difference between the project start date and the current date/time using a variation of another script:
<!-- set the revision number based on a build startdate -->
<property name="build.startdate" value="01 Oct 2005"/>
<script language="C#">
<imports>
<import namespace="System.Globalization"/>
<import namespace="System.Threading"/>
</imports>
<code>
<![CDATA[
public static void ScriptMain(Project project)
{
DateTime start = Convert.ToDateTime(project.Properties["build.startdate"]);
Calendar calendar = Thread.CurrentThread.CurrentCulture.Calendar;
int months = ((calendar.GetYear(DateTime.Today) - calendar.GetYear(start)) * 12) + calendar.GetMonth(DateTime.Today) - calendar.GetMonth(start);
int day = DateTime.Now.Day;
int revision = (months * 100) + day;
project.Properties["build.version.revision"] = revision.ToString();
}
]]>
</code>
</script>
However, I like the fact that the build number only changes when the source changes (due to checkins) rather than incrementing simply due to time. Also, with Vault, the version number of the folder ends up matching the build label applied to it which just seems neat. I like neat :)
That's it. Hope someone finds this of some use!