├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── main └── groovy │ └── com │ └── neoteric │ └── jenkins │ ├── BranchView.groovy │ ├── ConcreteJob.groovy │ ├── GitApi.groovy │ ├── JenkinsApi.groovy │ ├── JenkinsApiReadOnly.groovy │ ├── JenkinsJobManager.groovy │ ├── Main.groovy │ ├── SonarApi.groovy │ ├── SonarApiReadOnly.groovy │ └── TemplateJob.groovy └── test └── groovy └── com └── neoteric └── jenkins ├── GitApiTests.groovy ├── JenkinsApiReadOnlyTests.groovy ├── JenkinsApiTests.groovy └── JenkinsJobManagerTests.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | *.iml 3 | *.ipr 4 | *.iws 5 | *.diff 6 | build 7 | .gradle 8 | .classpath 9 | .project 10 | .settings 11 | out 12 | atlassian-ide-plugin.xml 13 | /bin/ 14 | /.nb-gradle/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jenkins Build Per Git Flow Branch 2 | 3 | [![Join the chat at https://gitter.im/neoteric-eu/jenkins-build-per-gitflow-branch](https://badges.gitter.im/neoteric-eu/jenkins-build-per-gitflow-branch.svg)](https://gitter.im/neoteric-eu/jenkins-build-per-gitflow-branch?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | This script will allow you to keep your Jenkins jobs in sync with your Git repository (following Git Flow branching model). 6 | 7 | ### Genesis 8 | 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. 9 | So, we reused the concept of Entagen's script, but replaced the synchronization logic with what suited us better. 10 | 11 | ### Installation 12 | Requirements are the same for both script versions: 13 | - [Jenkins Git Plugin] 14 | - [Jenkins Gradle plugin] 15 | - The git command line app also must be installed (this is already a requirement of the Jenkins Git Plugin), and it must be configured with credentials (likely an SSH key) that has authorization rights to the git repository 16 | - The best idea is to clone / fork this repository for your own usage (to make sure that the script remain intact). However, you can still use ours if you like. 17 | 18 | ### Naming convention 19 | To make this script work properly, job names must follow few rules: 20 | 21 | Template jobs should follow 22 | `--` name, where: 23 | - *templateJobPrefix* - a prefix which distinguish particular type of template (one template type can be reused among several projects (explained further) 24 | - *jobName* - name of your Jenkins job purpose, ex. build etc. 25 | - *branchName* - one of the 3 Git Flow branch types: *feature*, *release*, *hotfix* 26 | 27 | Regular jobs should follow similar pattern `--`, where: 28 | - *jobPrefix* - is a prefix which distinguish particular project 29 | - for *jobName* and *branchName* apply the same rule as for templates 30 | 31 | Git Flow branches should start with *feature-*, *hotfix-*, *release-* prefixes. It is because, Jenkins is having hard time with slashes (i.e. *feature/*, *hotfix/*, *release/*). There are some workarounds (substituting '/' with and underscore for a jenkins job name - take a look into the [code of Entagen version]), but we were fine with this trade off. 32 | 33 | ### Usage 34 | Usage is also very similiar to the original, but let me retrace the steps: 35 | ##### 1. Create Jenkins synchronization job 36 | The whole idea is to have a single Jenkins job which executes periodically, checks Git repository and creates / removes Jenkins jobs for each of the Git Flow dynamic branch (other than master and development). 37 | > **Note**: If no template for particular branch / job is available, branch will be ignored (job won't be created nor deleted). 38 | 39 | - Create new "*Freestyle project*" kind of Jenkins job. 40 | - Name it accordingly, ex. ProjectName-SyncJobs. 41 | - For Git URL provide this script location (or your forked / cloned one): *git@github.com:neoteric-eu/jenkins-build-per-gitflow-branch.git* 42 | - Set appropriate branch to build (ours is *origin/master*) 43 | - Make sure it's triggered periodically (ex. every 5 minutes: __H/5 \* \* \* \*__) 44 | - Add a build step "*Invoke Gradle script*" and set it's *Tasks* field to **syncWithRepo** 45 | - Provide script parameters (explained below) in *Switches* box 46 | 47 | 48 | > **Important note from Entagen site**: This job is potentially destructive as it will delete old feature branch jobs for feature branches that no longer exist. It's strongly recommended that you back up your jenkins jobs directory before running, just in case. Another good alternative would be to put your jobs directory under git version control. Ignore workspace and builds directories and just about everything can be added. Commit periodocally and if something bad happens, revert back to the last known good version. 49 | 50 | ##### 2. Add script parameters (provided in Switches box) 51 | - `-DjenkinsUrl` URL of the Jenkins.You should be able to append api/json to the URL to get JSON feed. 52 | - `-DjenkinsUser` Jenkins HTTP basic authorization user name. (optional) 53 | - `-DjenkinsPassword` Jenkins HTTP basic authorization password. (optional) 54 | - `-DgitUrl` URL of the Git repository to make the synchronization against. 55 | - `-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) 56 | - `-DtemplateJobPrefix` Prefix name of template jobs to use 57 | - `-DjobPrefix` Prefix name of project jobs to create 58 | - `-DcreateJobInView` If you want the script to create the job in a view provide the view name here. It also supports nested views, just separate them with a slash '/', ex. *view/nestedview* 59 | - `-DnoDelete` pass this flag with *true* value to avoid removing obsolete jobs (with no corresponding git branch) (optional) 60 | - `-DsonarUrl` URL of the Sonar. (optional) 61 | - `-DsonarUser` Sonar HTTP basic authorization user name. (optional) 62 | - `-DsonarPassword` Sonar HTTP basic authorisation password. (optional) 63 | 64 | Sample parameters configuration: 65 | ``` 66 | -DjenkinsUrl=http://myjenkinshost.com:8080/ 67 | -DjenkinsUser=username 68 | -DjenkinsPassword=password 69 | -DgitUrl=git@githost.com/project.git 70 | -DtemplateJobPrefix=SimpleJarTemplate 71 | -DjobPrefix=ProjectOne 72 | -DcreateJobInView=ProjectOne 73 | -DsonarPassword=password 74 | -DsonarUser=user 75 | -DsonarUrl=http://mysonarhost.com:9090/ 76 | ``` 77 | 78 | ##### 3. Templates 79 | The idea of this script is to be able to handle separate template version per Git Flow branch type. What's more you can reuse a template set among your projects. For example, we are currently basing only on two sets of templates. One for simple jar modules and the second one for the final build of our micro services (built to Debian packages). In each set we have a template for each branch (feature, release, hotfix). Because of releasing and versioning capabilities we want to handle each branch type differently. 80 | 81 | Notes on configuring your template: 82 | - If you want to start your job immediately after it's created, mark the template as parametrized build and add a Boolean parameter named **startOnCreate** and set it's default value to true (tick in the checkbox) 83 | - Git repository URL is going to be replaced by the script (with the project Git URL set in sync job parameters) 84 | - Branch to build is going to be determined and set by the script 85 | - If you use Sonar and want to have Sonar builds separated for each branch type, just add Sonar capability to your template and the Sonar branch option will be determined and set by the script 86 | 87 | ##### 4. Sonar Notes 88 | When a -DsonarUser and -DsonarUrl flags are both used script will try to delete Sonar project created by a template, when a job in Jenkins becomes deprecated. 89 | 90 | [Jenkins Build Per Branch]:http://entagen.github.io/jenkins-build-per-branch/ 91 | [GitHub flow]:http://scottchacon.com/2011/08/31/github-flow.html 92 | [Jenkins Git Plugin]:https://wiki.jenkins-ci.org/display/JENKINS/Git+Plugin 93 | [Jenkins Gradle plugin]:https://wiki.jenkins-ci.org/display/JENKINS/Gradle+Plugin 94 | [code of Entagen version]:https://github.com/entagen/jenkins-build-per-branch/blob/master/src/main/groovy/com/entagen/jenkins/TemplateJob.groovy 95 | 96 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'groovy' 2 | apply plugin: 'eclipse' 3 | 4 | repositories { 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | compile 'org.codehaus.groovy:groovy-all:2.3.3' 10 | compile 'org.apache.ivy:ivy:2.2.0' 11 | compile 'commons-cli:commons-cli:1.2' // should be part of groovy, but not available when running for some reason 12 | compile 'org.jooq:joox:1.2.0' 13 | testCompile 'junit:junit:4.11' 14 | testCompile 'org.assertj:assertj-core:1.7.0' 15 | testCompile 'com.github.stefanbirkner:system-rules:1.7.0' 16 | compile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1' 17 | } 18 | 19 | task createSourceDirs(description : 'Create empty source directories for all defined sourceSets') << { 20 | sourceSets*.allSource.srcDirs.flatten().each { File sourceDirectory -> 21 | if (!sourceDirectory.exists()) { 22 | println "Making $sourceDirectory" 23 | sourceDirectory.mkdirs() 24 | } 25 | } 26 | } 27 | 28 | task syncWithRepo(dependsOn: 'classes', type: JavaExec) { 29 | main = 'com.neoteric.jenkins.Main' 30 | classpath = sourceSets.main.runtimeClasspath 31 | // pass through specified system properties to the call to main 32 | ['help', 'jenkinsUrl', 'jenkinsUser', 'jenkinsPassword', 'gitUrl', 'jobPrefix', 'templateJobPrefix', 'dryRun', 'createJobInView', 'scriptCommand', 'noDelete', 'sonarPassword', 'sonarUser', 'sonarUrl'].each { 33 | if (System.getProperty(it)) systemProperty it, System.getProperty(it) 34 | } 35 | 36 | } 37 | 38 | task wrapper(type: Wrapper) { 39 | gradleVersion = '1.12' 40 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neoteric-eu/jenkins-build-per-gitflow-branch/48558e5337cadb5389c1165e44eafa685b150d28/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 15 12:03:24 CEST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-1.12-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/groovy/com/neoteric/jenkins/BranchView.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | class BranchView { 4 | String templateJobPrefix 5 | String branchName 6 | 7 | public String getViewName() { 8 | return "$templateJobPrefix-$safeBranchName" 9 | } 10 | 11 | public String getSafeBranchName() { 12 | return branchName.replaceAll('/', '_') 13 | } 14 | 15 | 16 | public String toString() { 17 | return this.viewName 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/groovy/com/neoteric/jenkins/ConcreteJob.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | import groovy.transform.EqualsAndHashCode; 4 | import groovy.transform.ToString; 5 | 6 | @ToString 7 | @EqualsAndHashCode 8 | class ConcreteJob { 9 | TemplateJob templateJob 10 | String jobName 11 | String branchName 12 | } 13 | -------------------------------------------------------------------------------- /src/main/groovy/com/neoteric/jenkins/GitApi.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | import java.util.regex.Pattern 4 | 5 | class GitApi { 6 | 7 | String gitUrl 8 | 9 | public List getBranchNames() { 10 | String command = "git ls-remote --heads ${gitUrl}" 11 | List branchNames = [] 12 | 13 | eachResultLine(command) { String line -> 14 | String branchNameRegex = "^.*\trefs/heads/(.*)\$" 15 | String branchName = line.find(branchNameRegex) { full, branchName -> branchName } 16 | Boolean selected = passesFilter(branchName) 17 | println "\t" + (selected ? "* " : " ") + "$line" 18 | // lines are in the format of: \trefs/heads/BRANCH_NAME 19 | // ex: b9c209a2bf1c159168bf6bc2dfa9540da7e8c4a26\trefs/heads/master 20 | if (selected) branchNames << branchName 21 | } 22 | 23 | return branchNames 24 | } 25 | 26 | public Boolean passesFilter(String branchName) { 27 | if (!branchName) return false 28 | return true 29 | } 30 | 31 | // assumes all commands are "safe", if we implement any destructive git commands, we'd want to separate those out for a dry-run 32 | public void eachResultLine(String command, Closure closure) { 33 | println "executing command: $command" 34 | def process = command.execute() 35 | def inputStream = process.getInputStream() 36 | def gitOutput = "" 37 | 38 | while(true) { 39 | int readByte = inputStream.read() 40 | if (readByte == -1) break // EOF 41 | byte[] bytes = new byte[1] 42 | bytes[0] = readByte 43 | gitOutput = gitOutput.concat(new String(bytes)) 44 | } 45 | process.waitFor() 46 | 47 | if (process.exitValue() == 0) { 48 | gitOutput.eachLine { String line -> 49 | closure(line) 50 | } 51 | } else { 52 | String errorText = process.errorStream.text?.trim() 53 | println "error executing command: $command" 54 | println errorText 55 | throw new Exception("Error executing command: $command -> $errorText") 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/groovy/com/neoteric/jenkins/JenkinsApi.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | import groovyx.net.http.ContentType 4 | import groovyx.net.http.HTTPBuilder 5 | import groovyx.net.http.RESTClient 6 | import org.apache.commons.lang.StringEscapeUtils 7 | 8 | import static groovyx.net.http.ContentType.* 9 | 10 | import org.apache.http.conn.HttpHostConnectException 11 | import org.apache.http.client.HttpResponseException 12 | import org.apache.http.HttpStatus 13 | import org.apache.http.HttpRequestInterceptor 14 | import org.apache.http.protocol.HttpContext 15 | import org.apache.http.HttpRequest 16 | 17 | class JenkinsApi { 18 | 19 | 20 | final String SHOULD_START_PARAM_NAME = "startOnCreate" 21 | String jenkinsServerUrl 22 | RESTClient restClient 23 | HttpRequestInterceptor requestInterceptor 24 | boolean findCrumb = true 25 | def crumbInfo 26 | 27 | public void setJenkinsServerUrl(String jenkinsServerUrl) { 28 | if (!jenkinsServerUrl.endsWith("/")) jenkinsServerUrl += "/" 29 | this.jenkinsServerUrl = jenkinsServerUrl 30 | this.restClient = new RESTClient(jenkinsServerUrl) 31 | } 32 | 33 | public void addBasicAuth(String jenkinsServerUser, String jenkinsServerPassword) { 34 | println "use basic authentication" 35 | 36 | this.requestInterceptor = new HttpRequestInterceptor() { 37 | void process(HttpRequest httpRequest, HttpContext httpContext) { 38 | def auth = jenkinsServerUser + ':' + jenkinsServerPassword 39 | httpRequest.addHeader('Authorization', 'Basic ' + auth.bytes.encodeBase64().toString()) 40 | } 41 | } 42 | 43 | this.restClient.client.addRequestInterceptor(this.requestInterceptor) 44 | } 45 | 46 | List getJobNames(String prefix = null) { 47 | println "getting project names from " + jenkinsServerUrl + "api/json" 48 | def response = get(path: 'api/json') 49 | def jobNames = response.data.jobs.name 50 | if (prefix) return jobNames.findAll { it.startsWith(prefix) } 51 | return jobNames 52 | } 53 | 54 | String getJobConfig(String jobName) { 55 | def response = get(path: "job/${jobName}/config.xml", contentType: TEXT, 56 | headers: [Accept: 'application/xml']) 57 | response.data.text 58 | } 59 | 60 | void cloneJobForBranch(String jobPrefix, ConcreteJob missingJob, String createJobInView, String gitUrl, String scriptCommand) { 61 | String createJobInViewPath = resolveViewPath(createJobInView) 62 | println "-----> createInView after" + createJobInView 63 | String missingJobConfig = configForMissingJob(missingJob, gitUrl, scriptCommand) 64 | TemplateJob templateJob = missingJob.templateJob 65 | 66 | //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) 67 | post(createJobInViewPath + 'createItem', missingJobConfig, [name: missingJob.jobName, mode: 'copy', from: templateJob.jobName], ContentType.XML) 68 | 69 | post('job/' + missingJob.jobName + "/config.xml", missingJobConfig, [:], ContentType.XML) 70 | //Forced disable enable to work around Jenkins' automatic disabling of clones jobs 71 | //But only if the original job was enabled 72 | post('job/' + missingJob.jobName + '/disable') 73 | if (!missingJobConfig.contains("true")) { 74 | post('job/' + missingJob.jobName + '/enable') 75 | } 76 | } 77 | 78 | public String resolveViewPath(String createInView) { 79 | if (!createInView) { 80 | return "" 81 | } 82 | List elements = createInView.tokenize("/") 83 | elements = elements.collect { "view/" + it + "/" } 84 | elements.join(); 85 | } 86 | 87 | String configForMissingJob(ConcreteJob missingJob, String gitUrl, String scriptCommand) { 88 | TemplateJob templateJob = missingJob.templateJob 89 | String config = getJobConfig(templateJob.jobName) 90 | return processConfig(config, missingJob.branchName, gitUrl, scriptCommand) 91 | } 92 | 93 | public String processConfig(String entryConfig, String branchName, String gitUrl, String scriptCommand) { 94 | 95 | def root = new XmlParser().parseText(entryConfig) 96 | // update branch name 97 | root.scm.branches."hudson.plugins.git.BranchSpec".name[0].value = "*/$branchName" 98 | 99 | // update GIT url 100 | root.scm.userRemoteConfigs."hudson.plugins.git.UserRemoteConfig".url[0].value = "$gitUrl" 101 | 102 | //update Sonar 103 | if (root.publishers."hudson.plugins.sonar.SonarPublisher".branch[0] != null) { 104 | root.publishers."hudson.plugins.sonar.SonarPublisher".branch[0].value = "$branchName" 105 | } 106 | 107 | //update Publish over SSH exec 108 | def publishers = root.postbuilders."jenkins.plugins.publish__over__ssh.BapSshBuilderPlugin".delegate.delegate.publishers."jenkins.plugins.publish__over__ssh.BapSshPublisher" 109 | if (publishers != null) { 110 | for (publisher in publishers) { 111 | publisher.transfers[0]."jenkins.plugins.publish__over__ssh.BapSshTransfer"[0].execCommand[0].value = "$scriptCommand" 112 | } 113 | } 114 | 115 | //remove template build variable 116 | Node startOnCreateParam = findStartOnCreateParameter(root) 117 | if (startOnCreateParam) { 118 | startOnCreateParam.parent().remove(startOnCreateParam) 119 | } 120 | 121 | //check if it was the only parameter - if so, remove the enclosing tag, so the project won't be seen as build with parameters 122 | def propertiesNode = root.properties 123 | def parameterDefinitionsProperty = propertiesNode."hudson.model.ParametersDefinitionProperty".parameterDefinitions[0] 124 | 125 | if (!parameterDefinitionsProperty.attributes() && !parameterDefinitionsProperty.children() && !parameterDefinitionsProperty.text()) { 126 | root.remove(propertiesNode) 127 | new Node(root, 'properties') 128 | } 129 | 130 | 131 | def writer = new StringWriter() 132 | XmlNodePrinter xmlPrinter = new XmlNodePrinter(new PrintWriter(writer)) 133 | xmlPrinter.setPreserveWhitespace(true) 134 | xmlPrinter.print(root) 135 | return writer.toString() 136 | } 137 | 138 | void startJob(ConcreteJob job) { 139 | String templateConfig = getJobConfig(job.templateJob.jobName) 140 | if (shouldStartJob(templateConfig)) { 141 | println "Starting job ${job.jobName}." 142 | post('job/' + job.jobName + '/build') 143 | } 144 | } 145 | 146 | public boolean shouldStartJob(String config) { 147 | Node root = new XmlParser().parseText(config) 148 | Node startOnCreateParam = findStartOnCreateParameter(root) 149 | if (!startOnCreateParam) { 150 | return false 151 | } 152 | return startOnCreateParam.defaultValue[0]?.text().toBoolean() 153 | } 154 | 155 | Node findStartOnCreateParameter(Node root) { 156 | return root.properties."hudson.model.ParametersDefinitionProperty".parameterDefinitions."hudson.model.BooleanParameterDefinition".find { 157 | it.name[0].text() == SHOULD_START_PARAM_NAME 158 | } 159 | } 160 | 161 | void deleteJob(String jobName) { 162 | println "deleting job $jobName" 163 | post("job/${jobName}/doDelete") 164 | } 165 | 166 | 167 | protected get(Map map) { 168 | // get is destructive to the map, if there's an error we want the values around still 169 | Map mapCopy = map.clone() as Map 170 | def response 171 | 172 | assert mapCopy.path != null, "'path' is a required attribute for the GET method" 173 | 174 | try { 175 | response = restClient.get(map) 176 | } catch (HttpHostConnectException ex) { 177 | println "Unable to connect to host: $jenkinsServerUrl" 178 | throw ex 179 | } catch (UnknownHostException ex) { 180 | println "Unknown host: $jenkinsServerUrl" 181 | throw ex 182 | } catch (HttpResponseException ex) { 183 | def message = "Unexpected failure with path $jenkinsServerUrl${mapCopy.path}, HTTP Status Code: ${ex.response?.status}, full map: $mapCopy" 184 | throw new Exception(message, ex) 185 | } 186 | 187 | assert response.status < 400 188 | return response 189 | } 190 | 191 | /** 192 | * @author Kelly Robinson 193 | * from https://github.com/kellyrob99/Jenkins-api-tour/blob/master/src/main/groovy/org/kar/hudson/api/PostRequestSupport.groovy 194 | */ 195 | protected Integer post(String path, postBody = [:], params = [:], ContentType contentType = ContentType.URLENC) { 196 | println "----> MAKING POST with PATH: " + path 197 | //Added the support for jenkins CSRF option, this could be changed to be a build flag if needed. 198 | //http://jenkinsurl.com/crumbIssuer/api/json get crumb for csrf protection json: {"crumb":"c8d8812d615292d4c0a79520bacfa7d8","crumbRequestField":".crumb"} 199 | if (findCrumb) { 200 | findCrumb = false 201 | println "Trying to find crumb: ${jenkinsServerUrl}crumbIssuer/api/json" 202 | try { 203 | def response = restClient.get(path: "crumbIssuer/api/json") 204 | 205 | if (response.data.crumbRequestField && response.data.crumb) { 206 | crumbInfo = [:] 207 | crumbInfo['field'] = response.data.crumbRequestField 208 | crumbInfo['crumb'] = response.data.crumb 209 | } else { 210 | println "Found crumbIssuer but didn't understand the response data trying to move on." 211 | println "Response data: " + response.data 212 | } 213 | } 214 | catch (HttpResponseException e) { 215 | if (e.response?.status == 404) { 216 | println "Couldn't find crumbIssuer for jenkins. Just moving on it may not be needed." 217 | } else { 218 | def msg = "Unexpected failure on ${jenkinsServerUrl}crumbIssuer/api/json: ${resp.statusLine} ${resp.status}" 219 | throw new Exception(msg) 220 | } 221 | } 222 | } 223 | 224 | if (crumbInfo) { 225 | params[crumbInfo.field] = crumbInfo.crumb 226 | } 227 | 228 | HTTPBuilder http = new HTTPBuilder(jenkinsServerUrl) 229 | 230 | if (requestInterceptor) { 231 | http.client.addRequestInterceptor(this.requestInterceptor) 232 | } 233 | 234 | Integer status = HttpStatus.SC_EXPECTATION_FAILED 235 | 236 | http.handler.failure = { resp -> 237 | def msg = "Unexpected failure on $jenkinsServerUrl$path: ${resp.statusLine} ${resp.status}" 238 | status = resp.statusLine.statusCode 239 | throw new Exception(msg) 240 | } 241 | 242 | http.post(path: path, body: postBody, query: params, 243 | requestContentType: contentType) { resp -> 244 | assert resp.statusLine.statusCode < 400 245 | status = resp.statusLine.statusCode 246 | } 247 | return status 248 | } 249 | 250 | } 251 | -------------------------------------------------------------------------------- /src/main/groovy/com/neoteric/jenkins/JenkinsApiReadOnly.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | import groovyx.net.http.ContentType 4 | import groovyx.net.http.HTTPBuilder 5 | import groovyx.net.http.RESTClient 6 | import org.apache.http.client.HttpResponseException 7 | import org.apache.http.conn.HttpHostConnectException 8 | 9 | import static groovyx.net.http.ContentType.TEXT 10 | import org.apache.http.HttpStatus 11 | 12 | class JenkinsApiReadOnly extends JenkinsApi { 13 | 14 | @Override 15 | protected Integer post(String path, postBody = [:], params = [:], ContentType contentType = ContentType.URLENC) { 16 | println "READ ONLY! skipping POST to $path with params: ${params}, postBody:\n$postBody" 17 | // we never want to post anything with a ReadOnly API, just return OK for all requests to it 18 | return HttpStatus.SC_OK 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/groovy/com/neoteric/jenkins/JenkinsJobManager.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | class JenkinsJobManager { 4 | 5 | String templateJobPrefix 6 | String jobPrefix 7 | String gitUrl 8 | String jenkinsUrl 9 | String createJobInView 10 | String jenkinsUser 11 | String jenkinsPassword 12 | String scriptCommand 13 | String sonarUrl 14 | String sonarUser 15 | String sonarPassword 16 | 17 | Boolean dryRun = false 18 | Boolean noDelete = false 19 | Boolean startOnCreate = false 20 | 21 | String featureSuffix = "feature-" 22 | String hotfixSuffix = "hotfix-" 23 | String releaseSuffix = "release-" 24 | 25 | String templateFeatureSuffix = "feature" 26 | String templateHotfixSuffix = "hotfix" 27 | String templateReleaseSuffix = "release" 28 | 29 | def branchSuffixMatch = [(templateFeatureSuffix): featureSuffix, 30 | (templateHotfixSuffix) : hotfixSuffix, 31 | (templateReleaseSuffix): releaseSuffix] 32 | 33 | JenkinsApi jenkinsApi 34 | GitApi gitApi 35 | SonarApi sonarApi 36 | 37 | JenkinsJobManager(Map props) { 38 | for (property in props) { 39 | this."${property.key}" = property.value 40 | } 41 | initJenkinsApi() 42 | initGitApi() 43 | 44 | if (sonarUrl && sonarUser) { 45 | initSonarApi() 46 | } 47 | } 48 | 49 | void syncWithRepo() { 50 | List allBranchNames = gitApi.branchNames 51 | println "-------------------------------------" 52 | println "All branch names:" + allBranchNames 53 | 54 | List allJobNames = jenkinsApi.jobNames 55 | println "-------------------------------------" 56 | println "All job names:" + allJobNames 57 | 58 | List templateJobs = findRequiredTemplateJobs(allJobNames) 59 | println "-------------------------------------" 60 | println "Template Jobs:" + templateJobs 61 | 62 | List jobsWithJobPrefix = allJobNames.findAll { jobName -> 63 | jobName.startsWith(jobPrefix + '-') 64 | } 65 | println "-------------------------------------" 66 | println "Jobs with provided prefix:" + jobsWithJobPrefix 67 | 68 | // create any missing template jobs and delete any jobs matching the template patterns that no longer have branches 69 | syncJobs(allBranchNames, jobsWithJobPrefix, templateJobs) 70 | 71 | } 72 | 73 | public List findRequiredTemplateJobs(List allJobNames) { 74 | String regex = /^($templateJobPrefix)-(.*)-($templateFeatureSuffix|$templateReleaseSuffix|$templateHotfixSuffix)$/ 75 | 76 | List templateJobs = allJobNames.findResults { String jobName -> 77 | 78 | TemplateJob templateJob = null 79 | jobName.find(regex) { full, templateName, baseJobName, branchName -> 80 | templateJob = new TemplateJob(jobName: full, baseJobName: baseJobName, templateBranchName: branchName) 81 | } 82 | return templateJob 83 | } 84 | 85 | 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" 86 | return templateJobs 87 | } 88 | 89 | public void syncJobs(List allBranchNames, List jobNames, List templateJobs) { 90 | 91 | def templateJobsByBranch = templateJobs.groupBy({ template -> template.templateBranchName }) 92 | 93 | List missingJobs = []; 94 | List jobsToDelete = []; 95 | 96 | templateJobsByBranch.keySet().each { templateBranchToProcess -> 97 | println "-> Checking $templateBranchToProcess branches" 98 | List branchesWithCorrespondingTemplate = allBranchNames.findAll { branchName -> 99 | branchName.startsWith(branchSuffixMatch[templateBranchToProcess]) 100 | } 101 | 102 | println "---> Founded corresponding branches: $branchesWithCorrespondingTemplate" 103 | branchesWithCorrespondingTemplate.each { branchToProcess -> 104 | println "-----> Processing branch: $branchToProcess" 105 | List expectedJobsPerBranch = templateJobsByBranch[templateBranchToProcess].collect { TemplateJob templateJob -> 106 | templateJob.concreteJobForBranch(jobPrefix, branchToProcess) 107 | } 108 | println "-------> Expected jobs:" 109 | expectedJobsPerBranch.each { println " $it" } 110 | List jobNamesPerBranch = jobNames.findAll { it.endsWith(branchToProcess) } 111 | println "-------> Job Names per branch:" 112 | jobNamesPerBranch.each { println " $it" } 113 | List missingJobsPerBranch = expectedJobsPerBranch.findAll { expectedJob -> 114 | !jobNamesPerBranch.any { it.contains(expectedJob.jobName) } 115 | } 116 | println "-------> Missing jobs:" 117 | missingJobsPerBranch.each { println " $it" } 118 | missingJobs.addAll(missingJobsPerBranch) 119 | } 120 | 121 | List deleteCandidates = jobNames.findAll { it.contains(branchSuffixMatch[templateBranchToProcess]) } 122 | List jobsToDeletePerBranch = deleteCandidates.findAll { candidate -> 123 | !branchesWithCorrespondingTemplate.any { candidate.endsWith(it) } 124 | } 125 | 126 | println "-----> Jobs to delete:" 127 | jobsToDeletePerBranch.each { println " $it" } 128 | jobsToDelete.addAll(jobsToDeletePerBranch) 129 | } 130 | println "\nSummary:\n---------------" 131 | if (missingJobs) { 132 | for (ConcreteJob missingJob in missingJobs) { 133 | println "Creating missing job: ${missingJob.jobName} from ${missingJob.templateJob.jobName}" 134 | jenkinsApi.cloneJobForBranch(jobPrefix, missingJob, createJobInView, gitUrl, scriptCommand) 135 | jenkinsApi.startJob(missingJob) 136 | } 137 | } 138 | 139 | if (!noDelete && jobsToDelete) { 140 | 141 | if (sonarApi) { 142 | println "Invoking sonar to delete deprecated jobs:\n\t${jobsToDelete.join('\n\t')}" 143 | 144 | jobsToDelete.each { String jobName -> 145 | sonarApi.delete(jenkinsApi.getJobConfig(jobName)) 146 | } 147 | } 148 | 149 | println "Deleting deprecated jobs:\n\t${jobsToDelete.join('\n\t')}" 150 | jobsToDelete.each { String jobName -> 151 | jenkinsApi.deleteJob(jobName) 152 | } 153 | 154 | } 155 | } 156 | 157 | JenkinsApi initJenkinsApi() { 158 | if (!jenkinsApi) { 159 | assert jenkinsUrl != null 160 | if (dryRun) { 161 | println "DRY RUN! Not executing any POST commands to Jenkins, only GET commands" 162 | this.jenkinsApi = new JenkinsApiReadOnly(jenkinsServerUrl: jenkinsUrl) 163 | } else { 164 | this.jenkinsApi = new JenkinsApi(jenkinsServerUrl: jenkinsUrl) 165 | } 166 | 167 | if (jenkinsUser || jenkinsPassword) this.jenkinsApi.addBasicAuth(jenkinsUser, jenkinsPassword) 168 | } 169 | 170 | return this.jenkinsApi 171 | } 172 | 173 | GitApi initGitApi() { 174 | if (!gitApi) { 175 | assert gitUrl != null 176 | this.gitApi = new GitApi(gitUrl: gitUrl) 177 | } 178 | 179 | return this.gitApi 180 | } 181 | 182 | SonarApi initSonarApi() { 183 | 184 | println("Sonar API - initializing") 185 | 186 | if (!sonarApi) { 187 | 188 | if (dryRun) { 189 | println "DRY RUN! Not executing any delete command to Sonar!" 190 | this.sonarApi = new SonarApiReadOnly() 191 | }else { 192 | this.sonarApi = new SonarApi() 193 | } 194 | println "Sonar API - initialized" 195 | 196 | sonarApi.setSonarServerUrl(sonarUrl) 197 | this.sonarApi.addBasicAuth(sonarUser, sonarPassword) 198 | } 199 | return this.sonarApi 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/main/groovy/com/neoteric/jenkins/Main.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | /* 4 | Bootstrap class that parses command line arguments, or system properties passed in by jenkins, and starts the jenkins-build-per-branch sync process 5 | */ 6 | 7 | class Main { 8 | public static final Map> opts = [ 9 | h : [longOpt: 'help', required: false, args: 0, argName: 'help', description: "Print usage information - gradle flag -Dhelp=true"], 10 | j : [longOpt: 'jenkins-url', required: true, args: 1, argName: 'jenkinsUrl', description: "Jenkins URL - gradle flag -DjenkinsUrl="], 11 | u : [longOpt: 'git-url', required: true, args: 1, argName: 'gitUrl', description: "Git Repository URL - gradle flag -DgitUrl="], 12 | p : [longOpt: 'job-prefix', required: true, args: 1, argName: 'jobPrefix', description: "Job Prefix, - gradle flag -DjobPrefix="], 13 | a : [longOpt: 'template-job-prefix', required: true, args: 1, argName: 'templateJobPrefix', description: "Template Job Prefix, - gradle flag -DtemplatejobPrefix="], 14 | d : [longOpt: 'dry-run', required: false, args: 0, argName: 'dryRun', description: "Dry run, don't actually modify, create, or delete any jobs, just print out what would happen - gradle flag: -DdryRun=true"], 15 | i : [longOpt: 'create-job-in-view', required: false, args: 1, argName: 'createJobInView', description: "Create new job in specified view. When using this suppress view creation as well (-DnoViews=true) - gradle flag -DcreateInView=nestedView/view"], 16 | s : [longOpt: 'script-command', required: false, args: 1, argName: 'scriptCommand', description: "Script command to execute on remote server via Publish over SSH plugin"], 17 | k : [longOpt: 'no-delete', required: false, args: 0, argName: 'noDelete', description: "Do not delete (keep) branches and views - gradle flag -DnoDelete=true"], 18 | usr: [longOpt: 'jenkins-user', required: false, args: 1, argName: 'jenkinsUser', description: "Jenkins username - gradle flag -DjenkinsUser="], 19 | pwd: [longOpt: 'jenkins-password', required: false, args: 1, argName: 'jenkinsPassword', description: "Jenkins password - gradle flag -DjenkinsPassword="], 20 | sp : [longOpt: 'sonar-password', required: false, args: 1, argName: 'sonarPassword', description: "Sonar password - gradle flag -DsonarPassword="], 21 | su : [longOpt: 'sonar-user', required: false, args: 1, argName: 'sonarUser', description: "Sonar username - gradle flag -DsonarUser="], 22 | surl : [longOpt: 'sonar-url', required: false, args: 1, argName: 'sonarUrl', description: "Sonar URL - gradle flag -DsonarUrl="] 23 | ] 24 | 25 | public static void main(String[] args) { 26 | Map argsMap = parseArgs(args) 27 | showConfiguration(argsMap) 28 | JenkinsJobManager manager = new JenkinsJobManager(argsMap) 29 | manager.syncWithRepo() 30 | } 31 | 32 | public static Map parseArgs(String[] args) { 33 | def cli = createCliBuilder() 34 | OptionAccessor commandLineOptions = cli.parse(args) 35 | 36 | // this is necessary as Gradle's command line parsing stinks, it only allows you to pass in system properties (or task properties which are basically the same thing) 37 | // we need to merge in those properties in case the script is being called from `gradle syncWithGit` and the user is giving us system properties 38 | Map argsMap = mergeSystemPropertyOptions(commandLineOptions) 39 | 40 | if (argsMap.help) { 41 | cli.usage() 42 | System.exit(0) 43 | } 44 | 45 | if (argsMap.printConfig) { 46 | showConfiguration(argsMap) 47 | System.exit(0) 48 | } 49 | 50 | def missingArgs = opts.findAll { shortOpt, optMap -> 51 | if (optMap.required) return !argsMap."${optMap.argName}" 52 | } 53 | 54 | if (missingArgs) { 55 | missingArgs.each { shortOpt, missingArg -> println "missing required argument: ${missingArg.argName}" } 56 | cli.usage() 57 | System.exit(1) 58 | } 59 | 60 | return argsMap 61 | } 62 | 63 | public static createCliBuilder() { 64 | def cli = new CliBuilder(usage: "jenkins-build-per-branch [options]", header: 'Options, if calling from `gradle syncWithGit`, you need to use a system property format -D=value, ex: (gradle -DgitUrl=git@github.com:yourname/yourrepo.git syncWithGit):') 65 | opts.each { String shortOpt, Map optMap -> 66 | if (optMap.args) { 67 | cli."$shortOpt"(longOpt: optMap.longOpt, args: optMap.args, argName: optMap.argName, optMap.description) 68 | } else { 69 | cli."$shortOpt"(longOpt: optMap.longOpt, optMap.description) 70 | } 71 | } 72 | return cli 73 | } 74 | 75 | public static showConfiguration(Map argsMap) { 76 | println "===============================================================" 77 | argsMap.each { k, v -> println " $k: ${formatValue(k, v)}" } 78 | println "===============================================================" 79 | } 80 | 81 | public static formatValue(String key, String value) { 82 | return (key == "jenkinsPassword") ? "********" : value 83 | } 84 | 85 | public static Map mergeSystemPropertyOptions(OptionAccessor commandLineOptions) { 86 | Map mergedArgs = [:] 87 | opts.each { String shortOpt, Map optMap -> 88 | if (optMap.argName) { 89 | mergedArgs[optMap.argName] = commandLineOptions."$shortOpt" ?: System.getProperty(optMap.argName) 90 | } 91 | } 92 | return mergedArgs.findAll { k, v -> v } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/groovy/com/neoteric/jenkins/SonarApi.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | import groovyx.net.http.RESTClient 4 | import org.apache.http.HttpRequest 5 | import org.apache.http.HttpRequestInterceptor 6 | import org.apache.http.client.HttpResponseException 7 | import org.apache.http.conn.HttpHostConnectException 8 | import org.apache.http.protocol.HttpContext 9 | 10 | class SonarApi { 11 | 12 | String sonarServerUrl 13 | RESTClient restClient 14 | HttpRequestInterceptor requestInterceptor 15 | 16 | public void setSonarServerUrl(String sonarServerUrl) { 17 | if (!sonarServerUrl.endsWith("/")) sonarServerUrl += "/" 18 | this.sonarServerUrl = sonarServerUrl 19 | this.restClient = new RESTClient(sonarServerUrl) 20 | this.restClient.handler.failure = { resp -> 21 | println "request failed with status ${resp.status}, response body was [${resp.entity.content.text}]" 22 | return null 23 | } 24 | 25 | println ("Sonar API - registered restClient with " + sonarServerUrl) 26 | } 27 | 28 | public void addBasicAuth(String sonarServerUser, String sonarServerPassword) { 29 | println "Sonar API - use basic authentication" 30 | 31 | this.requestInterceptor = new HttpRequestInterceptor() { 32 | void process(HttpRequest httpRequest, HttpContext httpContext) { 33 | def auth = sonarServerUser + ':' + sonarServerPassword 34 | httpRequest.addHeader('Authorization', 'Basic ' + auth.bytes.encodeBase64().toString()) 35 | } 36 | } 37 | 38 | this.restClient.client.addRequestInterceptor(this.requestInterceptor) 39 | } 40 | 41 | protected void delete(String entryConfig) { 42 | 43 | def root = new XmlParser().parseText(entryConfig) 44 | def branchName = root.publishers."hudson.plugins.sonar.SonarPublisher".branch.text() 45 | 46 | String groupId = root.rootModule.groupId.text() 47 | 48 | String artifactId = root.rootModule.artifactId.text() 49 | 50 | StringBuilder sonarProject = new StringBuilder("") 51 | sonarProject.append(groupId).append(":").append(artifactId).append(":").append(branchName); 52 | 53 | println "Sonar API - project to delete: " + sonarProject 54 | 55 | try { 56 | restClient.post(path: "/api/projects/delete", query: ['key' : sonarProject]) 57 | } catch (HttpResponseException e) { 58 | println "Sonar API - Error: $e.statusCode : $e.message" 59 | } 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/groovy/com/neoteric/jenkins/SonarApiReadOnly.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | 4 | class SonarApiReadOnly extends SonarApi{ 5 | 6 | @Override 7 | protected void delete(String entryConfig) { 8 | 9 | println "Sonar API - warning - DRY RUN - no changes will be applied to Sonar projects" 10 | 11 | def root = new XmlParser().parseText(entryConfig) 12 | def branchName = root.publishers."hudson.plugins.sonar.SonarPublisher".branch.text() 13 | 14 | String groupId = root.rootModule.groupId.text() 15 | 16 | String artifactId = root.rootModule.artifactId.text() 17 | 18 | StringBuilder sonarProject = new StringBuilder("") 19 | sonarProject.append(groupId).append(":").append(artifactId).append(":").append(branchName); 20 | 21 | println "Sonar API - project to delete: " + sonarProject 22 | println "Sonar API - warning - DRY RUN - skipped delete" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/groovy/com/neoteric/jenkins/TemplateJob.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | import groovy.transform.EqualsAndHashCode; 4 | import groovy.transform.ToString; 5 | 6 | @ToString 7 | @EqualsAndHashCode 8 | class TemplateJob { 9 | String jobName 10 | String baseJobName 11 | String templateBranchName 12 | 13 | String jobNameForBranch(String branchName) { 14 | // git branches often have a forward slash in them, but they make jenkins cranky, turn it into an underscore 15 | String safeBranchName = branchName.replaceAll('/', '_') 16 | return "$baseJobName-$safeBranchName" 17 | } 18 | 19 | ConcreteJob concreteJobForBranch(String jobPrefix, String branchName) { 20 | ConcreteJob concreteJob = new ConcreteJob(templateJob: this, branchName: branchName, jobName: jobPrefix + '-' + jobNameForBranch(branchName) ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/groovy/com/neoteric/jenkins/GitApiTests.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | import org.junit.Before; 4 | import org.junit.Test 5 | import static org.assertj.core.api.Assertions.assertThat 6 | 7 | import com.neoteric.jenkins.GitApi; 8 | 9 | class GitApiTests { 10 | 11 | final shouldFail = new GroovyTestCase().&shouldFail 12 | GitApi gitApi 13 | 14 | @Before 15 | void before() { 16 | gitApi = new GitApi(); 17 | } 18 | 19 | @Test 20 | public void shouldProcessGoodCommand() { 21 | gitApi.eachResultLine("echo foo bar baz") { String line -> 22 | assertThat(line).isEqualTo("foo bar baz") 23 | } 24 | } 25 | 26 | @Test 27 | public void shouldFailWhenProcessingBadCommand() { 28 | assert "Cannot run program \"mrmxyzptlk\": error=2, No such file or directory" == shouldFail { 29 | gitApi.eachResultLine("mrmxyzptlk") { String line -> 30 | fail("Should not have gotten here, this should throw an error as the command shouldn't exist") 31 | } 32 | } 33 | } 34 | 35 | @Test 36 | public void testBadCommandThrowsException() { 37 | assert "Error executing command: cat thisfiledoesntexist -> cat: thisfiledoesntexist: No such file or directory" == shouldFail { 38 | gitApi.eachResultLine("cat thisfiledoesntexist") { String line -> 39 | fail("Should not have gotten here, this should throw an error, the command exists, but it doesn't run successfully") 40 | } 41 | } 42 | } 43 | 44 | @Test 45 | public void testGetBranchNames() { 46 | String mockResult = """ 47 | 10b42258f451ebf2640d3c18850e0c22eecdad4\trefs/heads/ted/feature_branch 48 | b9c209a2bf1c159168bf6bc2dfa9540da7e8c4a26\trefs/heads/master 49 | abd856d2ae658ee5f14889b465f3adcaf65fb52b\trefs/heads/release_1.0rc1 50 | garbage line that should be ignored 51 | """.trim() 52 | 53 | GitApi gitApi = new GitApiMockedResult(mockResult: mockResult) 54 | List branchNames = gitApi.branchNames 55 | 56 | assert ["master", "release_1.0rc1", "ted/feature_branch"] == branchNames.sort() 57 | } 58 | } 59 | 60 | 61 | class GitApiMockedResult extends GitApi { 62 | String mockResult = "mock result" 63 | 64 | @Override 65 | void eachResultLine(String command, Closure closure) { 66 | mockResult.eachLine { String line -> 67 | closure(line) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/groovy/com/neoteric/jenkins/JenkinsApiReadOnlyTests.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | import org.junit.Test 4 | import org.apache.http.HttpStatus 5 | 6 | import com.neoteric.jenkins.JenkinsApi; 7 | import com.neoteric.jenkins.JenkinsApiReadOnly; 8 | 9 | class JenkinsApiReadOnlyTests { 10 | 11 | @Test 12 | public void testAllReadOnlyPostsReturnOK() { 13 | JenkinsApi api = new JenkinsApiReadOnly(jenkinsServerUrl: "http://localhost:9090/jenkins") 14 | assert api.post("http://foo.com") == HttpStatus.SC_OK 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/groovy/com/neoteric/jenkins/JenkinsApiTests.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | import static org.assertj.core.api.Assertions.assertThat 4 | 5 | import org.apache.http.conn.HttpHostConnectException 6 | import static org.joox.JOOX.*; 7 | import org.junit.Before; 8 | import org.junit.Test 9 | import org.w3c.dom.Document 10 | 11 | import groovy.mock.interceptor.MockFor 12 | 13 | import org.apache.http.client.HttpResponseException 14 | 15 | import com.neoteric.jenkins.ConcreteJob; 16 | import com.neoteric.jenkins.JenkinsApi; 17 | import com.neoteric.jenkins.TemplateJob; 18 | 19 | import groovyx.net.http.RESTClient 20 | import net.sf.json.JSON 21 | import net.sf.json.JSONObject 22 | 23 | class JenkinsApiTests { 24 | 25 | final shouldFail = new GroovyTestCase().&shouldFail 26 | 27 | @Test 28 | public void shouldThrowExceptionForInvalidUrl() { 29 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://some-invalid-hostname:9090/jenkins") 30 | assert shouldFail(UnknownHostException) { api.getJobNames("myproj") }.contains("some-invalid-hostname") 31 | } 32 | 33 | @Test 34 | public void shouldThrowHttpHostConnectExceptionWhenCantConnectToUrl() { 35 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:12345/jenkins") 36 | assert "Connection to http://localhost:12345 refused" == shouldFail(HttpHostConnectException) { 37 | api.getJobNames("myproj") 38 | } 39 | } 40 | 41 | @Test 42 | public void test404ThrowsException() { 43 | MockFor mockRESTClient = new MockFor(RESTClient) 44 | mockRESTClient.demand.get { Map args -> 45 | def ex = new HttpResponseException(404, "Not Found") 46 | ex.metaClass.getResponse = { -> [status: 404] } 47 | throw ex 48 | } 49 | 50 | mockRESTClient.use { 51 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:9090/goodHostAndPortBadUrl") 52 | assert "Unexpected failure with path http://localhost:9090/goodHostAndPortBadUrl/api/json, HTTP Status Code: 404, full map: [path:api/json]" == shouldFail() { 53 | api.getJobNames("myproj") 54 | } 55 | } 56 | } 57 | 58 | @Test 59 | public void testCreateInViewResolutor() { 60 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:9090/jenkins") 61 | assert api.resolveViewPath("abc/def") == "view/abc/view/def/" 62 | } 63 | 64 | @Test 65 | public void testGetJobNames_matchPrefix() { 66 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:9090/jenkins") 67 | 68 | Map json = [ 69 | jobs: [ 70 | [name: "myproj-FirstJob"], 71 | [name: "otherproj-SecondJob"] 72 | ] 73 | ] 74 | withJsonResponse(json) { 75 | List projectNames = api.getJobNames("myproj") 76 | assert projectNames == ["myproj-FirstJob"] 77 | } 78 | } 79 | 80 | @Test 81 | public void testGetJobNames_noPrefix() { 82 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:9090/jenkins") 83 | 84 | Map json = [ 85 | jobs: [ 86 | [name: "myproj-FirstJob"], 87 | [name: "otherproj-SecondJob"] 88 | ] 89 | ] 90 | withJsonResponse(json) { 91 | List projectNames = api.jobNames 92 | assert projectNames.sort() == [ 93 | "myproj-FirstJob", 94 | "otherproj-SecondJob" 95 | ] 96 | } 97 | } 98 | 99 | @Test 100 | public void shouldChangeConfigBranchName() { 101 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:9090/jenkins") 102 | def result = api.processConfig(CONFIG, "release-1.0.0", "newGitUrl", "n/a"); 103 | assertThat(result).contains("*/release-1.0.0") 104 | } 105 | 106 | @Test 107 | public void shouldChangeGitUrl() { 108 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:9090/jenkins") 109 | def result = api.processConfig(CONFIG, "release-1.0.0", "newGitUrl", "n/a"); 110 | assertThat(result).contains("newGitUrl") 111 | } 112 | 113 | @Test 114 | public void shouldChangeSonarBranchName() { 115 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:9090/jenkins") 116 | def result = api.processConfig(CONFIG, "release-1.0.0", "newGitUrl", "n/a"); 117 | } 118 | 119 | @Test 120 | public void shouldNotThrowExceptionWhenNoSonarConfig() { 121 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:9090/jenkins") 122 | def result = api.processConfig(CONFIG_NO_SONAR, "release-1.0.0", "newGitUrl", "n/a"); 123 | } 124 | 125 | @Test 126 | public void shouldChangeScriptName() { 127 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:9090/jenkins") 128 | 129 | def result = api.processConfig(CONFIG, "release-1.0.0", "newGitUrl", "nohup /opt/projects/jenkins-deployment/neo-tasks.sh > /opt/projects/jenkins-deployment/neo-tasks.log &"); 130 | assertThat(result).contains("nohup /opt/projects/jenkins-deployment/neo-tasks.sh > /opt/projects/jenkins-deployment/neo-tasks.log &") 131 | } 132 | 133 | @Test 134 | public void testShouldStartJob() { 135 | JenkinsApi api = new JenkinsApi(jenkinsServerUrl: "http://localhost:9090/jenkins") 136 | assert true == api.shouldStartJob(CONFIG) 137 | } 138 | 139 | public void withJsonResponse(Map toJson, Closure closure) { 140 | JSON json = toJson as JSONObject 141 | MockFor mockRESTClient = new MockFor(RESTClient) 142 | mockRESTClient.demand.get { Map args -> 143 | return [data: json] 144 | } 145 | 146 | mockRESTClient.use { closure() } 147 | } 148 | 149 | 150 | 151 | 152 | static final String CONFIG = ''' 153 | 154 | 155 | 156 | false 157 | 158 | 159 | 160 | 161 | startOnCreate 162 | 163 | true 164 | 165 | 166 | abc 167 | 168 | false 169 | 170 | 171 | abc 172 | 173 | xyz 174 | 175 | 176 | 177 | 178 | 179 | 2 180 | 181 | 182 | git@githost.com/repo.git 183 | 469a31b3-b5e5-45e0-b9c6-9cc3ef61203e 184 | 185 | 186 | 187 | 188 | */whatever 189 | 190 | 191 | false 192 | 193 | 194 | 7.0 195 | 196 | 197 | 198 | 199 | true 200 | false 201 | false 202 | false 203 | 204 | false 205 | 206 | com.neoteric 207 | artifactId 208 | 209 | clean install 210 | true 211 | false 212 | true 213 | false 214 | false 215 | false 216 | false 217 | false 218 | -1 219 | false 220 | false 221 | true 222 | 223 | 224 | 225 | 226 | 227 | (Inherit From Job) 228 | toBeChanged 229 | 230 | 231 | -Dsonar.java.source=1.7 232 | 233 | 234 | false 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | SSH: 243 | 244 | 245 | 246 | ntrc-delta 247 | false 248 | 249 | 250 | 251 | 252 | 253 | 254 | false 255 | false 256 | false 257 | false 258 | false 259 | [, ]+ 260 | toBeChanged 261 | 120000 262 | false 263 | 264 | 265 | false 266 | false 267 | 268 | 269 | mordor 270 | false 271 | 272 | 273 | 274 | 275 | 276 | 277 | false 278 | false 279 | false 280 | false 281 | false 282 | [, ]+ 283 | toBeChanged 284 | 120000 285 | false 286 | 287 | 288 | false 289 | false 290 | 291 | 292 | false 293 | false 294 | false 295 | 296 | 297 | 298 | 299 | 300 | 301 | FAILURE 302 | 2 303 | RED 304 | true 305 | 306 | ''' 307 | 308 | static final String CONFIG_NO_SONAR = ''' 309 | 310 | 311 | 312 | false 313 | 314 | 315 | 316 | 317 | startOnCreate 318 | 319 | true 320 | 321 | 322 | abc 323 | 324 | false 325 | 326 | 327 | abc 328 | 329 | xyz 330 | 331 | 332 | 333 | 334 | 335 | 2 336 | 337 | 338 | git@githost.com/repo.git 339 | 469a31b3-b5e5-45e0-b9c6-9cc3ef61203e 340 | 341 | 342 | 343 | 344 | */whatever 345 | 346 | 347 | false 348 | 349 | 350 | 7.0 351 | 352 | 353 | 354 | 355 | true 356 | false 357 | false 358 | false 359 | 360 | false 361 | 362 | com.neoteric 363 | artifactId 364 | 365 | clean install 366 | true 367 | false 368 | true 369 | false 370 | false 371 | false 372 | false 373 | false 374 | -1 375 | false 376 | false 377 | true 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | FAILURE 387 | 2 388 | RED 389 | true 390 | 391 | ''' 392 | } 393 | 394 | -------------------------------------------------------------------------------- /src/test/groovy/com/neoteric/jenkins/JenkinsJobManagerTests.groovy: -------------------------------------------------------------------------------- 1 | package com.neoteric.jenkins 2 | 3 | import static org.junit.Assert.*; 4 | import static org.assertj.core.api.Assertions.assertThat 5 | import groovy.mock.interceptor.MockFor; 6 | 7 | import org.junit.Before; 8 | import org.junit.Rule; 9 | import org.junit.Test 10 | import org.junit.contrib.java.lang.system.StandardOutputStreamLog 11 | 12 | import com.neoteric.jenkins.JenkinsJobManager; 13 | import com.neoteric.jenkins.TemplateJob; 14 | 15 | class JenkinsJobManagerTests { 16 | 17 | @Rule 18 | public final StandardOutputStreamLog log = new StandardOutputStreamLog(); 19 | 20 | final shouldFail = new GroovyTestCase().&shouldFail 21 | 22 | @Test 23 | public void testFindTemplateJobs() { 24 | JenkinsJobManager jenkinsJobManager = 25 | new JenkinsJobManager(templateJobPrefix: "template", jobPrefix: "myproj", jenkinsUrl: "http://dummy.com", gitUrl: "git@dummy.com:company/myproj.git") 26 | 27 | List allJobNames = [ 28 | "myproj-foo-master", 29 | "otherproj-foo-master", 30 | "template-foo-feature" 31 | ] 32 | List templateJobs = jenkinsJobManager.findRequiredTemplateJobs(allJobNames) 33 | assert templateJobs.size() == 1 34 | TemplateJob templateJob = templateJobs.first() 35 | assert templateJob.jobName == "template-foo-feature" 36 | assert templateJob.baseJobName == "foo" 37 | assert templateJob.templateBranchName == "feature" 38 | } 39 | 40 | 41 | @Test 42 | public void testFindTemplateJobs_noMatchingJobsThrowsException() { 43 | JenkinsJobManager jenkinsJobManager = new JenkinsJobManager(templateJobPrefix: "template", jobPrefix: "myproj", jenkinsUrl: "http://dummy.com", gitUrl: "git@dummy.com:company/myproj.git") 44 | List allJobNames = [ 45 | "otherproj-foo-master", 46 | "myproj-foo-featurebranch" 47 | ] 48 | String result = shouldFail(AssertionError) { jenkinsJobManager.findRequiredTemplateJobs(allJobNames) } 49 | 50 | assertThat(result).contains("Unable to find any jobs matching template regex") 51 | } 52 | 53 | 54 | @Test 55 | public void testGetTemplateJobs() { 56 | JenkinsJobManager jenkinsJobManager = new JenkinsJobManager(jobPrefix: "NeoDocs", templateJobPrefix: "NeoDocsTemplates", gitUrl: "git@dummy.com:company/myproj.git", jenkinsUrl: "http://dummy.com") 57 | 58 | List allJobNames = [ 59 | "NeoDocs-build-feature", 60 | "NeoDocsTemplates-build-feature", 61 | "NeoDocsTemplates-build-featured", 62 | "NeoDocsTemplates-deploy-feature", 63 | "NeoDocsTemplates-build-hotfix" 64 | ] 65 | List templateJobs = [ 66 | new TemplateJob(jobName: "NeoDocsTemplates-build-feature", baseJobName: "build", templateBranchName: "feature"), 67 | new TemplateJob(jobName: "NeoDocsTemplates-deploy-feature", baseJobName: "deploy", templateBranchName: "feature"), 68 | new TemplateJob(jobName: "NeoDocsTemplates-build-hotfix", baseJobName: "build", templateBranchName: "hotfix") 69 | ] 70 | 71 | assert templateJobs == jenkinsJobManager.findRequiredTemplateJobs(allJobNames) 72 | } 73 | 74 | @Test 75 | public void testSync() { 76 | 77 | List templateJobs = [ 78 | new TemplateJob(jobName: "NeoDocsTemplates-build-feature", baseJobName: "build", templateBranchName: "feature"), 79 | new TemplateJob(jobName: "NeoDocsTemplates-deploy-feature", baseJobName: "deploy", templateBranchName: "feature"), 80 | new TemplateJob(jobName: "NeoDocsTemplates-build-hotfix", baseJobName: "build", templateBranchName: "hotfix") 81 | ] 82 | 83 | List jobNames = [ 84 | "NeoDocs-build-feature-test1", 85 | // add missing deploy test1 86 | "NeoDocs-deploy-feature-test2", 87 | // add missing build test2 88 | "NeoDocs-deploy-feature-test3", 89 | // to delete 90 | "NeoDocs-build-hotfix-emergency", 91 | // do nothing - already there 92 | "NeoDocs-build-release" // do nothing - no template avail 93 | ] 94 | 95 | List branchNames = [ 96 | "feature-test1", 97 | "feature-test2", 98 | "master", 99 | "release-1.0.0", 100 | "hotfix-emergency" 101 | ] 102 | JenkinsJobManager jenkinsJobManager = new JenkinsJobManager(jobPrefix: "NeoDocs", templateJobPrefix: "NeoDocsTemplates", gitUrl: "git@dummy.com:company/myproj.git", jenkinsUrl: "http://dummy.com") 103 | 104 | jenkinsJobManager.jenkinsApi = new JenkinsApiMocked() 105 | jenkinsJobManager.syncJobs(branchNames ,jobNames, templateJobs) 106 | 107 | assertThat(log.getLog().substring(log.getLog().indexOf("Summary"))).containsSequence( 108 | "Creating", "NeoDocs-deploy-feature-test1 from NeoDocsTemplates-deploy-feature", 109 | "NeoDocs-build-feature-test2 from NeoDocsTemplates-build-feature", 110 | "Deleting", "NeoDocs-deploy-feature-test3") 111 | } 112 | 113 | class JenkinsApiMocked extends JenkinsApi { 114 | 115 | @Override 116 | public void cloneJobForBranch(String jobPrefix, ConcreteJob missingJob, String createJobInView, String gitUrl, String scriptCommand) { 117 | } 118 | 119 | @Override 120 | public void deleteJob(String jobName) { 121 | } 122 | 123 | @Override 124 | public void startJob(ConcreteJob job) { 125 | } 126 | } 127 | 128 | } 129 | --------------------------------------------------------------------------------