Skip to main content
Blog

How to start with declarative Jenkins pipelines

By 16 januari 2020No Comments

So, we have been using pipelines-as-code in Jenkins for a while now and we generally are using scripted pipelines, Groovy scripts that run in Jenkins. Very flexible, but it is missing something for me. While we were used to the webforms of Jenkins before pipelines, these constructs aren’t always available in scripted pipelines. Enter declarative pipelines. These are pipelines-as-code as well, but resemble the old webforms much more closely. Things like post build step constructs are available here and don’t need try-catch-finally blocks like with scripted pipelines. You may also want something that is closer to several different technologies, such as Kubernetes declarations or even Terraform code. This blog is for people, like me, looking for something else then the scripted pipelines and are just starting out with declarative pipelines. It will take you through the basics and leave you room to explore more deeply.

Building blocks

There are several building blocks in declarative pipelines:

  • A block is just some code with a beginning and an end. The term block is usually used to describe the pipeline block, which contains all the code.
  • Sections are pieces of code that contain items that need to be executed at some point during your build. The items may include directives, steps, and conditionals. Stages wrap all the steps that should be executed, in the different phases of the build. The basic types of section are:
    • stages
    • steps
    • posts 
  • Steps are a wrappers for a set of DSL steps within the stage. They can separate different items within a stage. The post section wraps around the steps that can be done when a stage or the entire pipeline is done.
  • Directives can be used to define values, configure behavior or specify actions to be done. Examples of directives are
    • agents
    • triggers
    • stages

Conditionals

In scripted pipelines, we often use if statements to see if the build is from a pull request, or to see which branch is build so other steps can be done for master and a feature branch. In declarative pipelines, we have when conditions. These when conditions have the following logical operators and are in the respective form.

and allOf{}
or anyOf{}
not not{}

You could check branch names, environment names and expressions. Equals can be used to check 2 values, whilst changeRequest can be used to check if the build was triggered from a pull request. BuildingTag and Tag can be used to check if the build is for a tag or a specific tag respectively.

when { // This is strictly speaking a directive and is optional
  allOf { // also anyOf{} and not{}
    branch 'master'
    environment name: "BUILD_CONFIG", value: "DEBUG"
    expression {
      params.BUILD_TYPE == 'RELEASE
    }
  }
}

Parallel steps

So you have some steps that you can do at the same time. Something like unit tests on different environments or deploying to different servers or who knows what you want to do at the same time. In parallel steps, you can define the agents where you want to run these steps. Post sections can also be added to the stages, just like with non parallel stages.

stage('Run Tests') {
  parallel {
    stage('Test On Windows') {
      steps {
        bat "run-tests.bat"
      }
    }
    stage('Test On Linux') {
      steps {
        sh "run-tests.sh"
      }
    }
  }
}

Libraries

With jenkins pipelines, it has become possible to put common code in libraries. These libraries can live elsewhere, say in a different git repository. But how do you use them? In scripted pipelines you would use the @Library annotation. In declarative pipelines, you use the libraries directive. Each library is declared in a lib statement and the syntax for this construct is similar to the scripted version: <libraryName@version>. It still is possible though to use the @Library annotation.

...
agent any
libraries {
  lib("libA@tag")
  lib("libB")
}
...

So now you’ve got the library loaded you want to use it. In the library, you have a file with the name of the step and a call method inside that file. In your pipeline, you can use the name of the file as the step

...
steps {
  yourStepName parameter
}
...

Post steps

While working with scripted pipelines, you might want to do some post processing. This is usually done in a try-catch-finally block. This is often used for notification if a build failes for example. In a declarative style pipeline, we have the post section. In the post section, you have an always directive. This directive will always do what you put in it, every time a build is done. The other directives changed, failure and success are only run when the build has the right status for that directive.

post {
  always{
    ... // Put any git, sh, echo, etc here
  }
  changed{
    ...
  }
  failure{
    ...
  }
  success{
    ...
  }
  fixed{
    ...
  }
  regression{
    ...
  }
}

Wrapping up

I haven’t gone in to detail about everything here, but it should give you a good idea on how a declarative pipeline is build up. There are new things being added to the declarative pipelines on a regular basis, it’s evolving every day. Jenkins also has a great code generator in Blue Ocean. It’s a great way to get started. If you want to read more, go to https://jenkins.io/doc/book/pipeline/syntax/. Putting it all together your pipeline could look  something like this:

post {
pipeline {
  agent any 
  environment ...
  tools ...
  options ...
  triggers ...
  parameters ...
  libraries ...
  stages {
    stage('nameA') {
      agent any
      environment ...
      tools ...
      when {
        allOf { // also anyOf{} and not{}
      branch 'master'
          environment name: "BUILD_CONFIG", value: "DEBUG"
          expression {params.BUILD_TYPE == 'RELEASE'}
        }
    
        branch 'foo'
      }
      steps { 
        ... // Put any git, sh, echo, etc here
      }
      post { 
        ... // Put any git, sh, echo, etc here
      }
    }
    stage('nameB') { 
      parallel { // TODO read chapter 3 and put this right
        steps { 
          ... // Put any git, sh, echo, etc here
        }
      }
    }
    post { // This is a section
      always{ 
        ... // Put any git, sh, echo, etc here
      }
      changed{
        ...
      }
      failure{ 
        ... // Put any git, sh, echo, etc here
      }
      success{ 
        ... // Put any git, sh, echo, etc here
      }
    }
  }
}