Scripted vs. Declarative Pipelines in Jenkins

Pipelines in Jenkins

When Jenkins 2 was released, the main feature introduced was the possibility of creating jobs as code using a DSL. This functionality is known as "Pipeline as code" and it is supported by all relevant CI tools nowadays.

The main advantages of Pipelines as code are:

  • Creating Pipelines by coding instead of configuring it through the UI of a CI/CD tool

  • Tracking changes in a Version Control System (VCS)

  • Easy update of a Pipeline by committing changes into a VCS

  • Flexibility provided by code

When I tried to create my first Pipeline in Jenkins I looked for examples in the Web and I felt a bit confused at the beginning. There were subtle differences in structure and code that I didn’t understand. Later I learned that those differences were related to the two different flavors of Pipelines available in Jenkins: Scripted and Declarative.

In this post I want to make a summary of the differences between them, pros and cons, and in which case you should use one rather than the other.

Scripted vs. Declarative

The best way to explain the differences is using an example, so you can find below a Pipeline in Scripted syntax and just after it the same version but translated to Declarative syntax.

Note the callouts numbers as I will use them to explain the main differences.

Scripted style
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
properties([
    parameters([                                       (1)
        gitParameter(branch: '',
                     branchFilter: 'origin/(.*)',
                     defaultValue: 'master',
                     description: '',
                     name: 'BRANCH',
                     quickFilterEnabled: false,
                     selectedValue: 'NONE',
                     sortMode: 'NONE',
                     tagFilter: '*',
                     type: 'PT_BRANCH')
    ])
])

def SERVER_ID="artifactory"                            (2)

node {                                                 (3)
    stage("Checkout") {
        git branch: "${params.BRANCH}", url: 'https://github.com/sergiosamu/blog-pipelines.git'
    }
    stage("Build") {
        try {                                          (4)
            withMaven(maven: "Maven363") {
                sh "mvn package"
            }
        } catch (error) {
            currentBuild.result='UNSTABLE'
        }
    }
    stage("Publish artifact") {
        def server = Artifactory.server "$SERVER_ID"   (5)

        def uploadSpec = """{
          "files": [
            {
              "pattern": "target/blog-pipelines*.jar",
              "target": "libs-snapshot-local/com/sergiosanchez/pipelines/"
            }
         ]
        }"""

        server.upload(uploadSpec)
    }
}
1 Input parameters as defined in the properties section
2 Variables are defined in Groovy language
3 The first element of a Scripted Pipeline is node
4 Error control is managed with a try/catch clause in Groovy Syntax
5 Artifactory configuration is defined through variables
Declarative style
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
properties([
    parameters([                                       (1)
        gitParameter(branch: '',
                     branchFilter: 'origin/(.*)',
                     defaultValue: 'master',
                     description: '',
                     name: 'BRANCH',
                     quickFilterEnabled: false,
                     selectedValue: 'NONE',
                     sortMode: 'NONE',
                     tagFilter: '*',
                     type: 'PT_BRANCH')
    ])
])

pipeline {                                             (2)
    agent any

    environment {                                      (3)
        SERVER_ID = 'artifactory'
    }

    stages {
        stage("Checkout") {
            steps {
                git branch: "${params.BRANCH}", url: 'https://github.com/sergiosamu/blog-pipelines.git'
            }
        }
        stage("Build") {
            steps {
                warnError("Fallo pruebas unitarias") { (4)
                    withMaven(maven: "Maven363") {
                        sh "mvn package"
                    }
                }
            }
        }
        stage("Publish artifact") {
            steps {
                rtUpload (                             (5)
                    serverId: "$SERVER_ID",
                    spec: '''{
                          "files": [
                            {
                              "pattern": "target/blog-pipelines*.jar",
                              "target": "libs-snapshot-local/com/sergiosanchez/pipelines/"
                            }
                         ]
                    }'''
                )
            }
        }
    }
}
1 Input parameters are defined in the same way as the Scripted Pipeline because the properties section is outside the Pipeline main structure
2 The first element of a Scripted Pipeline is pipeline. This is the best way to identify a Declarative Pipeline
3 Variables are defined in the Environments section. No Groovy-like variable declaration are allowed in Declarative syntax.
4 Try/catch structure is not allowed like any other Groovy syntax. The custom step warnError is used to manage build state
5 Artifactory plugin provides a step to easily upload an artifact without requiring Groovy code.


You can find both scripts as Jenkinsfile(s) in Github

Which one to choose?

I’ll share with you my thoughts on this, but as you know, it depends on each other’s context and project, so my advices may not be applicable to you.

First of all, the Declarative approach is the modern way of developing Pipelines and where Cloudbees, the company behind Jenkins, is putting more effort to evolve. In fact, the newest UI (Blue Ocean) is optimized and work best with Declarative Pipelines.

On the other hand, the Scripted syntax is more flexible since it is powered by the Groovy language and it can provide an easier way to implement Pipelines with complex logic. This can be a downside as well because you can rapidly find yourself fighting with hard to read and maintain Pipelines.

Although Declarative Pipelines are less flexible, they are more structured and readable. In addition, you can overcome their limitations when it comes to creating complex Pipelines by using a couple of approaches:

  1. Using a script step that allows you to embed Scripted syntax in a Declarative Pipeline. However, while this can be used as a temporary workaround or quick fix, it is not the recommended approach.

  2. Using shared libraries which is an utility that Jenkins provides to encapsulate and reuse common code among different Pipelines. These libraries are developed in Groovy language and can be easily invoked from Pipelines.

In my opinion, if you are already working with Scripted Pipelines and they work fine, I wouldn’t invest any effort in migrating them to Declarative ones (there is probably more to lose than to gain). However, for brand new Pipelines, I suggest trying with Declarative syntax and Shared libraries combo if possible.

Finally, I would like to sum up pros and cons for both approaches in the tables below to help you decide which style could fit better with your use case

Table 1. Scripted Pipelines
Pros Cons

Flexibility powered by Groovy language

Extensibility

Allows restarting from specific Stage

Shallow learning curve

Less readability harder to maintain

Harder to maintain

No integration with Blue Ocean

Restart from Stage option unavailable

Table 2. Declarative Pipelines
Pros Cons

Modern way of developing Pipelines and favored by the company behind Jenkins

Structured. Simpler syntax

Easy to read

Steep learning curve

Best integration with Blue Ocean interface

Allows restarting from specific stage

Less suitable for Pipelines with complex logic

Lack of compatibility with old plugins

Restrictive syntax

Running the examples

In case you are interested in running the Pipelines provided in this post, you will need a Jenkins 2 instance (I used version 2.204.2) and the following plugins installed:

  • Git parameter: in order to create the parameter to choose the git branch to run the Pipeline on.

  • Maven Integration: provides direct integration with maven through the withMaven step

  • Artifactory: for the integration with the repository manager

In addition, for the Publish artifact stage to work, you will need a target Artifactory instance running. It can be easily setup by any of these ways:

Once the instance is ready you have to set it up in Jenkins with the name of artifactory and the corresponding URL and credentials to allow Jenkins to upload artifacts.

This is done in the Artifactory section within Jenkins → Manage Jenkins → Configure System (see image below):

Artifactory config
comments powered by Disqus