50

I am attempting to write a scripted Jenkinsfile using the groovy DSL which will have parallel steps within a set of stages.

Here is my jenkinsfile:

node {   
stage('Build') {
    sh 'echo "Build stage"'
}

stage('API Integration Tests') {
    parallel Database1APIIntegrationTest: {
        try {
            sh 'echo "Build Database1APIIntegrationTest parallel stage"'
        }
        finally {
            sh 'echo "Finished this stage"'
        }               

    }, Database2APIIntegrationTest: {
        try {
            sh 'echo "Build Database2APIIntegrationTest parallel stage"'
        }
        finally {
            sh 'echo "Finished this stage"'
        }

    }, Database3APIIntegrationTest: {
        try {
            sh 'echo "Build Database3APIIntegrationTest parallel stage"'
        }
        finally {
            sh 'echo "Finished this stage"'
        }
    }
}

stage('System Tests') {
    parallel Database1APIIntegrationTest: {
        try {
            sh 'echo "Build Database1APIIntegrationTest parallel stage"'
        }
        finally {
            sh 'echo "Finished this stage"'
        }               

    }, Database2APIIntegrationTest: {
        try {
            sh 'echo "Build Database2APIIntegrationTest parallel stage"'
        }
        finally {
            sh 'echo "Finished this stage"'
        }

    }, Database3APIIntegrationTest: {
        try {
            sh 'echo "Build Database3APIIntegrationTest parallel stage"'
        }
        finally {
            sh 'echo "Finished this stage"'
        }
    }
}
}

I want to have 3 stages: Build; Integration Tests and System Tests. Within the two test stages, I want to have 3 sets of the tests executed in parallel, each one against a different database.

I have 3 available executors. One on the master, and 2 agents and I want each parallel step to run on any available executor.

What I've noticed is that after running my pipeline, I only see the 3 stages, each marked out as green. I don't want to have to view the logs for that stage to determine whether any of the parallel steps within that stage were successful/unstable/failed.

I want to be seeing the 3 steps within my test stages - marked as either green, yellow or red (Success, unstable or failed).

I've considered expanding the tests out into their own stages, but have realised that parallel stages are not supported (Does anyone know whether this will ever be supported?), so I cannot do this as the pipeline would take far too long to complete.

Any insight would be much appreciated, thanks

Ryan Jones
  • 501
  • 1
  • 4
  • 5

8 Answers8

90

In Jenkins scripted pipeline, parallel(...) takes a Map describing each stage to be built. Therefore you can programatically construct your build stages up-front, a pattern which allows flexible serial/parallel switching.
I've used code similar to this where the prepareBuildStages returns a List of Maps, each List element is executed in sequence whilst the Map describes the parallel stages at that point.

// main script block
// could use eg. params.parallel build parameter to choose parallel/serial 
def runParallel = true
def buildStages

node('master') {
  stage('Initialise') {
    // Set up List<Map<String,Closure>> describing the builds
    buildStages = prepareBuildStages()
    println("Initialised pipeline.")
  }

  for (builds in buildStages) {
    if (runParallel) {
      parallel(builds)
    } else {
      // run serially (nb. Map is unordered! )
      for (build in builds.values()) {
        build.call()
      }
    }
  }

  stage('Finish') {
      println('Build complete.')
  }
}

// Create List of build stages to suit
def prepareBuildStages() {
  def buildStagesList = []

  for (i=1; i<5; i++) {
    def buildParallelMap = [:]
    for (name in [ 'one', 'two', 'three' ] ) {
      def n = "${name} ${i}"
      buildParallelMap.put(n, prepareOneBuildStage(n))
    }
    buildStagesList.add(buildParallelMap)
  }
  return buildStagesList
}

def prepareOneBuildStage(String name) {
  return {
    stage("Build stage:${name}") {
      println("Building ${name}")
      sh(script:'sleep 5', returnStatus:true)
    }
  }
}

The resulting pipeline appears as: Jenkins Blue Ocean parallel pipeline

There are certain restrictions on what can be nested within a parallel block, refer to the pipeline documentation for exact details. Unfortunately much of the reference seems biased towards declarative pipeline, despite it being rather less flexible than scripted (IMHO). The pipeline examples page was the most helpful.

Ed Randall
  • 6,887
  • 2
  • 50
  • 45
  • 2
    Is it possible to have multiple steps in a parallel execution? What I mean: is is possible to have { || || } as a parallel step, where the 1.n are non-parallel stages? – D. Kovács Apr 26 '19 at 10:04
  • @D.Kovács I think you can only have one `stage{}` since it's the value of the map. But you can certainly put multiple steps within that `stage{}`, where a step is one of https://jenkins.io/doc/pipeline/steps/ as described in https://jenkins.io/doc/book/pipeline/syntax/#scripted-steps Possibly the Jenkins/BlueOcean UI would display them as just a single dot. – Ed Randall May 01 '19 at 11:34
  • This code works if I use it inside 'Pipeline Script", but not when I use "Pipeline Script from SCM" (where I have the same code in a .groovy file - coming from GIT/BitBucket) **when I wrap it** within `pipeline { ... } ` section. How can I get that solved and also get the "Multi-columns" created for each Dynamically created "Stages" when I click on the Jenkinsfile job's dashboard (GUI). PS: Im not using Blue Ocean for now – AKS Nov 15 '19 at 21:11
  • @ArunSangal when you wrap in `pipeline{ ... }` you're not using scripted pipeline, you're using pipeline DSL. Someone thought it would be a good idea to create an easier way to write pipelines. But there are so many limitations and gotcha's you might as well ignore it and go with scripted syntax IMNSHO. – Ed Randall Nov 16 '19 at 21:57
  • 1
    @EdRandall So I tried it over the weekend and I think I got it (hello world way for now). Your post helped too. I'm tracking it here: https://stackoverflow.com/questions/58885845/jenkinsfile-how-to-show-multi-columns-in-jobs-dashboard-gui-for-all-dynamical (**when in pipeline { ... } section**) – AKS Nov 20 '19 at 17:11
  • 1
    @EdRandall Great post! A pity a doc on parallel scripting is missing in the Jenkins pipelines. cookbook There is some confusion here with your return parameters. I would rename the output of `prepareBuildStages()` to `buildStages` or rename the variable within the `Initialise` stage ;-) – jaques-sam Feb 14 '20 at 09:37
  • Note that you can't directly use the loop variable inside the closure, as it is captured by reference (`name` in this example): https://stackoverflow.com/questions/56348341/closures-in-groovy-not-capturing-outside-variables – iliis Jan 19 '22 at 13:56
  • See also https://www.jenkins.io/doc/pipeline/examples/#parallel-from-list for more examples. – iliis Jan 19 '22 at 13:58
  • 1
    +1 for bias towards declarative pipeline, scripting capabilities is somethign what makes jenkins unique among many others and they don't value it. – Roman Badiornyi Mar 06 '23 at 09:44
30

Here's a simple example without loops or functions based on @Ed Randall's post:

node('docker') {
    stage('unit test') {
        parallel([
            hello: {
                echo "hello"
            },
            world: {
                echo "world"
            }
        ])
    }

    stage('build') {
        def stages = [:]

        stages["mac"] = {
            echo "build for mac"
        }
        stages["linux"] = {
            echo "build for linux"
        }

        parallel(stages)
    }
}

...which yields this:

blue-ocean-view

Note that the values of the Map don't need to be stages. You can give the steps directly.

adanilev
  • 3,008
  • 3
  • 15
  • 20
  • This works nicely but is there any way to use failFast or something similar in scripted pipelines? – Fjaoos Jul 21 '21 at 11:39
  • 1
    @Fjaoos, I haven't tested it but a quick google gives some hints: https://stackoverflow.com/questions/58026189/how-to-use-failfast-in-dynamic-pipeline-in-jenkins also, the jenkins pipeline snippet generator has some advice: parallel firstBranch: { // do something }, secondBranch: { // do something else }, failFast: true|false – adanilev Jul 21 '21 at 12:27
14

Here is an example from their docs:

Parallel execution

The example in the section above runs tests across two different platforms in a linear series. In practice, if the make check execution takes 30 minutes to complete, the "Test" stage would now take 60 minutes to complete!

Fortunately, Pipeline has built-in functionality for executing portions of Scripted Pipeline in parallel, implemented in the aptly named parallel step.

Refactoring the example above to use the parallel step:

// Jenkinsfile (Scripted Pipeline)


stage('Build') {
    /* .. snip .. */
}

