Monday, February 23, 2009

Continous Installation : knowing the version

With the setup I have made to install the software at our clients server, there was a small problem : how to know which version was installed where? Now you can keep logs in excel/word or whatever, but my experience is that these get outdated very easily. So I wanted to know which version was installed, and the ability to get an overview of this information of ALL software on ALL servers. Turns out that this is not so hard to do : just create a custom labeler, and use the dashboard to view all the servers.
Normally a 'build' label is generated when you check something out of source control, a continuous increasing number, a date based number, ... but always something related to a source control. Luckily CCNet already made abstraction of this, making it easy to create labels with custom logic. Since this logic is very dependant on our setup, I wrote it as a plugin, adding it to the core off CCNet is not a good choice for this.
Writing a labeler plugin is not more difficult than writing a publisher or so, it means implementing ThoughtWorks.CruiseControl.Core.ILabeller

A bit of of background on our deployment setup :
° whenever a program passes QA, it gets zipped
format is version_number_program name
° it is placed in a folder hierarchy so programs belonging to the same 'family' stand together (all finance programs are together)
° we always install the latest version, (should the need arise that we need to install a previous version, we just delete the newer zip files from the folder)

So suppose I do an install of bookkeeping, the labeler must just get the zip file with the highest number from the finance/bookkeeping folder. One caveat : 1_2_0_10 is higher than 1_2_0_3 so plain sorting on the file names is a no-go. Anyway, here's the code of the labeler :

using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core;

[ReflectorType("releaseLabeller")]
public class ReleaseLabeller : ILabeller
{

private string _ProgramtoInstall;

[ReflectorProperty("programToInstall")]
public string ProgramToInstall {
get { return this._ProgramtoInstall; }
set { this._ProgramtoInstall = value; }
}

public string Generate(IIntegrationResult integrationResult)
{
return GetLatestVersionOfDeployedProgram("\Install", ProgramToInstall, integrationResult);
}

public void Run(ThoughtWorks.CruiseControl.Core.IIntegrationResult result)
{
result.Label = Generate(result);
}

private string GetLatestVersionOfDeployedProgram(string installFolder, string program, IIntegrationResult integrationResult)
{

string[] DeployedFiles = null;
string FolderToScan = System.IO.Path.Combine(installFolder, program);
string Dummy = null;
System.IO.FileInfo fi = default(System.IO.FileInfo);
string[] VersionArray = null;
string Version = null;

DeployedFiles = System.IO.Directory.GetFiles(FolderToScan, "*.zip");

if (DeployedFiles.Length == 0) {
return integrationResult.LastSuccessfulIntegrationLabel;
}

Array.Sort(DeployedFiles, new ReleaseZipFileSorter());

fi = new System.IO.FileInfo(DeployedFiles(DeployedFiles.Length - 1));
Dummy = fi.Name;

VersionArray = Dummy.Split('_');

if (VersionArray.Length < 4) {
throw new Exception(string.Format("Unsupported zip file {0} found in {1} for determining latest version", Dummy, FolderToScan));
}

Version = string.Format("{0}.{1}.{2}.{3}", VersionArray(0), VersionArray(1), VersionArray(2), VersionArray(3));

return Version;

}
}


The programToInstall contains which program is being installed (duh)
so in the ccnet.config I fill this with finance/bookkeeping which is the same argument as I pass to the install program.

Here's the code for the sorting of the zip files :
Basically, I format the numeric parts to length 5 and use the string sorting on it.
So 1_0_3_0 becomes 00001_00000_00003_00000 for the sort procedure.


public class ReleaseZipFileSorter : IComparer, IComparer<string>
{

public int Compare(object x, object y)
{
return FormatCCNetLabel(x.ToString).CompareTo(FormatCCNetLabel(y.ToString));
}

public int Compare1(string x, string y)
{
return FormatCCNetLabel(x).CompareTo(FormatCCNetLabel(y));
}
int System.Collections.Generic.IComparer<string>.Compare(string x, string y)
{
return Compare1(x, y);
}

private string FormatCCNetLabel(string label)
{
Text.StringBuilder Result = new Text.StringBuilder();
string Dummy = label.Substring(label.LastIndexOf("\\") + 1);

string[] parts = Dummy.Split('_');

if (parts.Length != 5) {
throw new Exception("Invalid zip file name : " + label);
}

Result.Append(Convert.ToInt32(parts(0)).ToString("00000"));
Result.Append(".");

Result.Append(Convert.ToInt32(parts(1)).ToString("00000"));
Result.Append(".");

Result.Append(Convert.ToInt32(parts(2)).ToString("00000"));
Result.Append(".");

Result.Append(Convert.ToInt32(parts(3)).ToString("00000"));

return Result.ToString;

}
}

No comments:

Post a Comment