Skip to content

Instantly share code, notes, and snippets.

@jimklimov
Last active November 7, 2022 18:11
Show Gist options
  • Save jimklimov/28e480a635c8de2d0cdf2250a4277c4f to your computer and use it in GitHub Desktop.
Save jimklimov/28e480a635c8de2d0cdf2250a4277c4f to your computer and use it in GitHub Desktop.
Jenkins/groovy scripting ideas from hichhiker @IRC

= Self-wrapping code()

I have my own version of "declarative" we use, and in that case you can add parameters like timestamps: true so I have code roughly (see below) like:

def workflow = { // stuff to do }
if (config.timestamps){
    //*******NEEDED *****//
    def inner = workflow()
    //********************//
    workflow = { timestamps { inner() } }
}
// other possible wrappers
echo "Starting workflow"
workflow()

scope is important, if same payload variable is used, it is not evaluated until payload is called, which may end up in trouble - your best bet is to make sure the scope is isolated for each call

// this should isolate payload for each call - so that you do not have collisions/circular references
def wrapWithMatrix(matrixTag, userBody){
  def payload = { userBody.call() } 
  if (matrixTag != null) { 
    return { script.withEnv([matrixTag]) { payload() } } 
  }
  return payload
}

If all is needed in one go, temporary vars can help avoid infinite recursion (see inside vs perform):

    // Wrap with timestamps, if necessary and available
    if (config.timestamps) {
      if (validStep('timestamps')) {
        debug 'enabling timestamps'
        def inside = perform
        perform = {
          steps.timestamps {
            return inside()
          }
        }
      } else {
          steps.echo 'WARNING: timetamps are requested but plugin is missing, not enabling'
      }
    } else {
      debug 'timestamps are requested, not plugin is not installed'
    }

== Running in node... or not:

  • i.e.
....
  // Note: named closure, not method
  def content = {
    // execute code
  }
  if (condition) {
      node("label&&label2"){
        content()
      } else{
        content()
      }
  }

so in your case if you want to simplify things:

def withOptionalNode(label, closure code){
    if (label){
      code = {
        node(label){
          code()
        }
      }
    code()
}

Also add an overload:

 def withOptionalNode(Closure code){
    withOptionalNode(null, code)
  }

now you can just use

   ...
   withOptionalNode {
        ...
   }

   withOptionalNode(label) {
        ...
   }

and it will allocate node if label var has content and not if not

  • BTW, it occurs to me that even simpler version of withOptionalNode would be (together with overload):
/**
  * Run code with optional node allocation if label is specified
  **/
def withOptionalNode(label, Closure code) {
  return label ? node(label, code) : code()
}

/**
  * wrapper for `withOptionalNode` without label provided
  **/
def withOptionalNode(Closure code) {
  return withOptionalNode(null, code)
}

== Populating parallel blocks

def myParallelTasks = [
  task1: { ..... },
  task2: { .... },
  ...
  failFast: false
]

...with generated closure contents:

i.e.

 def makeContent(param1, param2){
   def computeParam3 = ...
   return {
      echo "Param1: ${param1}"
      echo "Param2: ${param2}"
      echo "computeParam3: ${computeParam3}"
   }
}

...
def parts = [
   part1: makeContent("1", "2"),    
   part2: makeContent("3", "4"),
]

and also

    part3: {
        node("bob"){
             makeContent("bob", "5").call()
        }
    }

== Missing parameters

btw, should groovy fail or not for access to map entry by name that is not defined there? :)

groovy will return null if parameter not present in map and fail if map is null - but you can protect yourself:

Map map = [a:'b']
assert  map.a == 'b'
map = []
assert  map.a == null
map = null
// this with throw an exception
assert map.a != null
// this will work
assert map?.a == null

this can fail: map.a.lower() so will this map?.a.lower() however this will return null: map?.a?.lower()

you can use elvis to have a non-null return too, i.e. echo "${map?.a?.lower() ?: '')" will not print null if a is missing

  • Idea on Separating reuse of same closure with unique args so parallels do not step on each other's toes:
def makeAction(arg){
  return { ->
    echo "Something that uses ${arg}"
  }
}
parallel [
  one: makeAction('one'),
  two: makeAction('two'),
]
  • i.e. instead of
def action = { x -> ... }

either do

def action(x){
    ...
}

or, if you really want it to be a closure

def makeAction(){
  return {x -> ... }
}
  • on methods vs closures... something like that:
parallel [
  one: { action('one') }, // if action is a method and not closure
  two: { makeAction()('two') } // if makeAction returns a closuire
]
  • for scripted it is same as parallel, but instead of parallel you do something like
stages.each { name, closure ->
  stage(name){
    closure()
  }
}

though you may or may not have an issue with order, you can use list instead of map for closures to maintain order

  • Parallel with "quiet" failfast equivalent: let running stages complete, do not start new ones
//in the beginning
boolean failed=false

def stageWrapper(Closure action){
  if (! failed){
    try {
      action()
    } catch (exception ex) {
      failed = true
      //handle exception - or just re-throw it
    }
  }
}

parallel([
    one: { stageWrapper{
        // your code
      },
   two: { stageWrapper{
        // your code
      }
    }
])
  • Wrapping default/custom closures for generic code
// vars/lockableDB.groovy

def onprem(Map options=[:]){
  def config = [ 
     setidCloneDBFunc: { param -> return "default closure with ${param}" },
     param: 'default value'
  ] << options ?: [:]
  //code
  echo "${config.setidCloneDBFunc(config.param)}"
}

and users call it as:

  lockableDB.onperm()

or

lockableDB.onprem  setidCloneDBFunc: { param ->
          return "custom closure with ${param}"
      }
}

NOTE: it's an example, not exact solution

If you want an object/class code, you can place it behind the var implementation

This sort of an approach makes it more DSL-y on the enduser, similar to declarative approach - but works either way. Also removes the need for imports and other complex code things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment