Jenkins Library with Example for T12s Team Rotor

If you find yourself copy-pasting code snippets and pipeline sections in different Jenkins pipelines, then it is time to ensure that the code is written correctly and properly. You usually have multiple software projects with a similar CI/CD setup. What I have often observed is duplicated code all over the Jenkins pipelines, even if all project structures are almost the same. In order to avoid duplicating the same code snippets in all Jenkins pipelines and make the pipeline code clean, I use a library that provides steps, such as awsCloudPackerBuildImage. This step has almost 200 lines of code, but using it makes the Jenkins pipeline definition much easier to maintain and understand.

A Jenkins library is a simple git repository with certain file/folder structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(root)
+- src                     # Groovy source files
|   +- org
|       +- foo
|           +- Bar.groovy  # for org.foo.Bar class
+- vars
|   +- foo.groovy          # for global 'foo' variable
|   +- foo.txt             # help for 'foo' variable
+- resources               # resource files (external libraries only)
|   +- org
|       +- foo
|           +- bar.json    # static helper data for org.foo.Bar

– source https://www.jenkins.io/doc/book/pipeline/shared-libraries/#directory-structure

At the time of writing the Jenkins library for Team Rotor looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
(root)
├── README.md
├── pom.xml
├── resources
├── src
│   └── io
│       └── t12s
│           └── automator
│               └── team
│                   └── rotor
│                       └── jenkins
│                           └── T12sTeamRotor.groovy
└── vars
  ├── t12sRotationDryRun.groovy
  ├── t12sRotationDryRun.md
  ├── t12sRotationFetchResults.groovy
  ├── t12sRotationFetchResults.md
  ├── t12sRotationRun.groovy
  └── t12sRotationRun.md

I usually write the core logic of the library in (different) classes and store them in src folder. For example the class T12sTeamRotor has the following content:

 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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package io.t12s.automator.team.rotor.jenkins


import groovy.json.JsonSlurperClassic

import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.time.Duration

class T12sTeamRotor implements Serializable {
  @Serial
  static final long serialVersionUID = 1L
  private static final String BASE_URI_STRING = "https://team-api.t12s-automator.app/resources"

  private final String baseUri
  private final String teamId
  private final String teamSecret
  private final JsonSlurperClassic jsonParser
  private final HttpClient httpClient
  private final def step
  private final Duration timeoutDuration = Duration.ofSeconds(45)

  T12sTeamRotor(final String baseUri, final step, final String teamId, final String teamSecret) {
    this.baseUri = baseUri
    this.teamId = teamId
    this.teamSecret = teamSecret
    this.step = step
    jsonParser = new JsonSlurperClassic()
    httpClient = HttpClient.newHttpClient()
  }

  T12sTeamRotor(final def step, final String teamId, final String teamSecret) {
    this(BASE_URI_STRING, step, teamId, teamSecret)
  }

  List<Map<String, ?>> fetchRotationRunResults(final String rotationId) {
    final def rotationResultsUri = URI.create(baseUri +
      "/team/" + encode(teamId) + "/rotation/" + encode(rotationId) +
      "/runResults?secret=" + encode(teamSecret))

    final def rotationResultsRequest = HttpRequest.newBuilder(rotationResultsUri).
      timeout(timeoutDuration).
      header("accept", "application/json").
      GET().
      build()

    final rotationResultsResponse = httpClient.send(rotationResultsRequest, HttpResponse.BodyHandlers.ofString())
    if (rotationResultsResponse.statusCode() == 200) {
      return jsonParser.parseText(rotationResultsResponse.body()) as List<Map<String, ?>>
    } else {
      step.echo("can not execute operation fetchRotationRunResults, statusCode: " + rotationResultsResponse.statusCode())
      step.echo(rotationResultsResponse.body())
      throw new IllegalStateException("can not execute operation fetchRotationRunResults: \n" + rotationResultsResponse.body())
    }
  }

