diff --git a/CHANGELOG.md b/CHANGELOG.md index b19dcc2..3030b51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ _Release Date: 2020-06-01_ ### Added -* Solution for Jenkins extensions that interact with the `Jenkins` singelton at classload- or Descriptor-instantiation-time: +* Solution for Jenkins extensions that interact with the `Jenkins` singleton at classload- or Descriptor-instantiation-time: * jenkins-spock automatically injects a mock Jenkins singleton _before_ any Jenkins extensions are classloaded or instantiated, so that `Jenkins.getInstanceOrNull()` is not `null`. * `makeStaticJenkins()` method to allow test suites to provide their own pre-test-suite Jenkins singleton if necessary, such as if the spec needs to stub pre-test-suite interaction with `Jenkins` * Please see the "Mock Jenkins" section of the `JenkinsPipelineSpecification` GroovyDoc. diff --git a/examples/helper-script-gradle/.gitignore b/examples/helper-script-gradle/.gitignore new file mode 100644 index 0000000..3223f6e --- /dev/null +++ b/examples/helper-script-gradle/.gitignore @@ -0,0 +1,8 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +# Ignore IntelliJ IDEA project files +/.idea/ diff --git a/examples/helper-script-gradle/.keep b/examples/helper-script-gradle/.keep new file mode 100644 index 0000000..e69de29 diff --git a/examples/helper-script-gradle/build.gradle b/examples/helper-script-gradle/build.gradle new file mode 100644 index 0000000..e3a3456 --- /dev/null +++ b/examples/helper-script-gradle/build.gradle @@ -0,0 +1,58 @@ +plugins { + // Apply the groovy plugin to add support for Groovy + id 'groovy' + // Apply Gradle JPI plugin to simplify dependency management + id 'org.jenkins-ci.jpi' version '0.43.0' +} + +group 'com.example' +version 'O.1-SNAPSHOT' + +jenkinsPlugin { + // verions of Jenkins core jpi plugin depends on + jenkinsVersion = '2.355' + // optional list of package prefixes to mask + maskClasses = 'jaxen' +} + +allprojects { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 +} + +sourceSets { + main { + groovy { + srcDirs = ['src', 'vars'] + } + } + test { + groovy { + srcDirs = ['test'] + } + } +} + +repositories { + mavenCentral() + mavenLocal() + maven { url 'https://repo.jenkins-ci.org/releases/' } +} + +dependencies { + implementation 'org.codehaus.groovy:groovy-all:2.5.17' + + testImplementation 'com.homeaway.devtools.jenkins:jenkins-spock:2.1.5' + testImplementation 'ch.qos.logback:logback-core:1.2.11' + testImplementation 'ch.qos.logback:logback-classic:1.2.11' + testImplementation 'javax.servlet:javax.servlet-api:4.0.1' + testImplementation 'org.jenkins-ci.main:jenkins-core:2.355' + testImplementation 'org.jenkins-ci.plugins.workflow:workflow-cps:2725.v7b_c717eb_12ce' + // provides isUnix() step + testImplementation 'org.jenkins-ci.plugins.workflow:workflow-basic-steps:948.v2c72a_091b_b_68' + // provides sh() step + testImplementation 'org.jenkins-ci.plugins.workflow:workflow-durable-task-step:1146.v1a_d2e603f929' + // provides sshagent() step + testImplementation 'org.jenkins-ci.plugins:ssh-agent:295.v9ca_a_1c7cc3a_a_' +} + diff --git a/examples/helper-script-gradle/gradle/wrapper/gradle-wrapper.jar b/examples/helper-script-gradle/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/examples/helper-script-gradle/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/helper-script-gradle/gradle/wrapper/gradle-wrapper.properties b/examples/helper-script-gradle/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8fad3f5 --- /dev/null +++ b/examples/helper-script-gradle/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/helper-script-gradle/gradlew b/examples/helper-script-gradle/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/examples/helper-script-gradle/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/helper-script-gradle/gradlew.bat b/examples/helper-script-gradle/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/examples/helper-script-gradle/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/helper-script-gradle/src/.keep b/examples/helper-script-gradle/src/.keep new file mode 100644 index 0000000..e69de29 diff --git a/examples/helper-script-gradle/test/DeployerSpec.groovy b/examples/helper-script-gradle/test/DeployerSpec.groovy new file mode 100644 index 0000000..63395a9 --- /dev/null +++ b/examples/helper-script-gradle/test/DeployerSpec.groovy @@ -0,0 +1,27 @@ +import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification + +public class DeployerSpec extends JenkinsPipelineSpecification { + + def Deployer = null + + def setup() { + script_class_path = ["vars", "build/classes/groovy/main"] + Deployer = loadPipelineScriptForTest("/Deployer.groovy") + } + + def "deploy function deploys to TEST when asked" () { + when: + Deployer.deploy( "test" ) + then: + 1 * getPipelineMock("sshagent")(["test-ssh"], _ as Closure) + 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-test .*/}) + } + + def "deploy function deploys to PRODUCTION when asked" () { + when: + Deployer.deploy( "production" ) + then: + 1 * getPipelineMock("sshagent")(["prod-ssh"], _ as Closure) + 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-prod .*/}) + } +} \ No newline at end of file diff --git a/examples/helper-script-gradle/test/ExecTest.groovy b/examples/helper-script-gradle/test/ExecTest.groovy new file mode 100644 index 0000000..c1e266c --- /dev/null +++ b/examples/helper-script-gradle/test/ExecTest.groovy @@ -0,0 +1,86 @@ +import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification + +// Ideally should be named ExecSpec.groovy +// if I name it ExecSpec, I get the following stack trace +// +//java.lang.NoClassDefFoundError: Exec (wrong name: exec) +// +//at java.lang.ClassLoader.defineClass(ClassLoader.java:756) +//at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) +//at java.net.URLClassLoader.defineClass(URLClassLoader.java:468) +//at java.net.URLClassLoader.access$100(URLClassLoader.java:74) +//at java.net.URLClassLoader$1.run(URLClassLoader.java:369) +//at java.net.URLClassLoader$1.run(URLClassLoader.java:363) +//at java.net.URLClassLoader.findClass(URLClassLoader.java:362) +//at java.lang.ClassLoader.loadClass(ClassLoader.java:418) +//at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355) +//at java.lang.ClassLoader.loadClass(ClassLoader.java:351) +//at java.lang.Class.forName(Class.java:264) +//at com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification.setup(JenkinsPipelineSpecification.groovy:1091) +class ExecTest extends JenkinsPipelineSpecification { + + def Exec + + def setup() { + script_class_path = ["vars", "build/classes/groovy/main"] + Exec = loadPipelineScriptForTest('/exec.groovy') + } + + def "Sanity-Check isUnix"() { + expect: + isUnix() == null + } + + def "Sanity-Check mocking isUnix"() { + setup: + getPipelineMock('isUnix')() >> { return true } + expect: + isUnix() != null + } + + def "Sanity-Check expecting isUnix"() { + when: + Exec('ls') + then: + _ * getPipelineMock('isUnix')() >> { return true } + 1 * getPipelineMock('sh') ('ls') + } + + def "(broken) Test on Windows"() { + setup: + getPipelineMock('isUnix')() >> { return false } + + when: + Exec('ls') + then: + 1 * getPipelineMock('isUnix') () + 1 * getPipelineMock('bat') ('ls') + } + + def "(broken) Test on Linux"() { + setup: + getPipelineMock('isUnix')() >> { return true } + + when: + Exec('ls') + then: + 1 * getPipelineMock('isUnix') () + 0 * getPipelineMock('sh') ('ls') + } + + def "Test on Windows"() { + when: + Exec('ls') + then: + 1 * getPipelineMock('isUnix') () >> { return false } + 1 * getPipelineMock('bat') ('ls') + } + + def "Test on Linux"() { + when: + Exec('ls') + then: + 1 * getPipelineMock('isUnix') () >> { return true } + 1 * getPipelineMock('sh') ('ls') + } +} \ No newline at end of file diff --git a/examples/helper-script-gradle/vars/Deployer.groovy b/examples/helper-script-gradle/vars/Deployer.groovy new file mode 100644 index 0000000..0b744e3 --- /dev/null +++ b/examples/helper-script-gradle/vars/Deployer.groovy @@ -0,0 +1,20 @@ +def deploy( _env ) { + + def DEPLOY_COMMAND=""" + docker-compose pull && \ + docker-compose down && \ + docker-compose rm -f && \ + docker-compose up -d --force-recreate""" + + if( _env == "test" ) { + sshagent(["test-ssh"]) { + sh( "ssh deployer@app-test -c '${DEPLOY_COMMAND}'" ) + } + } else if( _env == "production" ) { + sshagent(["prod-ssh"]) { + sh( "ssh deployer@app-prod -c '${DEPLOY_COMMAND}'" ) + } + } +} + +return this diff --git a/examples/helper-script-gradle/vars/exec.groovy b/examples/helper-script-gradle/vars/exec.groovy new file mode 100644 index 0000000..3468401 --- /dev/null +++ b/examples/helper-script-gradle/vars/exec.groovy @@ -0,0 +1,7 @@ +void call(String command) { + if (isUnix()) { + sh command + } else{ + bat command + } +} \ No newline at end of file diff --git a/examples/shared-library-gradle/.gitignore b/examples/shared-library-gradle/.gitignore new file mode 100644 index 0000000..3223f6e --- /dev/null +++ b/examples/shared-library-gradle/.gitignore @@ -0,0 +1,8 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +# Ignore IntelliJ IDEA project files +/.idea/ diff --git a/examples/shared-library-gradle/Makefile b/examples/shared-library-gradle/Makefile new file mode 100644 index 0000000..5016f26 --- /dev/null +++ b/examples/shared-library-gradle/Makefile @@ -0,0 +1,3 @@ +.PHONY: test +test: + ./gradlew clean build diff --git a/examples/shared-library-gradle/README.md b/examples/shared-library-gradle/README.md new file mode 100644 index 0000000..d81d351 --- /dev/null +++ b/examples/shared-library-gradle/README.md @@ -0,0 +1,49 @@ +Pipeline Shared Library +============================== + +This project is a Jenkins Pipeline Shared Library that could be loaded by the [Pipeline Shared Groovy Libraries Plugin](https://plugins.jenkins.io/workflow-cps-global-lib). + +It delivers two new pipeline steps: + +`DefaultPipeline` +------------------------- + +A simple Jenkins pipeline that + +1. Builds & Tests all branches +2. Notifies Slack if tests fail +3. Deploys the "master" branch to the Production environment +4. Deploys other branches to the "Test" environment. + +Usage: + +`Jenkinsfile`: + +```groovy +@Library("shared-library") _ +DefaultPipeline() +``` + +`Deployer` +------------------------- + +A step that SSHs to a machine in an environment, and bounces an application using `docker-compose`. + +Usage: + +``` +Deployer("test") // deploy the latest version of the application to the TEST environment +``` + +Testing +============================== + +To test the library, run `./gradlew clean build --info`. + +Requirements +============================== + +The following tools should be installed in order to work with this project: + +1. `make` +2. `java` 1.8+ diff --git a/examples/shared-library-gradle/build.gradle b/examples/shared-library-gradle/build.gradle new file mode 100644 index 0000000..0c70d49 --- /dev/null +++ b/examples/shared-library-gradle/build.gradle @@ -0,0 +1,87 @@ +plugins { + // Apply the groovy plugin to add support for Groovy + id 'groovy' + // Apply Gradle JPI plugin to simplify dependency management + id 'org.jenkins-ci.jpi' version '0.43.0' +} + +def testLogLevel = project.findProperty('testLogLevel') ?: 'ERROR' + +group 'com.example' +version 'O.1-SNAPSHOT' + +jenkinsPlugin { + // verions of Jenkins core jpi plugin depends on + jenkinsVersion = '2.355' + // optional list of package prefixes to mask + maskClasses = 'jaxen' +} + +allprojects { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 +} + +sourceSets { + main { + groovy { + srcDirs = ['src', 'vars'] + } + } + test { + groovy { + srcDirs = ['test'] + } + resources { + srcDirs = ['test/resources'] + } + } +} + +repositories { + mavenCentral() + mavenLocal() + maven { url 'https://repo.jenkins-ci.org/releases/' } +} + +dependencies { + implementation 'org.codehaus.groovy:groovy-all:2.5.17' + + testImplementation 'com.homeaway.devtools.jenkins:jenkins-spock:2.1.5' + testImplementation 'ch.qos.logback:logback-core:1.2.11' + testImplementation 'ch.qos.logback:logback-classic:1.2.11' + testImplementation 'javax.servlet:javax.servlet-api:3.1.0' + testImplementation 'org.jenkins-ci.main:jenkins-core:2.355' + // provides stage() step + testImplementation 'org.jenkins-ci.plugins:pipeline-stage-step:293.v200037eefcd5' + // provides isUnix() step + testImplementation 'org.jenkins-ci.plugins.workflow:workflow-basic-steps:948.v2c72a_091b_b_68' + // provides sh() step + testImplementation 'org.jenkins-ci.plugins.workflow:workflow-durable-task-step:1146.v1a_d2e603f929' + // provides libraryResource() step + testImplementation 'org.jenkins-ci.plugins.workflow:workflow-cps-global-lib:588.v576c103a_ff86' + // provides slackSend() step + testImplementation 'org.jenkins-ci.plugins:slack:608.v19e3b_44b_b_9ff' + // provides sshagent() step + testImplementation 'org.jenkins-ci.plugins:ssh-agent:295.v9ca_a_1c7cc3a_a_' +} + +task cleanSharedLibraries(type: Delete) { + delete "$buildDir/classes/groovy/test/vars" +} + +task prepareSharedLibraries(type: Copy) { + dependsOn tasks.cleanSharedLibraries + from 'vars' + include '*' + into "$buildDir/classes/groovy/test/vars" +} + +test { + dependsOn tasks.prepareSharedLibraries + systemProperties = [ + 'root.loglevel': testLogLevel, + 'root.appender': 'Stdout', + 'logdir': project.buildDir + ] +} diff --git a/examples/shared-library-gradle/gradle/wrapper/gradle-wrapper.jar b/examples/shared-library-gradle/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/examples/shared-library-gradle/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/shared-library-gradle/gradle/wrapper/gradle-wrapper.properties b/examples/shared-library-gradle/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8fad3f5 --- /dev/null +++ b/examples/shared-library-gradle/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/shared-library-gradle/gradlew b/examples/shared-library-gradle/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/examples/shared-library-gradle/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/shared-library-gradle/gradlew.bat b/examples/shared-library-gradle/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/examples/shared-library-gradle/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/shared-library-gradle/settings.gradle b/examples/shared-library-gradle/settings.gradle new file mode 100644 index 0000000..2f43853 --- /dev/null +++ b/examples/shared-library-gradle/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'jenkinsfile-test-shared-library' \ No newline at end of file diff --git a/examples/shared-library-gradle/src/com/example/SharedLibraryConstants.groovy b/examples/shared-library-gradle/src/com/example/SharedLibraryConstants.groovy new file mode 100644 index 0000000..1de832f --- /dev/null +++ b/examples/shared-library-gradle/src/com/example/SharedLibraryConstants.groovy @@ -0,0 +1,9 @@ +package com.example + +public class SharedLibraryConstants { + public static final String DEPLOY_COMMAND = """ + docker-compose pull && \ + docker-compose down && \ + docker-compose rm -f && \ + docker-compose up -d --force-recreate""" +} diff --git a/examples/shared-library-gradle/test/DefaultPipelineSpec.groovy b/examples/shared-library-gradle/test/DefaultPipelineSpec.groovy new file mode 100644 index 0000000..84ace0e --- /dev/null +++ b/examples/shared-library-gradle/test/DefaultPipelineSpec.groovy @@ -0,0 +1,51 @@ +import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification + +public class DefaultPipelineSpec extends JenkinsPipelineSpecification { + + def DefaultPipeline = null + + public static class DummyException extends RuntimeException { + public DummyException(String _message) { super( _message ); } + } + + def setup() { + script_class_path = ["vars"] + DefaultPipeline = loadPipelineScriptForTest("/DefaultPipeline.groovy") + DefaultPipeline.getBinding().setVariable( "scm", null ) + getPipelineMock("libraryResource")(_) >> { + return "Dummy Message" + } + } + + def "Slack is notified when tests fail" () { + setup: + getPipelineMock("sh")("docker run --entrypoint python whole-pipeline -m unittest discover") >> { + throw new DummyException("Dummy test failure") + } + when: + try { + DefaultPipeline() + } catch( DummyException e ) {} + then: + 1 * getPipelineMock("slackSend")( _ as Map ) + } + + def "Attempts to deploy MASTER branch to PRODUCTION" () { + setup: + DefaultPipeline.getBinding().setVariable( "BRANCH_NAME", "master" ) + when: + DefaultPipeline() + then: + 1 * getPipelineMock("Deployer.call")("production") + } + + def "Does NOT attempt to deploy non-MASTER branch PRODUCTION" () { + setup: + DefaultPipeline.getBinding().setVariable( "BRANCH_NAME", "develop" ) + when: + DefaultPipeline() + then: + 0 * getPipelineMock("Deployer.call")("production") + } +} + diff --git a/examples/shared-library-gradle/test/DeployerSpec.groovy b/examples/shared-library-gradle/test/DeployerSpec.groovy new file mode 100644 index 0000000..7d420de --- /dev/null +++ b/examples/shared-library-gradle/test/DeployerSpec.groovy @@ -0,0 +1,27 @@ +import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification + +public class DeployerSpec extends JenkinsPipelineSpecification { + + def Deployer = null + + def setup() { + script_class_path = ["vars"] + Deployer = loadPipelineScriptForTest("/Deployer.groovy") + } + + def "deploy function deploys to TEST when asked" () { + when: + Deployer( "test" ) + then: + 1 * getPipelineMock("sshagent")(["test-ssh"], _ as Closure) + 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-test .*/}) + } + + def "deploy function deploys to PRODUCTION when asked" () { + when: + Deployer( "production" ) + then: + 1 * getPipelineMock("sshagent")(["prod-ssh"], _ as Closure) + 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-prod .*/}) + } +} \ No newline at end of file diff --git a/examples/shared-library-gradle/test/resources/logback-test.xml b/examples/shared-library-gradle/test/resources/logback-test.xml new file mode 100644 index 0000000..a4fe093 --- /dev/null +++ b/examples/shared-library-gradle/test/resources/logback-test.xml @@ -0,0 +1,41 @@ + + + + + + + + + + [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n + + + + + + ${logdir}/stdout.log + + ${logdir}/stdout.log.%i + + 1 + 10 + + + 20MB + + + [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n + + + + + + + + + + \ No newline at end of file diff --git a/examples/shared-library-gradle/vars/DefaultPipeline.groovy b/examples/shared-library-gradle/vars/DefaultPipeline.groovy new file mode 100644 index 0000000..debcc21 --- /dev/null +++ b/examples/shared-library-gradle/vars/DefaultPipeline.groovy @@ -0,0 +1,40 @@ +def call( Map _args ) { + + node { + stage( "Checkout" ) { + checkout scm + } + + stage( "Build" ) { + sh( "docker build --tag whole-pipeline ." ) + } + + stage( "Test" ) { + try { + sh( "docker run --entrypoint python whole-pipeline -m unittest discover" ) + } catch( Exception e ) { + + def message = evaluate( '"""' + libraryResource( "com/example/SlackMessageTemplate.txt" ) + '"""' ) + + slackSend( + color: 'error', + message: message ) + throw e + } + } + + stage( "Push" ) { + sh( "docker push whole-pipeline" ) + } + + stage( "Deploy to TEST" ) { + Deployer( "test" ) + } + + if( BRANCH_NAME == "master" ) { + stage( "Deploy to PRODUCTION" ) { + Deployer( "production" ) + } + } + } +} diff --git a/examples/shared-library-gradle/vars/Deployer.groovy b/examples/shared-library-gradle/vars/Deployer.groovy new file mode 100644 index 0000000..1e594fd --- /dev/null +++ b/examples/shared-library-gradle/vars/Deployer.groovy @@ -0,0 +1,14 @@ +import com.example.SharedLibraryConstants + +def call( _env ) { + + if( _env == "test" ) { + sshagent(["test-ssh"]) { + sh( "ssh deployer@app-test -c '${SharedLibraryConstants.DEPLOY_COMMAND}'" ) + } + } else if( _env == "production" ) { + sshagent(["prod-ssh"]) { + sh( "ssh deployer@app-prod -c '${SharedLibraryConstants.DEPLOY_COMMAND}'" ) + } + } +} diff --git a/examples/whole-pipeline-gradle/.gitignore b/examples/whole-pipeline-gradle/.gitignore new file mode 100644 index 0000000..3223f6e --- /dev/null +++ b/examples/whole-pipeline-gradle/.gitignore @@ -0,0 +1,8 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +# Ignore IntelliJ IDEA project files +/.idea/ diff --git a/examples/whole-pipeline-gradle/Dockerfile b/examples/whole-pipeline-gradle/Dockerfile new file mode 100644 index 0000000..d7d6dc1 --- /dev/null +++ b/examples/whole-pipeline-gradle/Dockerfile @@ -0,0 +1,13 @@ +FROM jfloff/alpine-python:3.6 + +RUN pip install --upgrade \ + Flask \ + pip + +COPY app /app + +WORKDIR app +ENV FLASK_APP=hello.py +EXPOSE 5000 + +ENTRYPOINT ["flask", "run", "--host", "0.0.0.0"] diff --git a/examples/whole-pipeline-gradle/Jenkinsfile b/examples/whole-pipeline-gradle/Jenkinsfile new file mode 100644 index 0000000..a6c6137 --- /dev/null +++ b/examples/whole-pipeline-gradle/Jenkinsfile @@ -0,0 +1,49 @@ +def deploy( _env ) { + + def DEPLOY_COMMAND="docker-compose pull && docker-compose down && docker-compose rm -f && docker-compose up -d --force-recreate" + + if( _env == "test" ) { + sshagent(["test-ssh"]) { + sh( "ssh deployer@app-test -c '${DEPLOY_COMMAND}'" ) + } + } else if( _env == "production" ) { + sshagent(["prod-ssh"]) { + sh( "ssh deployer@app-prod -c '${DEPLOY_COMMAND}'" ) + } + } +} + +node { + stage( "Checkout" ) { + checkout scm + } + + stage( "Build" ) { + sh( "docker build --tag whole-pipeline ." ) + } + + stage( "Test" ) { + try { + sh( "docker run --entrypoint python whole-pipeline -m unittest discover" ) + } catch( Exception e ) { + slackSend( + color: 'error', + message: 'whole-pipeline unit tests failed.' ) + throw e + } + } + + stage( "Push" ) { + sh( "docker push whole-pipeline" ) + } + + stage( "Deploy to TEST" ) { + deploy( "test" ) + } + + if( BRANCH_NAME == "master" ) { + stage( "Deploy to PRODUCTION" ) { + deploy( "production" ) + } + } +} diff --git a/examples/whole-pipeline-gradle/Makefile b/examples/whole-pipeline-gradle/Makefile new file mode 100644 index 0000000..145091e --- /dev/null +++ b/examples/whole-pipeline-gradle/Makefile @@ -0,0 +1,20 @@ +.PHONY: all +all: + $(MAKE) test + $(MAKE) test-pipeline + +.PHONY: build +build: + docker build -t helper-script . + +.PHONY: run +run: build + docker run --rm -it --publish 5000:5000 helper-script + +.PHONY: test +test: build + docker run --entrypoint python helper-script -m unittest discover + +.PHONY: test-pipeline +test-pipeline: + ./gradlew clean build diff --git a/examples/whole-pipeline-gradle/README.md b/examples/whole-pipeline-gradle/README.md new file mode 100644 index 0000000..1f8d31c --- /dev/null +++ b/examples/whole-pipeline-gradle/README.md @@ -0,0 +1,37 @@ +Jenkinsfile +============================== + +This project is a containerized Python web application with a Jenkins pipeline that + +1. Builds & Tests all branches +2. Notifies Slack if tests fail +3. Deploys the "master" branch to the Production environment +4. Deploys other branches to the "Test" environment. + +Building +============================== + +To build the web application, run `make build`. + +Running +============================== + +To run the application, run `make run`. + +To see the running application, visit http://localhost:5000 in a web browser. + +Testing +============================== + +To test the _application_, run `make test`. + +To test the _pipeline_, run `make test-pipeline`. + +Requirements +============================== + +The following tools should be installed in order to work with this project: + +1. `docker` +2. `make` +3. `java` 1.8+ diff --git a/examples/whole-pipeline-gradle/app/counter.py b/examples/whole-pipeline-gradle/app/counter.py new file mode 100644 index 0000000..56097e9 --- /dev/null +++ b/examples/whole-pipeline-gradle/app/counter.py @@ -0,0 +1,2 @@ +def plusone(_number): + return _number + 1 diff --git a/examples/whole-pipeline-gradle/app/hello.py b/examples/whole-pipeline-gradle/app/hello.py new file mode 100644 index 0000000..7beacb4 --- /dev/null +++ b/examples/whole-pipeline-gradle/app/hello.py @@ -0,0 +1,15 @@ +import counter +from flask import Flask + +app = Flask(__name__) +greeted_times = 0 + +@app.route("/") +def hello(): + global greeted_times + + greeting = "Hello World! I've greeted {0} times!".format( greeted_times ) + + greeted_times = counter.plusone( greeted_times ) + + return greeting \ No newline at end of file diff --git a/examples/whole-pipeline-gradle/app/test_counter.py b/examples/whole-pipeline-gradle/app/test_counter.py new file mode 100644 index 0000000..a731d97 --- /dev/null +++ b/examples/whole-pipeline-gradle/app/test_counter.py @@ -0,0 +1,6 @@ +import unittest +import counter + +class TestCounter(unittest.TestCase): + def test_plusone(self): + self.assertEqual(2, counter.plusone(1)) \ No newline at end of file diff --git a/examples/whole-pipeline-gradle/app/test_hello.py b/examples/whole-pipeline-gradle/app/test_hello.py new file mode 100644 index 0000000..85cfe83 --- /dev/null +++ b/examples/whole-pipeline-gradle/app/test_hello.py @@ -0,0 +1,8 @@ +import unittest +import hello + +class TestCounter(unittest.TestCase): + def test_hello(self): + self.assertEqual( "Hello World! I've greeted 0 times!", hello.hello() ) + self.assertEqual( "Hello World! I've greeted 1 times!", hello.hello() ) + self.assertEqual( "Hello World! I've greeted 2 times!", hello.hello() ) \ No newline at end of file diff --git a/examples/whole-pipeline-gradle/build.gradle b/examples/whole-pipeline-gradle/build.gradle new file mode 100644 index 0000000..d2bcba4 --- /dev/null +++ b/examples/whole-pipeline-gradle/build.gradle @@ -0,0 +1,72 @@ +plugins { + // Apply the groovy plugin to add support for Groovy + id 'groovy' + // Apply Gradle JPI plugin to simplify dependency management + id 'org.jenkins-ci.jpi' version '0.43.0' +} + +def testLogLevel = project.findProperty('testLogLevel') ?: 'ERROR' + +group 'com.example' +version 'O.1-SNAPSHOT' + +jenkinsPlugin { + // verions of Jenkins core jpi plugin depends on + jenkinsVersion = '2.355' + // optional list of package prefixes to mask + maskClasses = 'jaxen' +} + +allprojects { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 +} + +repositories { + mavenCentral() + mavenLocal() + maven { url 'https://repo.jenkins-ci.org/releases/' } +} + +dependencies { + implementation 'org.codehaus.groovy:groovy-all:2.5.17' + + testImplementation 'com.homeaway.devtools.jenkins:jenkins-spock:2.1.5' + testImplementation 'ch.qos.logback:logback-core:1.2.11' + testImplementation 'ch.qos.logback:logback-classic:1.2.11' + testImplementation 'javax.servlet:javax.servlet-api:3.1.0' + testImplementation 'org.jenkins-ci.main:jenkins-core:2.355' + // provides stage() step + testImplementation 'org.jenkins-ci.plugins:pipeline-stage-step:293.v200037eefcd5' + // provides sh() step + testImplementation 'org.jenkins-ci.plugins.workflow:workflow-durable-task-step:1146.v1a_d2e603f929' + // provides GlobalVariable + testImplementation 'org.jenkins-ci.plugins.workflow:workflow-cps-global-lib:588.v576c103a_ff86' + // provides slackSend() step + testImplementation 'org.jenkins-ci.plugins:slack:608.v19e3b_44b_b_9ff' + // provides sshagent() step + testImplementation 'org.jenkins-ci.plugins:ssh-agent:295.v9ca_a_1c7cc3a_a_' + + // plugin dependencies + testImplementation 'com.cloudbees:groovy-cps:1.32' // required by workflow-cps plugin +} + +task cleanSharedLibraries(type: Delete) { + delete "$buildDir/classes/groovy/test/vars" +} + +task prepareSharedLibraries(type: Copy) { + dependsOn tasks.cleanSharedLibraries + from '.' + include 'Jenkinsfile' + into "$buildDir/classes/groovy/test/vars" +} + +test { + dependsOn tasks.prepareSharedLibraries + systemProperties = [ + 'root.loglevel': testLogLevel, + 'root.appender': 'Stdout', + 'logdir': project.buildDir + ] +} diff --git a/examples/whole-pipeline-gradle/gradle/wrapper/gradle-wrapper.jar b/examples/whole-pipeline-gradle/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/examples/whole-pipeline-gradle/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/whole-pipeline-gradle/gradle/wrapper/gradle-wrapper.properties b/examples/whole-pipeline-gradle/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8fad3f5 --- /dev/null +++ b/examples/whole-pipeline-gradle/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/whole-pipeline-gradle/gradlew b/examples/whole-pipeline-gradle/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/examples/whole-pipeline-gradle/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/whole-pipeline-gradle/gradlew.bat b/examples/whole-pipeline-gradle/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/examples/whole-pipeline-gradle/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/whole-pipeline-gradle/settings.gradle b/examples/whole-pipeline-gradle/settings.gradle new file mode 100644 index 0000000..2a547e4 --- /dev/null +++ b/examples/whole-pipeline-gradle/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'jenkinsfile-test-whole-pipeline' \ No newline at end of file diff --git a/examples/whole-pipeline-gradle/src/test/groovy/JenkinsfileSpec.groovy b/examples/whole-pipeline-gradle/src/test/groovy/JenkinsfileSpec.groovy new file mode 100644 index 0000000..f4c1c74 --- /dev/null +++ b/examples/whole-pipeline-gradle/src/test/groovy/JenkinsfileSpec.groovy @@ -0,0 +1,64 @@ +import com.homeaway.devtools.jenkins.testing.JenkinsPipelineSpecification + +public class JenkinsfileSpec extends JenkinsPipelineSpecification { + + def Jenkinsfile = null + + public static class DummyException extends RuntimeException { + public DummyException(String _message) { super( _message ); } + } + + def setup() { + script_class_path = ["."] + Jenkinsfile = loadPipelineScriptForTest("/Jenkinsfile") + Jenkinsfile.getBinding().setVariable( "scm", null ) + } + + def "Slack is notified when tests fail" () { + setup: + getPipelineMock("sh")("docker run --entrypoint python whole-pipeline -m unittest discover") >> { + throw new DummyException("Dummy test failure") + } + when: + try { + Jenkinsfile.run() + } catch( DummyException e ) {} + then: + 1 * getPipelineMock("slackSend")( _ as Map ) + } + + def "Attempts to deploy MASTER branch to PRODUCTION" () { + setup: + Jenkinsfile.getBinding().setVariable( "BRANCH_NAME", "master" ) + when: + Jenkinsfile.run() + then: + 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-prod .*/}) + } + + def "Does NOT attempt to deploy non-MASTER branch PRODUCTION" () { + setup: + Jenkinsfile.getBinding().setVariable( "BRANCH_NAME", "develop" ) + when: + Jenkinsfile.run() + then: + 0 * getPipelineMock("sh")({it =~ /ssh deployer@app-prod .*/}) + } + + def "deploy function deploys to TEST when asked" () { + when: + Jenkinsfile.deploy( "test" ) + then: + 1 * getPipelineMock("sshagent")(["test-ssh"], _ as Closure) + 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-test .*/}) + } + + def "deploy function deploys to PRODUCTION when asked" () { + when: + Jenkinsfile.deploy( "production" ) + then: + 1 * getPipelineMock("sshagent")(["prod-ssh"], _ as Closure) + 1 * getPipelineMock("sh")({it =~ /ssh deployer@app-prod .*/}) + } +} + diff --git a/examples/whole-pipeline-gradle/src/test/resources/logback-test.xml b/examples/whole-pipeline-gradle/src/test/resources/logback-test.xml new file mode 100644 index 0000000..a4fe093 --- /dev/null +++ b/examples/whole-pipeline-gradle/src/test/resources/logback-test.xml @@ -0,0 +1,41 @@ + + + + + + + + + + [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n + + + + + + ${logdir}/stdout.log + + ${logdir}/stdout.log.%i + + 1 + 10 + + + 20MB + + + [%date{ISO8601}]\(%t\)\([%X{requestMarker}]\) %p %logger{0} - %m%n + + + + + + + + + + \ No newline at end of file diff --git a/src/main/groovy/com/homeaway/devtools/jenkins/testing/JenkinsPipelineSpecification.groovy b/src/main/groovy/com/homeaway/devtools/jenkins/testing/JenkinsPipelineSpecification.groovy index bc3dc99..f542bee 100644 --- a/src/main/groovy/com/homeaway/devtools/jenkins/testing/JenkinsPipelineSpecification.groovy +++ b/src/main/groovy/com/homeaway/devtools/jenkins/testing/JenkinsPipelineSpecification.groovy @@ -383,13 +383,13 @@ then: * Therefore, the descriptor must classloaded and then instantiated in order to get the right name. *

*

- * Some Jenkins extensions try to access the {@link Jenkins} singelton in static { ... } setup, + * Some Jenkins extensions try to access the {@link Jenkins} singleton in static { ... } setup, * or in their Descriptor's constructor. * The mock Jenkins that is automatically created for every test case won't help here because no test cases are running yet: * jenkins-spock is still setting up. *

*

- * In case a test suite involves classes that behave this way, jenkins-spock automatically creates a separate, static Spock mock of the {@link Jenkins} singelton + * In case a test suite involves classes that behave this way, jenkins-spock automatically creates a separate, static Spock mock of the {@link Jenkins} singleton * and injects it into the {@link Jenkins} class before any Extensions are classloaded and before any Descriptors are instantiated. * This mock cannot be stubbed and its interactions cannot be verified because * Spock mocks do not fully work outside a specification. @@ -704,7 +704,7 @@ public abstract class JenkinsPipelineSpecification extends Specification { // undefined, and un-mocked pipeline step. // print a helpful error message. MissingMethodException mme = new MissingMethodException( "(intercepted on instance [${object}] during test [${this}]) ${_name}", delegate.getClass(), _args ) - throw new IllegalStateException( "During a test, the pipeline step [${_name}] was called but there was no mock for it.\n\t1. Is the name correct?\n\t2. Does the pipeline step have a descriptor with that name?\n\t3. Does that step come from a plugin? If so, is that plugin listed as a dependency in your pom.xml?\n\t4. If not, you may need to call explicitlyMockPipelineStep('${_name}') in your test's setup: block.", mme ) + throw new IllegalStateException( "During a test, the pipeline step [${_name}] was called but there was no mock for it.\n\t1. Is the name correct?\n\t2. Does the pipeline step have a descriptor with that name?\n\t3. Does that step come from a plugin? If so, is that plugin listed as a dependency in your pom.xml (or build.gradle)?\n\t4. If not, you may need to call explicitlyMockPipelineStep('${_name}') in your test's setup: block.", mme ) } def originalPropertyMissing = object.metaClass.getMetaMethod("propertyMissing", "string" ) @@ -750,7 +750,7 @@ public abstract class JenkinsPipelineSpecification extends Specification { } MissingPropertyException mpe = new MissingPropertyException( "(intercepted on instance [${object}] during test [${this}]) ${_name}", object.getClass() ) - throw new IllegalStateException( "There is no pipeline variable mock for [${_name}].\n\t1. Is the name correct?\n\t2. Is it a GlobalVariable extension point? If so, does the getName() method return [${_name}]?\n\t3. Is that variable normally defined by Jenkins? If so, you may need to define it by hand in your Spec.\n\t4. Does that variable come from a plugin? If so, is that plugin listed as a dependency in your pom.xml?\n\t5. If not, you may need to call explicitlyMockPipelineVariable(\"${_name}\") during your test setup.", mpe ) + throw new IllegalStateException( "There is no pipeline variable mock for [${_name}].\n\t1. Is the name correct?\n\t2. Is it a GlobalVariable extension point? If so, does the getName() method return [${_name}]?\n\t3. Is that variable normally defined by Jenkins? If so, you may need to define it by hand in your Spec.\n\t4. Does that variable come from a plugin? If so, is that plugin listed as a dependency in your pom.xml (or build.gradle)?\n\t5. If not, you may need to call explicitlyMockPipelineVariable(\"${_name}\") during your test setup.", mpe ) } instrumented_objects.add( object ) @@ -800,7 +800,7 @@ public abstract class JenkinsPipelineSpecification extends Specification { } } - throw new IllegalStateException( "There is no pipeline step mock for [${_pipeline_extension}].\n\t1. Is the name correct?\n\t2. Does the pipeline step have a descriptor with that name?\n\t3. Does that step come from a plugin? If so, is that plugin listed as a dependency in your pom.xml?\n\t4. If not, you may need to call explicitlyMockPipelineStep('${_pipeline_extension}') in your test's setup: block." ) + throw new IllegalStateException( "There is no pipeline step mock for [${_pipeline_extension}].\n\t1. Is the name correct?\n\t2. Does the pipeline step have a descriptor with that name?\n\t3. Does that step come from a plugin? If so, is that plugin listed as a dependency in your pom.xml (or build.gradle)?\n\t4. If not, you may need to call explicitlyMockPipelineStep('${_pipeline_extension}') in your test's setup: block." ) } return mocks.get( _pipeline_extension ) @@ -824,7 +824,7 @@ public abstract class JenkinsPipelineSpecification extends Specification { * You should either *

*
    - *
  1. Add a dependency to your pom.xml that brings in the plugin that provides the necessary pipeline step
  2. + *
  3. Add a dependency to your pom.xml (or build.gradle) that brings in the plugin that provides the necessary pipeline step
  4. *
  5. Refactor your code to not depend on the pipeline step "magically" existing. *
*