david.perez
david.perez

Reputation: 7012

Can I create dynamically stages in a Jenkins pipeline?

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.

Upvotes: 90

Views: 124189

Answers (8)

np2807
np2807

Reputation: 1160

If you don't want to use a for loop and want the generated pipeline to be executed in parallel, 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 want to execute the generated stages in 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 that the return in generateStage is a must.

Upvotes: 39

BuckTheBug
BuckTheBug

Reputation: 428

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)
            }
        }
    }
}

Upvotes: 25

Krzysztof Skrzynecki
Krzysztof Skrzynecki

Reputation: 2525

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

Upvotes: 11

AKS
AKS

Reputation: 17306

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.

Upvotes: 1

Anton Yurchenko
Anton Yurchenko

Reputation: 619

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

Upvotes: 39

david.perez
david.perez

Reputation: 7012

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 '....'
    }
}

Upvotes: 54

Aaron D. Marasco
Aaron D. Marasco

Reputation: 6748

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

Upvotes: 21

JamesD
JamesD

Reputation: 2546

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
          }
        }

Upvotes: 3

Related Questions