stage('Test') {
    parallel linux: {
        node('linux') {
            checkout scm
            try {
                unstash 'app'
                sh 'make check'
            }
            finally {
                junit '**/target/*.xml'
            }
        }
    },
    windows: {
        node('windows') {
            /* .. snip .. */
        }
    }
}
Behrang
  • 46,888
  • 25
  • 118
  • 160
  • 1
    parallel in stage level is something a bit different then parallel in script level. Sadly on my Jenkins machine parallel in stage level ends with stackoverflow :(. – Marek R Nov 06 '19 at 09:49
10

To simplify the answer of @Ed Randall here. Remember this is Jenkinsfile scripted (not declarative)

stage("Some Stage") {
    // Stuff ...
}


stage("Parallel Work Stage") {

    // Prealocate dict/map of branchstages
    def branchedStages = [:]

    // Loop through all parallel branched stage names
    for (STAGE_NAME in ["Branch_1", "Branch_2", "Branch_3"]) {

        // Define and add to stages dict/map of parallel branch stages
        branchedStages["${STAGE_NAME}"] = {
            stage("Parallel Branch Stage: ${STAGE_NAME}") {
                // Parallel stage work here
                sh "sleep 10"
            }
        }

    }

    // Execute the stages in parallel
    parallel branchedStages
}


stage("Some Other Stage") {
    // Other stuff ...
}

Please pay attention to the curly braces. This will result in the following result (with the BlueOcean Jenkins Plugin):

Scripted Jenkinsfile Result Link

RedBassett
  • 3,469
  • 3
  • 32
  • 56
cat_nip_coffee
  • 113
  • 1
  • 8
  • 2
    There might be an issue? In my test, the code `stage("Parallel Branch Stage: ${STAGE_NAME}")` always use the last one in the list, because STAGE_NAME is a variable outside of the closure. – Wei Huang Jun 07 '21 at 05:23
  • 1
    @WeiHuang Yes, you can't directly use the loop variable, as the closure will capture the *variable*, not the *content*. See https://stackoverflow.com/questions/56348341/closures-in-groovy-not-capturing-outside-variables – iliis Jan 19 '22 at 13:53
  • Can you please help me to figure out the solution to similar problem ? https://stackoverflow.com/questions/76407000/jenkins-dynamic-job-creation-throws-pipeline-cps-method-mismatches-error – RISHI KHANNA Jun 13 '23 at 04:57
4

I was also trying similar sort of steps to execute parallel stages and display all of them in a stage view. You should write a stage inside a parallel step as shown in the following code block.

// Jenkinsfile (Scripted Pipeline)

stage('Build') {
    /* .. Your code/scripts .. */
}

stage('Test') {
    parallel 'linux': {
        stage('Linux') {
            /* .. Your code/scripts .. */
        }
    }, 'windows': {
        stage('Windows') {
            /* .. Your code/scripts .. */
        }
    }
}
Harshit
  • 617
  • 1
  • 6
  • 15
3

The above example with a FOR is wrong, as varible STAGE_NAME will be overwritten everytime, I had the same problem as Wei Huang.

Found the solution here:

https://www.convalesco.org/notes/2020/05/26/parallel-stages-in-jenkins-scripted-pipelines.html

def branchedStages = [:]
def STAGE_NAMES =  ["Branch_1", "Branch_2", "Branch_3"]
STAGE_NAMES.each { STAGE_NAME ->
 // Define and add to stages dict/map of parallel branch stages
    branchedStages["${STAGE_NAME}"] = {
        stage("Parallel Branch Stage: ${STAGE_NAME}") {
        // Parallel stage work here
            sh "sleep 10"
        }
    }
  }
parallel branchedStages
0

I have used as below where the three stages are parallel.

def testCases() {
  stage('Test Cases') {
    def stages = [:]    // declaring empty list
      stages['Unit Testing'] = {
      sh "echo Unit Testing completed"
      }
      stages['Integration Testing'] = {
        sh "echo Integration Testing completed"
      }
      stages['Function Testing'] = {
        sh "echo Function Testing completed"
      }
    parallel(stages) // declaring parallel stages
  }
}   
-1

I have used stage{} in parallel blocks several times. Then each stage shows up in the Stage view. The parent stage that contains parallel doesn't include the timing for all the parallel stages, but each parallel stage shows up in stage view.

In blue ocean, the parallel stages appear separately instead of the stages showing. If there is a parent stage, it shows as the parent of the parallel stages.

If you don't have the same experience, maybe a plugin upgrade is due.

Rob Hales
  • 5,123
  • 1
  • 21
  • 33
  • 2
    Can you give an example of where you have used a stage within parallel blocks? I am struggling to find any documentation on this – Ryan Jones Oct 20 '17 at 08:13