diff --git a/README.md b/README.md index e7bc451..c772eb3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # Jenkins Build Per Git Flow Branch This script will allow you to keep your Jenkins jobs in sync with your Git repository (following Git Flow branching model). +**This repository was forked from https://github.com/neoteric-eu/jenkins-build-per-gitflow-branch** +*the build.gradle file was updated to contain the newest dependency versions along with the repository* + + ### Genesis This is a variation of a solution we found. Hence, the credit for the idea and initial implementation goes to Entagen and theirs [Jenkins Build Per Branch]. They explained it nicely, so it's advisable to take a look to their page. As stated, Entagen's version would suit better for a [GitHub flow] convention. Our need is to have three different templates for each of the Git Flow branches: features, releases, hotfixes and to sync them all in one 'scanning session' (single Jenkins sync job execution). I found it impossible using the original solution. So, we reused the concept of Entagen's script, but replaced the synchronization logic with what suited us better. @@ -47,7 +51,8 @@ The whole idea is to have a single Jenkins job which executes periodically, chec ##### 2. Add script parameters (provided in Switches box) - `-DjenkinsUrl` URL of the Jenkins.You should be able to append api/json to the URL to get JSON feed. - `-DjenkinsUser` Jenkins HTTP basic authorization user name. (optional) -- `-DjenkinsPasswrd` Jenkins HTTP basic authorization password. (optional) +- `-DjenkinsPassword` Jenkins HTTP basic authorization password. (optional) +*The original Usage has this variable as jenkinsPasswrd without the 'o'. This is why proofreading is so important" - `-DgitUrl` URL of the Git repository to make the synchronization against. - `-DdryRun` Pass this flag with any value and it won't make any changes to Jenkins (preview mode). It is recommended to use dry run until everything is set up correctly. (optional) - `-DtemplateJobPrefix` Prefix name of template jobs to use diff --git a/build.gradle b/build.gradle index 556eecc..9c1482e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,17 +2,19 @@ apply plugin: 'groovy' apply plugin: 'eclipse' repositories { - mavenCentral() + maven { + url "http://dlccasdigdsu08:8081/nexus/content/groups/aig_public/" + } } dependencies { - compile 'org.codehaus.groovy:groovy-all:2.3.3' - compile 'org.apache.ivy:ivy:2.2.0' + compile 'org.codehaus.groovy:groovy-all:2.4.4' + compile 'org.apache.ivy:ivy:2.4.0' compile 'commons-cli:commons-cli:1.2' // should be part of groovy, but not available when running for some reason - compile 'org.jooq:joox:1.2.0' + compile 'org.jooq:joox:1.2.0' testCompile 'junit:junit:4.11' testCompile 'org.assertj:assertj-core:1.7.0' - testCompile 'com.github.stefanbirkner:system-rules:1.7.0' + testCompile 'com.github.stefanbirkner:system-rules:1.7.0' compile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1' } @@ -36,5 +38,5 @@ task syncWithRepo(dependsOn: 'classes', type: JavaExec) { } task wrapper(type: Wrapper) { - gradleVersion = '1.12' + gradleVersion = '2.6' } \ No newline at end of file diff --git a/src/main/groovy/com/neoteric/jenkins/BranchView.groovy b/src/main/groovy/com/neoteric/jenkins/BranchView.groovy index 5582b13..5fb6ce6 100644 --- a/src/main/groovy/com/neoteric/jenkins/BranchView.groovy +++ b/src/main/groovy/com/neoteric/jenkins/BranchView.groovy @@ -9,6 +9,7 @@ class BranchView { } public String getSafeBranchName() { + println "---->geting the safe branch name -------->" return branchName.replaceAll('/', '_') } diff --git a/src/main/groovy/com/neoteric/jenkins/GitApi.groovy b/src/main/groovy/com/neoteric/jenkins/GitApi.groovy index eb7d42d..777cf7a 100644 --- a/src/main/groovy/com/neoteric/jenkins/GitApi.groovy +++ b/src/main/groovy/com/neoteric/jenkins/GitApi.groovy @@ -13,6 +13,7 @@ class GitApi { eachResultLine(command) { String line -> String branchNameRegex = "^.*\trefs/heads/(.*)\$" String branchName = line.find(branchNameRegex) { full, branchName -> branchName } + println "----- getBranchNames: branchName "+branchName Boolean selected = passesFilter(branchName) println "\t" + (selected ? "* " : " ") + "$line" // lines are in the format of: \trefs/heads/BRANCH_NAME diff --git a/src/main/groovy/com/neoteric/jenkins/JenkinsApi.groovy b/src/main/groovy/com/neoteric/jenkins/JenkinsApi.groovy index 0c60b6a..a45be49 100644 --- a/src/main/groovy/com/neoteric/jenkins/JenkinsApi.groovy +++ b/src/main/groovy/com/neoteric/jenkins/JenkinsApi.groovy @@ -13,8 +13,8 @@ import org.apache.http.protocol.HttpContext import org.apache.http.HttpRequest class JenkinsApi { - - + + final String SHOULD_START_PARAM_NAME = "startOnCreate" String jenkinsServerUrl RESTClient restClient @@ -42,7 +42,7 @@ class JenkinsApi { } List getJobNames(String prefix = null) { - println "getting project names from " + jenkinsServerUrl + "api/json" + def response = get(path: 'api/json') def jobNames = response.data.jobs.name if (prefix) return jobNames.findAll { it.startsWith(prefix) } @@ -50,26 +50,27 @@ class JenkinsApi { } String getJobConfig(String jobName) { + def response = get(path: "job/${jobName}/config.xml", contentType: TEXT, headers: [Accept: 'application/xml']) + response.data.text } - void cloneJobForBranch(String jobPrefix, ConcreteJob missingJob, String createJobInView, String gitUrl) { + void cloneJobForBranch(String templateJob, String missingJob, String branchName, String createJobInView, String gitUrl) { String createJobInViewPath = resolveViewPath(createJobInView) - println "-----> createInView after" + createJobInView - String missingJobConfig = configForMissingJob(missingJob, gitUrl) - TemplateJob templateJob = missingJob.templateJob + println "-----> createInView after: " + createJobInView + String missingJobConfig = configForMissingJob(templateJob, branchName, gitUrl) //Copy job with jenkins copy job api, this will make sure jenkins plugins get the call to make a copy if needed (promoted builds plugin needs this) - post(createJobInViewPath + 'createItem', missingJobConfig, [name: missingJob.jobName, mode: 'copy', from: templateJob.jobName], ContentType.XML) + post(createJobInViewPath + 'createItem', missingJobConfig, [name: missingJob, mode: 'copy', from: templateJob], ContentType.XML) - post('job/' + missingJob.jobName + "/config.xml", missingJobConfig, [:], ContentType.XML) + post('job/' + missingJob + "/config.xml", missingJobConfig, [:], ContentType.XML) //Forced disable enable to work around Jenkins' automatic disabling of clones jobs //But only if the original job was enabled - post('job/' + missingJob.jobName + '/disable') + post('job/' + missingJob + '/disable') if (!missingJobConfig.contains("true")) { - post('job/' + missingJob.jobName + '/enable') + post('job/' + missingJob+ '/enable') } } @@ -82,57 +83,62 @@ class JenkinsApi { elements.join(); } - String configForMissingJob(ConcreteJob missingJob, String gitUrl) { - TemplateJob templateJob = missingJob.templateJob - String config = getJobConfig(templateJob.jobName) - return processConfig(config, missingJob.branchName, gitUrl) + String configForMissingJob(String templateJob, String branchName, String gitUrl) { + String config = getJobConfig(templateJob) + return processConfig(config, branchName, gitUrl) } public String processConfig(String entryConfig, String branchName, String gitUrl) { + def root = new XmlParser().parseText(entryConfig) + // update branch name root.scm.branches."hudson.plugins.git.BranchSpec".name[0].value = "*/$branchName" - + // update GIT url root.scm.userRemoteConfigs."hudson.plugins.git.UserRemoteConfig".url[0].value = "$gitUrl" - + //update Sonar if (root.publishers."hudson.plugins.sonar.SonarPublisher".branch[0] != null) { root.publishers."hudson.plugins.sonar.SonarPublisher".branch[0].value = "$branchName" } - - + + //remove template build variable Node startOnCreateParam = findStartOnCreateParameter(root) if (startOnCreateParam) { startOnCreateParam.parent().remove(startOnCreateParam) } - + //check if it was the only parameter - if so, remove the enclosing tag, so the project won't be seen as build with parameters def propertiesNode = root.properties - def parameterDefinitionsProperty = propertiesNode."hudson.model.ParametersDefinitionProperty".parameterDefinitions[0] - - if(!parameterDefinitionsProperty.attributes() && !parameterDefinitionsProperty.children() && !parameterDefinitionsProperty.text()) { - root.remove(propertiesNode) - new Node(root, 'properties') + def parameterDefinitionsProperty + //the neoteric people hard coded property names and didn't do defensive coding. This causes an NPE. tisk tisk!! + if(propertiesNode."hudson.model.ParametersDefinitionProperty".parameterDefinitions[0] !=null){ + + parameterDefinitionsProperty = propertiesNode."hudson.model.ParametersDefinitionProperty".parameterDefinitions[0] + + if(!parameterDefinitionsProperty.attributes() && !parameterDefinitionsProperty.children() && !parameterDefinitionsProperty.text()) { + root.remove(propertiesNode) + new Node(root, 'properties') + } } - - + def writer = new StringWriter() XmlNodePrinter xmlPrinter = new XmlNodePrinter(new PrintWriter(writer)) xmlPrinter.setPreserveWhitespace(true) xmlPrinter.print(root) return writer.toString() } - - void startJob(ConcreteJob job) { - String templateConfig = getJobConfig(job.templateJob.jobName) + + void startJob(String templateJob, String jobName) { + String templateConfig = getJobConfig(templateJob) if (shouldStartJob(templateConfig)) { - println "Starting job ${job.jobName}." - post('job/' + job.jobName + '/build') + println "Starting job ${jobName}." + post('job/' + jobName + '/build') } } - + public boolean shouldStartJob(String config) { Node root = new XmlParser().parseText(config) Node startOnCreateParam = findStartOnCreateParameter(root) @@ -141,7 +147,7 @@ class JenkinsApi { } return startOnCreateParam.defaultValue[0]?.text().toBoolean() } - + Node findStartOnCreateParameter(Node root) { return root.properties."hudson.model.ParametersDefinitionProperty".parameterDefinitions."hudson.model.BooleanParameterDefinition".find { it.name[0].text() == SHOULD_START_PARAM_NAME diff --git a/src/main/groovy/com/neoteric/jenkins/JenkinsJobManager.groovy b/src/main/groovy/com/neoteric/jenkins/JenkinsJobManager.groovy index ebe3424..750e6a2 100644 --- a/src/main/groovy/com/neoteric/jenkins/JenkinsJobManager.groovy +++ b/src/main/groovy/com/neoteric/jenkins/JenkinsJobManager.groovy @@ -16,17 +16,13 @@ class JenkinsJobManager { Boolean noDelete = false Boolean startOnCreate = false - String featureSuffix = "feature-" - String hotfixSuffix = "hotfix-" - String releaseSuffix = "release-" - + String templateDevelopmentSuffix = "development" String templateFeatureSuffix = "feature" String templateHotfixSuffix = "hotfix" String templateReleaseSuffix = "release" - - def branchSuffixMatch = [(templateFeatureSuffix): featureSuffix, - (templateHotfixSuffix) : hotfixSuffix, - (templateReleaseSuffix): releaseSuffix] + List missingJobs + List jobsToDelete + def jobNameToBranchName JenkinsApi jenkinsApi GitApi gitApi @@ -35,130 +31,127 @@ class JenkinsJobManager { for (property in props) { this."${property.key}" = property.value } + initJenkinsApi() initGitApi() } void syncWithRepo() { List allBranchNames = gitApi.branchNames - println "-------------------------------------" - println "All branch names:" + allBranchNames + println "\n-------------------------------------" + println "All branch names:" + allBranchNames +"\t" List allJobNames = jenkinsApi.jobNames - println "-------------------------------------" - println "All job names:" + allJobNames - - List templateJobs = findRequiredTemplateJobs(allJobNames) - println "-------------------------------------" - println "Template Jobs:" + templateJobs - - List jobsWithJobPrefix = allJobNames.findAll { jobName -> - jobName.startsWith(jobPrefix + '-') - } - println "-------------------------------------" - println "Jobs with provided prefix:" + jobsWithJobPrefix + println "\n-------------------------------------" + println "All job names:" + allJobNames +"\t" + missingJobs = []; + jobsToDelete = []; + jobNameToBranchName = [:] // create any missing template jobs and delete any jobs matching the template patterns that no longer have branches - syncJobs(allBranchNames, jobsWithJobPrefix, templateJobs) - + syncJobs(allBranchNames, allJobNames) + addMissingJobs() + deleteJobs() } - public List findRequiredTemplateJobs(List allJobNames) { - String regex = /^($templateJobPrefix)-(.*)-($templateFeatureSuffix|$templateReleaseSuffix|$templateHotfixSuffix)$/ - - List templateJobs = allJobNames.findResults { String jobName -> - - TemplateJob templateJob = null - jobName.find(regex) {full, templateName, baseJobName, branchName -> - templateJob = new TemplateJob(jobName: full, baseJobName: baseJobName, templateBranchName: branchName) + void syncJobs(List allBranchNames, List allJobNames){ + //first check for missing jobs + List jobsWithJobPrefix = allJobNames.findAll { + jobName -> + jobName.startsWith(jobPrefix + '-') + } + println "\n-------------------------------------" + println "Jobs with provided prefix:" + jobsWithJobPrefix +"\t" + List jenkinsBranchNames = new ArrayList() + + //first check for branches that don't have jobs yet and add them + for(String branch:allBranchNames){ + String trueName = jobNameForBranch(branch,jobPrefix+"-"+templateJobPrefix); + jenkinsBranchNames.add(trueName) + if(!allJobNames.contains(trueName)){ + //add job + missingJobs.add(trueName) + jobNameToBranchName[trueName] = branch } - return templateJob } - assert templateJobs?.size() > 0, "Unable to find any jobs matching template regex: $regex\nYou need at least one job to match the templateJobPrefix and templateBranchName (feature, hotfix, release) suffix arguments" - return templateJobs + //then check for jobs that don't have branches anymore and need to be deleted + for(String job:allJobNames){ + if(!jenkinsBranchNames.contains(job)){ + //delete job + jobsToDelete.add(job) + } + } } - public void syncJobs(List allBranchNames, List jobNames, List templateJobs) { + void addMissingJobs(){ + for(String job:missingJobs){ + String templateJobName = jobPrefix+"-"+templateJobPrefix - def templateJobsByBranch = templateJobs.groupBy({ template -> template.templateBranchName }) - - List missingJobs = []; - List jobsToDelete = []; - - templateJobsByBranch.keySet().each { templateBranchToProcess -> - println "-> Checking $templateBranchToProcess branches" - List branchesWithCorrespondingTemplate = allBranchNames.findAll { branchName -> - branchName.startsWith(branchSuffixMatch[templateBranchToProcess]) + if(job.contains(templateJobName+"-"+templateDevelopmentSuffix)){ + templateJobName = templateJobName + "-" + templateDevelopmentSuffix } - - println "---> Founded corresponding branches: $branchesWithCorrespondingTemplate" - branchesWithCorrespondingTemplate.each { branchToProcess -> - println "-----> Processing branch: $branchToProcess" - List expectedJobsPerBranch = templateJobsByBranch[templateBranchToProcess].collect { TemplateJob templateJob -> - templateJob.concreteJobForBranch(jobPrefix, branchToProcess) - } - println "-------> Expected jobs:" - expectedJobsPerBranch.each { println " $it" } - List jobNamesPerBranch = jobNames.findAll{ it.endsWith(branchToProcess) } - println "-------> Job Names per branch:" - jobNamesPerBranch.each { println " $it" } - List missingJobsPerBranch = expectedJobsPerBranch.findAll { expectedJob -> - !jobNamesPerBranch.any {it.contains(expectedJob.jobName) } - } - println "-------> Missing jobs:" - missingJobsPerBranch.each { println " $it" } - missingJobs.addAll(missingJobsPerBranch) + else if(job.contains(templateJobName+"-"+templateFeatureSuffix)){ + templateJobName = templateJobName + "-" + templateFeatureSuffix } - - List deleteCandidates = jobNames.findAll { it.contains(branchSuffixMatch[templateBranchToProcess]) } - List jobsToDeletePerBranch = deleteCandidates.findAll { candidate -> - !branchesWithCorrespondingTemplate.any { candidate.endsWith(it) } + else if(job.contains(templateJobName+"-"+templateHotfixSuffix)){ + templateJobName = templateJobName + "-" + templateHotfixSuffix } - - println "-----> Jobs to delete:" - jobsToDeletePerBranch.each { println " $it" } - jobsToDelete.addAll(jobsToDeletePerBranch) - } - println "\nSummary:\n---------------" - if (missingJobs) { - for(ConcreteJob missingJob in missingJobs) { - println "Creating missing job: ${missingJob.jobName} from ${missingJob.templateJob.jobName}" - jenkinsApi.cloneJobForBranch(jobPrefix, missingJob, createJobInView, gitUrl) - jenkinsApi.startJob(missingJob) + else if(job.contains(templateJobName+"-"+templateReleaseSuffix)){ + templateJobName = templateJobName + "-" + templateReleaseSuffix + } + else { + //throw an error because a template job for this branch doesn't exist } + + String branchName = jobNameToBranchName[job] + + println "Creating missing job: ${job} from ${templateJobName}" + + jenkinsApi.cloneJobForBranch(templateJobName, missingJob, branchName, createJobInView, gitUrl) + jenkinsApi.startJob(templateJobName, missingJob) } - + } + + void deleteJobs(){ if (!noDelete && jobsToDelete) { println "Deleting deprecated jobs:\n\t${jobsToDelete.join('\n\t')}" - jobsToDelete.each { String jobName -> + jobsToDelete.each { + String jobName -> jenkinsApi.deleteJob(jobName) } } } - - JenkinsApi initJenkinsApi() { - if (!jenkinsApi) { - assert jenkinsUrl != null - if (dryRun) { - println "DRY RUN! Not executing any POST commands to Jenkins, only GET commands" - this.jenkinsApi = new JenkinsApiReadOnly(jenkinsServerUrl: jenkinsUrl) - } else { - this.jenkinsApi = new JenkinsApi(jenkinsServerUrl: jenkinsUrl) + + + JenkinsApi initJenkinsApi() { + if (!jenkinsApi) { + assert jenkinsUrl != null + if (dryRun) { + println "DRY RUN! Not executing any POST commands to Jenkins, only GET commands" + this.jenkinsApi = new JenkinsApiReadOnly(jenkinsServerUrl: jenkinsUrl) + } else { + this.jenkinsApi = new JenkinsApi(jenkinsServerUrl: jenkinsUrl) + } + + if (jenkinsUser || jenkinsPassword) this.jenkinsApi.addBasicAuth(jenkinsUser, jenkinsPassword) } - if (jenkinsUser || jenkinsPassword) this.jenkinsApi.addBasicAuth(jenkinsUser, jenkinsPassword) + return this.jenkinsApi } - return this.jenkinsApi - } + GitApi initGitApi() { + if (!gitApi) { + assert gitUrl != null + this.gitApi = new GitApi(gitUrl: gitUrl) + } - GitApi initGitApi() { - if (!gitApi) { - assert gitUrl != null - this.gitApi = new GitApi(gitUrl: gitUrl) + return this.gitApi } - return this.gitApi + String jobNameForBranch(String branchName, String baseJobName) { + // git branches often have a forward slash in them, but they make jenkins cranky, turn it into an underscore + String safeBranchName = branchName.replaceAll('/', '-') + return "$baseJobName-$safeBranchName" + } } -} diff --git a/src/main/groovy/com/neoteric/jenkins/TemplateJob.groovy b/src/main/groovy/com/neoteric/jenkins/TemplateJob.groovy index 902deea..3498cd2 100644 --- a/src/main/groovy/com/neoteric/jenkins/TemplateJob.groovy +++ b/src/main/groovy/com/neoteric/jenkins/TemplateJob.groovy @@ -6,17 +6,26 @@ import groovy.transform.ToString; @ToString @EqualsAndHashCode class TemplateJob { - String jobName - String baseJobName - String templateBranchName + String jobName + String baseJobName + String templateBranchName - String jobNameForBranch(String branchName) { - // git branches often have a forward slash in them, but they make jenkins cranky, turn it into an underscore - String safeBranchName = branchName.replaceAll('/', '_') - return "$baseJobName-$safeBranchName" - } - - ConcreteJob concreteJobForBranch(String jobPrefix, String branchName) { - ConcreteJob concreteJob = new ConcreteJob(templateJob: this, branchName: branchName, jobName: jobPrefix + '-' + jobNameForBranch(branchName) ) - } + TemplateJob () {} + + TemplateJob(String jobName, String baseJobName, String templateBranchName) { + this.jobName = jobName + this.baseJobName = baseJobName + this.templateBranchName = templateBranchName + } + + String jobNameForBranch(String branchName) { + // git branches often have a forward slash in them, but they make jenkins cranky, turn it into an underscore + String safeBranchName = branchName.replaceAll('/', '-') + println "\t\t\t\t jenkins branch name is "+safeBranchName + return "$baseJobName-$safeBranchName" + } + + ConcreteJob concreteJobForBranch(String jobPrefix, String branchName) { + ConcreteJob concreteJob = new ConcreteJob(templateJob: this, branchName: branchName, jobName: jobPrefix + '-' + jobNameForBranch(branchName) ) + } }