I’ve blogged before about how we use Jenkins at work for our continuous integration solution. One thing that our previous CI solution had was the ability for developers to run a standalone version of the tool on their development PC’s to check out large scale changes that might break several applications. Jenkins is much more difficult to do this with, mainly because we are using Rational Clearcase for SCM. We could use the Jenkins server to build from individual development streams if we wanted to, but it would require that all the files be checked in before being able to build locally.
I came up with a Groovy script that runs after each Nightly Build that collects the jobs that are currently in Jenkins and generates a standalone zip file that developers can download and launch a local instance of Jenkins to do a build from their view. The script is used as a Groovy build step.
import jenkins.model.*
import hudson.model.*
import java.io.*
import groovy.xml.*
import java.util.zip.ZipOutputStream
import java.util.zip.ZipEntry
import java.nio.channels.FileChannel
import java.util.regex.Pattern
// Retrieve parameters of the current build
def build = Thread.currentThread().executable
def build_id = 'build' + build.id
def root_dir = new File(build.workspace.toString())
def job_dir = new File(root_dir, 'jobs')
job_dir.mkdirs()
files = []
builds = []
def parentBuild = '''<?xml version='1.0' encoding='UTF-8'?>
<project>
<actions/>
<description></description>
<keepDependencies>false</keepDependencies>
<properties/>
<scm class="hudson.scm.NullSCM"/>
<canRoam>true</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers class="vector"/>
<concurrentBuild>false</concurrentBuild>
<builders/>
<publishers/>
<buildWrappers/>
</project>'''
def jenkinsConfigXml = '''<?xml version='1.0' encoding='UTF-8'?>
<hudson>
<disabledAdministrativeMonitors/>
<version>1.477</version>
<numExecutors>1</numExecutors>
<mode>NORMAL</mode>
<useSecurity>true</useSecurity>
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
<securityRealm class="hudson.security.SecurityRealm$None"/>
<projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
<workspaceDir>${ITEM_ROOTDIR}/workspace</workspaceDir>
<buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
<jdks/>
<viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
<myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
<clouds/>
<slaves/>
<quietPeriod>5</quietPeriod>
<scmCheckoutRetryCount>0</scmCheckoutRetryCount>
<views>
<hudson.model.AllView>
<owner class="hudson" reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
</hudson.model.AllView>
</views>
<primaryView>All</primaryView>
<slaveAgentPort>0</slaveAgentPort>
<label></label>
<nodeProperties/>
<globalNodeProperties/>
</hudson>'''
def binCopyXml = '''<?xml version='1.0' encoding='UTF-8'?>
<project>
<actions/>
<description></description>
<keepDependencies>false</keepDependencies>
<properties/>
<scm class="hudson.scm.NullSCM"/>
<canRoam>true</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers class="vector"/>
<concurrentBuild>false</concurrentBuild>
<builders>
<hudson.tasks.Shell>
<command>/usr/bin/find ${VIEW_ROOT} -iname '*.bin'
/usr/bin/mkdir -p "${RELEASE_DIR}"
/usr/bin/find ${VIEW_ROOT} -iname '*.bin' -print0 | /usr/bin/xargs -0 /usr/bin/cp -ut "${RELEASE_DIR}"
</command>
</hudson.tasks.Shell>
</builders>
<publishers/>
<buildWrappers/>
</project>'''
for(it in Jenkins.instance.items) {
if(it.name.contains('Hourly-Build') || it.name.contains('Daily-Build') || it.name.startsWith('ZZ')) continue
def this_job_dir = new File(job_dir, it.name)
this_job_dir.mkdirs()
def project = new XmlSlurper().parseText(it.configFile.asString())
// clean out stuff we don't need in the standalone build (email notification, etc)
project.builders."hudson.tasks.BatchFile".replaceNode {}
project.publishers."hudson.plugins.warnings.WarningsPublisher".replaceNode {}
project.publishers."hudson.plugins.postbuildtask.PostbuildTask".replaceNode {}
project.publishers."hudson.plugins.emailext.ExtendedEmailPublisher".replaceNode {}
project.buildWrappers."hudson.plugins.setenv.SetEnvBuildWrapper".replaceNode {}
project.properties."hudson.model.ParametersDefinitionProperty".replaceNode {}
configXml = new File(this_job_dir, "config.xml")
res = new StreamingMarkupBuilder().bind{
mkp.yield project
}
def outFile = new FileWriter(configXml)
def out = new PrintWriter(outFile)
out.write(res)
out.close()
outFile.close()
files.add(configXml)
builds.add(it.name)
println('Wrote: ' + configXml)
}
/*
<hudson.tasks.BuildTrigger>
<childProjects>PROJECTS</childProjects>
<threshold>
<name>SUCCESS</name>
<ordinal>0</ordinal>
<color>BLUE</color>
</threshold>
</hudson.tasks.BuildTrigger>
*/
// creat a build to copy binaries to the RELEASE_DIR
builds.add('ZZ-Binary-Copy')
def project = new XmlSlurper().parseText(parentBuild)
project.publishers.appendNode {
"hudson.tasks.BuildTrigger" {
childProjects(builds.join(','))
threshold {
name("SUCCESS")
ordinal("0")
color("BLUE")
}
}
}
def outFile = new FileWriter(new File(root_dir, 'config.xml'))
def out = new PrintWriter(outFile)
out.write(jenkinsConfigXml)
out.close()
// create a build to launch all the other builds
def buildAllDir = new File(job_dir, '00-Build-All')
buildAllDir.mkdirs()
outFile = new FileWriter(new File(buildAllDir, 'config.xml'))
out = new PrintWriter(outFile)
out.write(new StreamingMarkupBuilder().bind {
mkp.yield project
})
out.close()
def binCopyDir = new File(job_dir, 'ZZ-Binary-Copy')
binCopyDir.mkdirs()
outFile = new FileWriter(new File(binCopyDir, 'config.xml'))
out = new PrintWriter(outFile)
out.write(binCopyXml)
out.close()
// generate the bat file and zip file
def zipFileName = new File(root_dir, build_id + ".zip")
def root_dir_length = root_dir.absolutePath.length() + 1
def ignorePattern = Pattern.compile('\\.zip')
def jenkins_war = new File(new File(Jenkins.instance.servletContext.getRealPath(Jenkins.instance.servletContext.contextPath)).getParentFile().getParentFile(), 'jenkins.war')
def batfile = '''@echo off
set JENKINS_HOME=%cd%\\jenkins
IF NOT "%~1" == "" SET VIEW_ROOT=%1
IF NOT "%~2" == "" SET RELEASE_DIR=%2
IF "%VIEW_ROOT%." == "." GOTO ViewError
IF "%RELEASE_DIR%." == "." Goto ReleaseDirError
set PATH=C:\\cygwin\\bin;%PATH%
FOR /F "tokens=* USEBACKQ" %%A IN (`c:\\cygwin\\bin\\cygpath.exe "%VIEW_ROOT%"`) DO SET VIEW_ROOT=%%A
FOR /F "tokens=* USEBACKQ" %%A IN (`c:\\cygwin\\bin\\cygpath.exe "%RELEASE_DIR%"`) DO SET RELEASE_DIR=%%A
start "Jenkins" java -server -jar jenkins.war --httpPort=8099
echo "Jenkins will start and a browser window will be opened within 10 seconds"
@ping 127.0.0.1 -n 10 -w 1000> nul
start "Jenkins Web" http://localhost:8099
GOTO Done
:ViewError
ECHO "You must set the VIEW_ROOT environment variable (or pass it as the first parameter) to the PATH of the view to build in"
GOTO Done
:ReleaseDirError
ECHO "You must set the RELEASE_DIR environment variable (or pass it as the second parameter) to the PATH you want the binaries copied to"
:Done
'''
is = new ByteArrayInputStream(batfile.getBytes());
def zipFile = new ZipOutputStream(new FileOutputStream(zipFileName))
zipFile.putNextEntry(new ZipEntry('jenkins.war'))
zipFile << new FileInputStream(jenkins_war)
zipFile.putNextEntry(new ZipEntry('run_build_server.bat'))
zipFile << is
root_dir.eachFileRecurse { file ->
def relative = file.absolutePath.substring(root_dir_length).replace('\\', '/')
if ( file.isDirectory() && !relative.endsWith('/')){
relative += "/"
}
if( ignorePattern.matcher(relative).find() ){
return
}
zipFile.putNextEntry(new ZipEntry('jenkins/' + relative))
if( file.isFile() ){
zipFile << new FileInputStream(file)
}
}
zipFile.close()