I have two projects set up in CruiseControl.NET: CI build and nightly build.
Both of them execute the same NAnt script, but with different parameters.
The CruiseControl.NET label (currently generated by the DefaultLabeler) is embedded into AssemblyInfo as the build part of the version (for example, MajorVersion.MinorVersion.CCNET_Label.SVN_Revision).
For more consistent versioning I would like both projects to share the same CruiseControl.NET label value.
I have investigated the labelers that are available as part of the CruiseControl.NET installation, but I could not find one that does what I want.
How do I share a label value between multiple CruiseControl.NET builds?
If there is a better way to do this, I would like to know.
I found a way. See my answer below.
I could not find an existing solution that to do what I needed, so I ended up writing a custom CruiseControl.NET labeller.
Here's how it is done:
- Create a new project. This will be used as a plugin library by CC.NET
- The name of the output DLL needs to match *ccnet.\*.CruiseControl.plugin*. Go to project
properties and change "Assembly name" to *ccnet.<insert name here>.CruiseControl.plugin*
- In your project, add references to the three assemblies found in the CC.NET server installation directory (default is: C:\Program Files\CruiseControl.NET\server):
- NetReflector.dll
- ThoughtWorks.CruiseControl.Core.dll
- ThoughtWorks.CruiseControl.Remote.dll
- Create a new public class such as this:
using ThoughtWorks.CruiseControl.Core;
using ThoughtWorks.CruiseControl.Remote;
// this is the labeller name that will be used in ccnet.config
[ReflectorType("customLabeller")]
public class CustomLabeller : ILabeller
{
[ReflectorProperty("syncronisationFilePath", Required = true)]
public string SyncronisationFilePath { get; set; }
#region ILabeller Members
public string Generate(IIntegrationResult previousResult)
{
if (ShouldIncrementLabel(previousResult))
return IncrementLabel();
if (previousResult.Status == IntegrationStatus.Unknown)
return "0";
return previousResult.Label;
}
public void Run(IIntegrationResult result)
{
result.Label = Generate(result);
}
#endregion
private string IncrementLabel()
{
if(!File.Exists(SyncronisationFilePath))
return "0";
using (FileStream fileStream = File.Open(SyncronisationFilePath,
FileMode.OpenOrCreate,
FileAccess.ReadWrite,
FileShare.None))
{
// read last build number from file
var bytes = new byte[fileStream.Length];
fileStream.Read(bytes, 0, bytes.Length);
string rawBuildNumber = Encoding.ASCII.GetString(bytes);
// parse last build number
int previousBuildNumber = int.Parse(rawBuildNumber);
int newBuildNumber = previousBuildNumber + 1;
// increment build number and write back to file
bytes = Encoding.ASCII.GetBytes(newBuildNumber.ToString());
fileStream.Seek(0, SeekOrigin.Begin);
fileStream.Write(bytes, 0, bytes.Length);
return newBuildNumber.ToString();
}
}
private static bool ShouldIncrementLabel(IIntegrationResult previousResult)
{
return (previousResult.Status == IntegrationStatus.Success ||
previousResult.Status == IntegrationStatus.Unknown)
}
}
- Compile your project and copy the DLL to CC.NET server installation directory (default is: C:\Program Files\CruiseControl.NET\server)
- Restart CC.NET Windows service
- Create a text file to store the current build number
- Add the new labeler to you project definition in ccnet.config file:
<labeller type="sharedLabeller">
<syncronisationFilePath>C:\Program Files\CruiseControl.NET\server\shared\buildnumber.txt</syncronisationFilePath>
<incrementOnFailure>false</incrementOnFailure>
</labeller>
I ran into the same issue, but I found that using the <stateFileLabeller>
in conjunction with the <assemblyVersionLabeller>
proved to be a much simpler solution.
The only gotcha about using the stateFileLabeller is that you can't specify a directory for your state files in a project, because CruiseControl.NET won't find it. I left it in the default directory, and it works great.
I've modified the class Arnold made making it more of a replica of the defaultlabeller:
using System.IO;
using System.Text;
using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core;
using ThoughtWorks.CruiseControl.Remote;
// This namespace could be altered and several classes could be put into the same if you'd want to combine several plugins in one dll
namespace ccnet.SharedLabeller.CruiseControl.plugin
{
[ReflectorType("sharedLabeller")]
public class SharedLabeller : ILabeller
{
/// <summary>
/// The path where the file that holds the shared label should be located
/// </summary>
/// <default>none</default>
[ReflectorProperty("sharedLabelFilePath", Required = true)]
public string SharedLabelFilePath { get; set; }
/// <summary>
/// Any string to be put in front of all labels.
/// </summary>
[ReflectorProperty("prefix", Required = false)]
public string Prefix { get; set; }
/// <summary>
/// If true, the label will be incremented even if the build fails. Otherwise it will only be incremented if the build succeeds.
/// </summary>
[ReflectorProperty("incrementOnFailure", Required = false)]
public bool IncrementOnFailure { get; set; }
/// <summary>
/// If false, the label will never be incremented when this project is builded. This is usefull for deployment builds that
/// should use the last successfull of two or more builds
/// </summary>
[ReflectorProperty("increment", Required = false)]
public bool Increment { get; set; }
/// <summary>
/// Allows you to set the initial build number.
/// This will only be used when on the first build of a project, meaning that when you change this value,
/// you'll have to stop the CCNet service and delete the state file.
/// </summary>
/// <default>0</default>
[ReflectorProperty("initialBuildLabel", Required = false)]
public int InitialBuildLabel { get; set; }
public SharedLabeller()
{
IncrementOnFailure = false;
Increment = true;
InitialBuildLabel = 0;
}
#region ILabeller Members
public string Generate(IIntegrationResult integrationResult)
{
if (ShouldIncrementLabel(integrationResult.LastIntegration))
{
return Prefix + this.GetLabel();
}
else
{
return integrationResult.LastIntegration.Label;
}
}
public void Run(IIntegrationResult integrationResult)
{
integrationResult.Label = Generate(integrationResult);
}
#endregion
/// <summary>
/// Get and increments the label, unless increment is false then it only gets the label
/// </summary>
/// <returns></returns>
private string GetLabel()
{
ThoughtWorks.CruiseControl.Core.Util.Log.Debug("About to read label file. Filename: {0}", SharedLabelFilePath);
using (FileStream fileStream = File.Open(this.SharedLabelFilePath,
FileMode.OpenOrCreate,
FileAccess.ReadWrite,
FileShare.None))
{
// Read last build number from file
var bytes = new byte[fileStream.Length];
fileStream.Read(bytes, 0, bytes.Length);
string rawBuildNumber = Encoding.UTF8.GetString(bytes);
// Parse last build number
int previousBuildNumber;
if (!int.TryParse(rawBuildNumber, out previousBuildNumber))
{
previousBuildNumber = InitialBuildLabel - 1;
}
if (!Increment)
{
return previousBuildNumber.ToString();
}
int newBuildNumber = previousBuildNumber + 1;
// Increment build number and write back to file
bytes = Encoding.UTF8.GetBytes(newBuildNumber.ToString());
fileStream.Seek(0, SeekOrigin.Begin);
fileStream.Write(bytes, 0, bytes.Length);
return newBuildNumber.ToString();
}
}
private bool ShouldIncrementLabel(IntegrationSummary integrationSummary)
{
return integrationSummary == null || integrationSummary.Status == IntegrationStatus.Success || IncrementOnFailure;
}
}
}
The benefit should be that you now can specify prefix as well as "incrementonfailure". Also I've added a "increment" property that can be used for deployment builds that should not increment the build number at all. If you want to modify it yourself I would advise to have a look at their implementations:
CruiseControl.NET repository folder containing labellers