  private Map<String, ?> internalRotate(final String rotationId, final String runMode) {
    final def rotateUri = URI.create(baseUri +
      "/team/" + encode(teamId) + "/rotation/" + encode(rotationId) +
      "/runResults?secret=" + encode(teamSecret) + "&saveMode=" + encode(runMode))

    final def rotateRequest = HttpRequest.newBuilder(rotateUri).
      timeout(timeoutDuration).
      header("accept", "application/json").
      POST(HttpRequest.BodyPublishers.noBody()).
      build()

    final rotateResponse = httpClient.send(rotateRequest, HttpResponse.BodyHandlers.ofString())
    if (rotateResponse.statusCode() == 200) {
      return jsonParser.parseText(rotateResponse.body()) as Map<String, ?>
    } else {
      step.echo("can not execute operation internalRotate, statusCode: " + rotateResponse.statusCode())
      step.echo(rotateResponse.body())
      throw new IllegalStateException("can not execute operation internalRotate: \n" + rotateResponse.body())
    }

  }

  Map<String, ?> rotate(final String rotationId) {
    return internalRotate(rotationId, 'Save')
  }

  Map<String, ?> rotateDryRun(final String rotationId) {
    return internalRotate(rotationId, 'DryRun')
  }

  private static String encode(final String text) {
    return URLEncoder.encode(text, StandardCharsets.UTF_8)
  }
}

Source code: GitHub > Jenkins library for T12S-Automator Team Rotor > T12sTeamRotor.groovy

The steps for Jenkins are written in the var folder. For example, the code for the t12sRotationRun step is stored in a file called t12sRotationRun.groovy. This file contains a script that performs the following actions:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package io.t12s.automator.team.rotor.jenkins

Map<String, ?> call(final Map<String, String> config = [:]) {
    final Map<String, String> defaultConfig = ['verbose':'false']
    final Map<String, String> finalConfig = ([:] << defaultConfig) << config

    final def rotor = new T12sTeamRotor(this, finalConfig.teamId, finalConfig.teamSecret)

    final def rotationResult = rotor.rotate(finalConfig.rotationId)

    if ('verbose' in finalConfig.keySet() && finalConfig.verbose == 'true') {
        println rotationResult
    }

    return rotationResult
}
Source code: GitHub > Jenkins library for T12S-Automator Team Rotor > t12sRotationRun.groovy

How to use the Team Rotor Jenkins library in your Jenkins pipeline

The Git repository of the Jenkins library for Team Rotor can be found at GitHub https://github.com/techeule/t12s-team-rotor-jenkins-library.

Before using a Jenkins library you need to define it in your Jenkins Server. To do so, you can follow the steps at the Jenkins official docs https://www.jenkins.io/doc/book/pipeline/shared-libraries/#using-libraries

Here is a simple Jenkins pipeline with two stages. Each stage start/run a rotation:

 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
@Library('t12s-team-rotor-jenkins-library') _ //  do not forget the underscore "_"

pipeline {
    stages {
        stage('Stand-Up Presenter') {
            steps {
                script {
                    runRotation('9EF37F04-0301-4D14-B69C-672DF6C9BAE4')
                }
            }
        }
        stage('Sprint Review Presenter') {
            steps {
                script {
                    runRotation('49A73189-2BB2-4DB5-9BBE-CA04231DE76E')
                }
            }
        }
    }
}

private void runRotation(final String rotationId) {
    // the following step "t12sRotationRun" is defined in the t12s-team-rotor-jenkins-library
    final def result = t12sRotationRun([
            'teamId'    : 'your-team-id',
            'teamSecret': 'your-team-secret',
            'rotationId': rotationId
    ])

    final def resultAsString = result.toString()
    echo resultAsString
    final def chosenName = (result.memberOrder[0] as String).capitalize()
    final def rotationName = (result.rotationName as String).capitalize()

    final def message = """
                    ${rotationName} is :magic_wand: *${chosenName}* :party:.
                    Decision was made at `${result.createdAt}`.
                    _Fallback_: If *${chosenName}* can not do it, the next persons in line would be
                    *`${result.memberOrder.subList(1, result.memberOrder.size())}`*.
                    The order of the fallback persons could _change_ on next rotation run.
                    """.stripIndent()

    currentBuild.description = "${rotationName}: <b>${chosenName}</b>"
    slackSend(channel: 'your-slack-channel-id_name', color: '#00FF00', message: message)
}

Resources: