Showing posts with label capistrano. Show all posts
Showing posts with label capistrano. Show all posts

Saturday, March 12, 2011

Adventures in GrailsBuilding (and Deployment)

For one of my recent Grails projects, I wanted to do some scripted build and deployment. There seems to be a ton of options available. I've recently been using Capistrano for a *Rails* project, which is pretty much the de facto standard on that side of the world. Capistrano is pretty powerful, but I'd rather not mix apples and oranges, and I much prefer Groovy over Ruby.

In Grails/Groovy land, we have Gant (Groovy wrapper for Ant), which is a Make-like facade around Groovy AntBuilder. We have Gradle, which is what the Grails scripts use.

In the end, I decided to go with simple Groovy + AntBuilder. My initial version of the script just does two things -- it runs the Grails War task, then uses JSch to scp the warfile to a bunch of servers. Even for just these two simple tasks, a bit of fancy footwork was required.

First of all, I wanted to define the deploy configuration in a Groovy Config file read in using ConfigSlurper. This format easily allows switching between different environments in a highly readable format.


def env = args[0]
def config = new ConfigSlurper(env).parse(new File('deployconfig.groovy').toURL())


Next, running the Ant GrailsTask was non-trivial. There are lots of references on the web to people having problems similar to mine -- for whatever reason, GrailsTask is not compatible with Ant 1.8, which is what is included in Groovy 1.7. One interesting thing I figured out was that AntBuilder hard-codes the Ant logging level to INFO. To set it to DEBUG, you can do this:


def ant = new AntBuilder()
ant.project.getBuildListeners().each {
it.setMessageOutputLevel(Project.MSG_DEBUG)
}


IN the end, I ended up having to downgrade to Groovy 1.6. MacPort makes this a pretty simple process (including the ability to easily switch back and forth between versions) that is documented very well here.

Next, I use Grape to download the dependent jars (grails and jsch). I do this within the script to make it as self-contained as possible. Unfortunately, the Grape API changed from Groovy 1.6 to 1.7, so I had to do the following to add the repositories to Grape:


def groovyVersion = org.codehaus.groovy.runtime.InvokerHelper.getVersion()
println "Groovy version $groovyVersion"

if (groovyVersion.startsWith("1.6")) {
Grape.resolve(name:'jsch', root:'http://jsch.sf.net/maven2/')
Grape.resolve(name:'grails', root:'http://repository.codehaus.org/')
} else {
Grape.addResolver(name:'jsch', root:'http://jsch.sf.net/maven2/')
Grape.addResolver(name:'grails', root:'http://repository.codehaus.org/')
}


Finally, to get the GrailsTask to run, I had to set up a classpath for it:


def grailsHome = System.getenv('GRAILS_HOME')
ant.path(id:'grails.classpath') {
fileset(dir:"$grailsHome/dist") {
include(name:"grails-bootstrap-*.jar")
}
fileset(dir:"$grailsHome/lib") {
include(name:"groovy-all*.jar")
include(name:"ivy*.jar")
include(name:"gpars*.jar")
include(name:"gant_groovy*.jar")
}
}
ant.taskdef(name: "grails", classname: "grails.ant.GrailsTask",
classpathref: 'grails.classpath'
)
ant.grails(home:grailsHome, script:'War')


The final step to copy the WAR file is pretty simple:

config.hosts.each { host ->
def commonParams = [
keyfile: config.keyfile,
username: config.username,
passphrase: '',
trust: true
] // these are parameters that are used for any SSH-related tasks
ant.scp(commonParams +
[
file: 'target/app.war',
todir: "${config.username}@$host:/${config.deployDir}"
]
}


Just a random side note... I sure wish the Groovy Object class included each(). It's REALLY convenient to be able to pass objects that may either be individual instances or Collections back and forth. Having to check for a Collection is unnecessary. If this were implemented, my config file could contain either:
hosts: 'hostname'
or
hosts: ['hostname1','hostname2']

instead of the more bulky
hosts: ['hostname']

When I get some more time, I want to look more closely at Gradle. But for now, my standalone Groovy script will suit my needs just fine.