Thursday, April 29, 2010

Automatic build from tags (not trunk)


Requirements


My requirement for Continuous Integration is slightly different. Our project involves semi-manual Sql scripts preparation for a build, so I can not relaibly build every version from svn. Only the code in tags/Builds is good. But I am so lazy, I don't want even run my build script. I want to new build to be automatically detected and built. Just email me please when you are done :)

The first attempt to automate daily builds was to create my own "small" asp.net project to do it. But as soon s I realized that I need a winservice to perform long lasting tasks, such as source code checkout, I abandoned this idea. It is too much effort and I should be able to find something ready.


Almost continuous integration


So I decided to give a whirl to Cruise Control.NET. I heard of it before, but it does what I do not need: it tracks *any* change to the *trunk*, whereas I need to track new folder in "tags/Builds" and trigger svn checkout of this particular subfolder, not just "svn update" of "trunk" folder.

As I suspected, CCNet Svn plugin can crate new labels in tags but it can not track changes in tags folder.
I tried Hudson build manager too. More plugins, way much better UI but the same problem: it tracks trunk only.

Such a minor problems do not stop me and I dived inside Cruise Control.NET.
At first I tried to do what I want by introducing some faked task, which would check last build tag and compare it to the last one available in svn. But CCNet asks "source" plugin for changes and if it detects nothing, no "task" will be invoked.
So I started digging CCNet's "Svn" class. Well, it can be done, but a lot of work, and to make it flexible and not reflecting my particular setup even more work.

All the sudden I took another look at seemingly unrelated plugin "external source". It says that you use it to integrate with other source control systems, but you can do more with it. You can call your own script which will do custom svn search logic.

Bummer, "external source" command line is specified as "executable GETMODS "fromtimestamp" "totimestamp" args". So if I want to execute "ruby.exe /path/to/ruby/script.rb", I can't: script parameter is the last in the list of params. The same with "cmd.exe", I can't pass params in the order I want.

But this should be easy fixable: downloaded sources (make sure you get the same version of sources as you have installed as binary package), add one more parameter "argsLeading" and it works!

Configuration



<project name="Your Project">
<workingDirectory>C:\tmp\ccnet-working\YourProject</workingDirectory>
<artifactDirectory>C:\tmp\ccnet-working\YourProject.Artifacts</artifactDirectory>
<triggers>
<intervalTrigger name="interval" seconds="3600" initialSeconds="10"/>
</triggers>

<sourcecontrol type="external" autoGetSource="true">
<executable>ruby.exe</executable>
<argsLeading>C:\Projects\Your\Project\TagCheck.rb</argsLeading>
<args></args>
</sourcecontrol>

<tasks>
<!--<nullTask />-->
<msbuild projectFile="src/YourProject.sln">
<executable>C:\WINDOWS\Microsoft.NET\Framework\v3.5\MSBuild.exe</executable>
<logger>C:\Program Files (x86)\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MSBuild.dll</logger>
</msbuild>
</tasks>
<publishers>
<email mailhost="mail" from="build-no-reply@enviance.com">
<users>
<user name="BuildGuru" group="buildmaster" address="you@your.company.com" />
<user name="JoeDeveloper" group="developers" address="you@your.company.com" />
</users>
<groups>
<group name="developers">
<notifications>
<notificationType>Failed</notificationType>
<notificationType>Fixed</notificationType>
</notifications>
</group>
<group name="buildmaster">
<notifications>
<notificationType>Always</notificationType>
</notifications>
</group>
</groups>
</email>
<xmllogger/>
</publishers>
</project>



Handling script

$svn_tags='https://svn.yourcompany.com/svn/your/project/tags/Builds'

def last_tag
`svn.exe ls #{$svn_tags}`.split().last().chomp('/')
end

def last_build
Dir.entries('.').select {|d| d =~ /^\d{8,}/}.sort().last() || '0'
end

def svn_info(tag)
info = {}
`svn.exe info #{$svn_tags}/#{tag}`.
split("\n").each {|line|
pair=line.split(/ *: */)
info[pair[0]]=pair[1]
}
info
end

#
# Get Modifications
#
if ARGV[0] == 'GETMODS'
if not last_tag > last_build
puts ''
exit 0
end

info = svn_info(last_tag)
date=DateTime.parse(info['Last Changed Date']).strftime()

# in fact, we should filter only modifications which are in between
# the ones in command line, but seems CCNet does check the result,
# so let's always return the latest entry
puts "

#{info['Revision']}
New build
#{last_tag}
#{date}
#{info['Last Changed Author']}

"
exit 0
#
# Get Source
#
elsif ARGV[0] == 'GETSOURCE'
workdir = ARGV[1]
timestamp = DateTime.parse(ARGV[2])
last = last_tag
info = svn_info(last)
date = date=DateTime.parse(info['Last Changed Date'])
if date > timestamp
STDERR.puts "Command line timestamp must be less then svn. Svn: '#{date}' command line: '#{timestamp}'"
exit 1
end
puts `svn.exe export #{$svn_tags}/#{last} #{workdir} --force`
exit 0
end


exit 1


Patch



Index: project/core/sourcecontrol/ExternalSourceControl.cs
===================================================================
--- project/core/sourcecontrol/ExternalSourceControl.cs (revision 7225)
+++ project/core/sourcecontrol/ExternalSourceControl.cs (working copy)
@@ -183,6 +183,14 @@
[ReflectorProperty("args", Required = false)]
public string ArgString = string.Empty;

+ ///
+ /// The same as "arg" but it will be the first parameter in command line.
+ /// Is useful if external program is a script engine and you need 1st parameter
+ /// to be a script name.
+ ///

+ [ReflectorProperty("argsLeading", Required = false)]
+ public string ArgLeadingString = string.Empty;
+
///
/// Should we automatically obtain updated source from the source control system or not?
///

@@ -237,7 +245,8 @@
///
public override Modification[] GetModifications(IIntegrationResult from, IIntegrationResult to)
{
- string args = string.Format(@"GETMODS ""{0}"" ""{1}"" {2}",
+ string args = string.Format(@"{0} GETMODS ""{1}"" ""{2}"" {3}",
+ ArgLeadingString,
FormatCommandDate(to.StartTime),
FormatCommandDate(from.StartTime),
ArgString);
@@ -265,7 +274,8 @@

if (AutoGetSource)
{
- string args = string.Format(@"GETSOURCE ""{0}"" ""{1}"" {2}",
+ string args = string.Format(@"{0} GETSOURCE ""{1}"" ""{2}"" {3}",
+ ArgLeadingString,
result.WorkingDirectory,
FormatCommandDate(result.StartTime),
ArgString);
@@ -286,7 +296,8 @@
{
if (LabelOnSuccess && result.Succeeded && (result.Label != string.Empty))
{
- string args = string.Format(@"SETLABEL ""{0}"" ""{1}"" {2}",
+ string args = string.Format(@"{0} SETLABEL ""{1}"" ""{2}"" {3}",
+ ArgLeadingString,
result.Label,
FormatCommandDate(result.StartTime),
ArgString);