Monday, November 17, 2008

Hudson and Grails

I debated on whether or not to blog about my very positive experience with the Hudson CI server. I think the buzz pretty much speaks for itself, so I'll stick to how I implemented build number tagging with my Grails build.

My goal for my Hudson builds was to 'tag' the Hudson build number directly into the footer of my Grails GSP layouts. Since the applications that I'm applying this strategy to are internal facing, it suits us really well to be able to see the build number directly in the user interface.

Here's how I went about accomplishing it:

1) I added a filterset token in my layout ($PROJECT_HOME/grails-app/views/layouts/main.gsp):

<html>
<body>
...
<div class="footer">Build Version: @HUDSON_BUILD@</div>
</body>
</html>

2) I hooked into Grails' WAR target by defining a closure in $PROJECT_HOME/scripts/Events.groovy
eventWarStart = {
def hudsonBuild = Ant.antProject.properties."env.BUILD_NUMBER" ?: "Custom Build"
String relLayoutPath = "grails-app/views/layouts"
Ant.copy(todir: "$stagingDir/WEB-INF/$relLayoutPath", overwrite: true) {
fileset(dir: "${basedir}/$relLayoutPath", includes: "*.gsp")
filterset() {
filter(token: "HUDSON_BUILD", value: hudsonBuild)
}
}
}

The reason this works is because Hudson exposes a number of environment variables at build time. As you can see from the script above, I use the 'BUILD_NUMBER' variable as the token's replacement value.

Kudos to Jeff Brown and the Grails folks for releasing the Hudson plugin which enables Hudson builds using Gant.

9 comments:

iamsteveholmes said...

Hey Brock!
It's funny you should post about Hudson and Grails and on the same day ask me what I was doing on my blog:

http://capitalcodemonkey.blogspot.com/

It turns out what I was doing was exposing services (Gets and Posts) in the Grails application that I used by calling Groovy scripts from Hudson Jobs using HTTPClient. This was a basic CMDB that is now open source:
http://code.google.com/p/configuration-data-repository/

Brock Heinz said...

@iamsteveholmes

Ha - it's a small world :)

Tom Jenkins said...

Thanks for this. However in grails 1.1 the event has changed slightly. In your Events.groovy file change to

eventCreateWarStart = { warName, stagingDir ->
...
}

Everything else should be the same

Kevin J Slater said...

Tom Jenkins - thanks for posting that solution - it was driving me batty!

Anonymous said...

We have a similar feature on our internal site - but we were not allowed by the UI designers to add the version number and release date to the main site. So we made it as an "easter egg" by putting it in a div with display=none and added a small jQuery javascript to only reveal it when the logo is Ctrl-clicked:

$(document).ready(function() {
$("IMG.logo").bind("click", function(e) { if (e.metaKey) $("DIV.buildInfo").toggle("normal"); });
});


Almost as useful, and easier to "sneak in" in some environments - the UI designers said that they were fine with it, as long as it didn't bother regular users.

In addition, since it mostly wasn't shown, we added a few more lines of useful information there - of course, you must beware of revealing security-sensitive stuff.

Mike Hugo said...

thanks for tips - in Grails 1.0.3 I also needed to add this line to the top of your eventWarStart closure:
Ant.property(environment:"env")

Anonymous said...

This doesn't work for me using Grails 1.3.4 -- the war script depends on "compilegsp" which means that the GSP copied over to the WAR has already been used to create .class files, so any changes to the layout GSP during "eventCreateWarStart" won't affect the final app. Does anyone have this working at the moment? What did you do?

Thanks,


Rich

Brock Heinz said...

@Rich

Here's what I do now (1.3.2):

eventCompilegspStart = {arg1 ->
//move up the original layouts
mystaging = new File((File) grailsSettings.projectTargetDir, "mystaging")
String layoutPath = "${grailsSettings.baseDir}/grails-app/views/layouts"
ant.mkdir(dir: mystaging)
ant.move(todir: mystaging) {
ant.fileset(dir: layoutPath, includes: "*.gsp")
}

//transform them in
def hudsonBuild = ant.project.properties["environment.BUILD_NUMBER"] ?: "Custom Build"
ant.copy(todir: layoutPath, overwrite: true) {
ant.fileset(dir: mystaging, includes: "*.gsp")
ant.filterset() {
ant.filter(token: "HUDSON_BUILD", value: hudsonBuild)
}
}
}

eventCompilegspEnd = {arg1 ->
//GSP compiliation is done, move original layouts back
mystaging = new File((File) grailsSettings.projectTargetDir, "mystaging")
String layoutPath = "${grailsSettings.baseDir}/grails-app/views/layouts"
ant.move(todir: layoutPath) {
ant.fileset(dir: mystaging, includes: "*.gsp")
}
ant.delete(dir: mystaging)
}

Rich said...

@Brock

That's strange, I don't have a "CompilegspStart" event in my scripts (grails 1.3.4).

I've found a different solution which works fine for me here: http://wordpress.transentia.com.au/wordpress/2010/05/09/capturing-build-info-in-grails/