78

I need to launch a dynamic set of tests in a declarative pipeline. For better visualization purposes, I'd like to create a stage for each test. Is there a way to do so?

The only way to create a stage I know is:

stage('foo') {
   ...
}

I've seen this example, but I it does not use declarative syntax.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
david.perez
  • 6,090
  • 4
  • 34
  • 57

8 Answers8

50

Use the scripted syntax that allows more flexibility than the declarative syntax, even though the declarative is more documented and recommended.

For example stages can be created in a loop:

def tests = params.Tests.split(',')
for (int i = 0; i < tests.length; i++) {
    stage("Test ${tests[i]}") {
        sh '....'
    }
}
Adam Ocsvari
  • 8,056
  • 2
  • 17
  • 30
david.perez
  • 6,090
  • 4
  • 34
  • 57
  • I was able to put the loop inside a `script` block of the first `stage` that I call `Initialize`. – haridsv Jun 18 '18 at 11:22
  • 1
    @haridsv and for the display pipeline, does it show you seperately the stages ? – sirineBEJI Jul 04 '18 at 13:33
  • 1
    @codeGeass Yes, and it seems to also account the time correctly (i.e., it doesn't aggregate the time to the `Initialize` stage). – haridsv Jul 04 '18 at 17:16
  • 1
    You can visualize with Blue Ocean ? because I just get the Initialize stage and other stages are like steps of that stage. – sirineBEJI Jul 05 '18 at 08:14
  • 5
    This seems not to be working. Can you provide a complete example ? From what I unterstood we need to create a Pipeline project then that snippet does not work... – Jorge Machado Jul 11 '18 at 06:02
  • This does not work. First off, there must be steps. And I can't get dynamically created stages in the steps block – hiquetj Sep 05 '18 at 18:12
  • 1
    The code had a typo ( fixed it). This works for me, it creates stages depending on the input parameter. You need an input parameter named 'Tests' which is a coma (',') separated list of test cases. Conceptual issues: If you change the input, it creates new stages and you loose the history. – Adam Ocsvari Oct 19 '18 at 11:59
  • 1
    @haridsv it seems to work BUT there is one big draw on this: the restart stage feature does only detect your INIT stage as restartable stage. The "substages" are not restartable – Dakkar Nov 13 '18 at 08:08
  • 1
    @Dakkar Thanks for pointing that out! I have never tried that feature so was unaware of this limitation. – haridsv Nov 13 '18 at 12:34
  • 1
    All this works and I was also able to run dynamically generated stages in parallel. I see 2 issues btw, one is that Blu Ocean doesn't interpret really well the sequential stages ran in parallel (so I guess same as @codeGeass) the other problem is that in the workflow view the execution time sometimes reports NaNy NaNd. I believe that what interprets the pipeline might get confused view complex structures – Giuseppe Salvatore Apr 09 '19 at 14:58
37

If you don't want to use for loop, and generated pipeline to be executed in parallel then, here is an answer.

def jobs = ["JobA", "JobB", "JobC"]
 
def parallelStagesMap = jobs.collectEntries {
    ["${it}" : generateStage(it)]
}
 
def generateStage(job) {
    return {
        stage("stage: ${job}") {
                echo "This is ${job}."
        }
    }
}
 
pipeline {
    agent none
 
    stages {
        stage('non-parallel stage') {
            steps {
                echo 'This stage will be executed first.'
            }
        }
 
        stage('parallel stage') {
            steps {
                script {
                    parallel parallelStagesMap
                }
            }
        }
    }
}

Note that all generated stages will be executed into 1 node. If you are willing to executed the generated stages to be executed into different nodes.

def agents  = ['master', 'agent1', 'agent2']
// enter valid agent name in array.

def generateStage(nodeLabel) {
    return {
        stage("Runs on ${nodeLabel}") { 
            node(nodeLabel) {
                echo "Running on ${nodeLabel}"
            }
        }
    }
}
def parallelStagesMap = agents.collectEntries {
    ["${it}" : generateStage(it)]
}
pipeline {
    agent none
    stages {
        stage('non-parallel stage') {
            steps {
                echo 'This stage will be executed first.'
            }
        }

        stage('parallel stage') {
            steps {
                script {
                    parallel parallelStagesMap
                }
            }
        }        
    }
}

You can of course add more than 1 parameters and can use collectEntries for 2 parameters.

Please remember return in function generateStage is must.

np2807
  • 1,050
  • 15
  • 30
  • 1
    Concise, and it works with Jenkins 2.263.4 – mhvelplund Mar 14 '21 at 11:55
  • 1
    Hi, thank u! It works for me. But now, I want to cleanWorkspace on each agent. Where can I add this script: ```post { cleanup { cleanWs() }}}``` Please help! Thank in advance! – himneh Jul 28 '22 at 03:49
  • Hi.. can you please suggest what approach should I take to solve this issue? https://stackoverflow.com/questions/76407000/jenkins-dynamic-job-creation-throws-pipeline-cps-method-mismatches-error – RISHI KHANNA Jun 13 '23 at 04:48
34

As JamesD suggested, you may create stages dynamically (but they will be sequential) like that:

def list
pipeline {
    agent none
    options {buildDiscarder(logRotator(daysToKeepStr: '7', numToKeepStr: '1'))}
    stages {
        stage('Create List') {
            agent {node 'nodename'}
            steps {
                script {
                    // you may create your list here, lets say reading from a file after checkout
                    list = ["Test-1", "Test-2", "Test-3", "Test-4", "Test-5"]
                }
            }
            post {
                cleanup {
                    cleanWs()
                }
            }
        }
        stage('Dynamic Stages') {
            agent {node 'nodename'}
            steps {
                script {
                    for(int i=0; i < list.size(); i++) {
                        stage(list[i]){
                            echo "Element: $i"
                        }
                    }
                }
            }
            post {
                cleanup {
                    cleanWs()
                }
            }
        }
    }
}

That will result in: dynamic-sequential-stages

Anton Yurchenko
  • 540
  • 4
  • 5
  • 3
    Thanks a bunch, man! As an alternative to `for (...) {`, I was able to use `list.each { listItem ->`. I assume that `list.eachWithIndex { listItem, i ->` would also work, for people who happen to need the index. – Cameron Hudson Feb 14 '20 at 17:55
  • can we use multiple stages in a for loop? – coder here May 10 '21 at 10:19
25

@Jorge Machado: Because I cannot comment I had to post it as an answer. I've solved it recently. I hope it'll help you.

Declarative pipeline:

A simple static example:

stage('Dynamic') {
        steps {
            script {
                stage('NewOne') {

                        echo('new one echo')

                }
            }
        }
    }

Dynamic real-life example:

    // in a declarative pipeline
        stage('Trigger Building') {
              when {
                environment(name: 'DO_BUILD_PACKAGES', value: 'true')
              }
              steps {
                executeModuleScripts('build') // local method, see at the end of this script
              }
            }


    // at the end of the file or in a shared library
        void executeModuleScripts(String operation) {

          def allModules = ['module1', 'module2', 'module3', 'module4', 'module11']

          allModules.each { module ->  
          String action = "${operation}:${module}"  

          echo("---- ${action.toUpperCase()} ----")        
          String command = "npm run ${action} -ddd"                   

            // here is the trick           
            script {
              stage(module) {
                bat(command)
              }
            }
          }

}
BuckTheBug
  • 428
  • 4
  • 6
  • 7
    Thanks for posting this! I didn't pay as much attention to your response as I should have, because the lack of a `steps` block within the inner stage definition is quite important. Hopefully this may help someone else who just skimmed instead of really reading :) – Janis Peisenieks Apr 04 '19 at 10:22
21

You might want to take a look at this example - you can have a function return a closure which should be able to have a stage in it.

This code shows the concept, but doesn't have a stage in it.

def transformDeployBuildStep(OS) {
    return {
        node ('master') { 
        wrap([$class: 'TimestamperBuildWrapper']) {
...
        } } // ts / node
    } // closure
} // transformDeployBuildStep

stage("Yum Deploy") {
  stepsForParallel = [:]
  for (int i = 0; i < TargetOSs.size(); i++) {
      def s = TargetOSs.get(i)
      def stepName = "CentOS ${s} Deployment"
      stepsForParallel[stepName] = transformDeployBuildStep(s)
  }
  stepsForParallel['failFast'] = false
  parallel stepsForParallel
} // stage
Aaron D. Marasco
  • 6,506
  • 3
  • 26
  • 39
7

Just an addition to what @np2807 and @Anton Yurchenko have already presented: you can create stages dynamically and run the in parallel by simply delaying list of stages creation (but keeping its declaration), e.g. like that:

def parallelStagesMap

def generateStage(job) {
    return {
        stage("stage: ${job}") {
                echo "This is ${job}."
        }
    }
}
 
pipeline {
    agent { label 'master' }
 
    stages {
        stage('Create List of Stages to run in Parallel') {
            steps {
                script {
                    def list = ["Test-1", "Test-2", "Test-3", "Test-4", "Test-5"]
                    // you may create your list here, lets say reading from a file after checkout
                    // personally, I like to use scriptler scripts and load the as simple as:
                    // list = load '/var/lib/jenkins/scriptler/scripts/load-list-script.groovy'
                    parallelStagesMap = list.collectEntries {
                        ["${it}" : generateStage(it)]
                    }
                }
            }
        }
 
        stage('Run Stages in Parallel') {
            steps {
                script {
                    parallel parallelStagesMap
                }
            }
        }
    }
}

That will result in Dynamic Parallel Stages: Dynamic Parallel Stages pipeline diagram

Krzysztof Skrzynecki
  • 2,345
  • 27
  • 39
3

I use this to generate my stages which contain a Jenkins job in them. build_list is a list of Jenkins jobs that i want to trigger from my main Jenkins job, but have a stage for each job that is trigger.

build_list = ['job1', 'job2', 'job3']
        for(int i=0; i < build_list.size(); i++) {
          stage(build_list[i]){
               build job: build_list[i], propagate: false
          }
        }
JamesD
  • 2,466
  • 24
  • 40
1

if you are using Jenkinsfile then, I achieved it via dynamically creating the stages, running them in parallel and also getting Jenkinsfile UI to show separate columns. This assumes parallel steps are independent of each other (otherwise don't use parallel) and you can nest them as deep as you want (depending upon the # of for loops you'll nest for creating stages).

Jenkinsfile Pipeline DSL: How to Show Multi-Columns in Jobs dashboard GUI - For all Dynamically created stages - When within PIPELINE section see here for more.

AKS
  • 16,482
  • 43
  • 166
  • 258