Getting the right Jenkins build number using Python

One of the jobs in our CI pipeline is responsible for compiling, building and packing the code. The artifacts of the job is a directory on our storage with the build number and all the artifacts that are related to this build number.

For example: //storage/build_1000, //storage/build_1001 and etc.

There are other jobs that are triggered by a scheduler that takes the artifacts of the latest job and runs some tests on it.

The build job can run on multiple git branches so we wanted to create a mechanism that will allow the “testing” job take the latest build of a specific branch and not the latest job that was successful.

Unfortunately, I didn’t find a way in Jenkins to do that so I used some python code. Jenkins has a nice RESTful API that is wrapped in an easy to use python library:

1
pip install python-jenkins

For convenience, each one of the build jobs has the branch name as part of the display name, e.g. “#1000 (origin/master)”

The script looks like this:

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
#! /usr/bin/python

import re
import sys
import jenkins
import logging
from optparse import OptionParser

logger = logging.getLogger()
hdlr = logging.StreamHandler(sys.stderr)
formatter = logging.Formatter('%(asctime)s %(levelname)s\t%(message)s')
hdlr.setFormatter(formatter)
logger.addHandler(hdlr)
logger.setLevel(logging.DEBUG)

def connect(server, port, username, password):
    logger.info("Connecting to Jenkins at %s:%d (user: %s)" % (server, port, username))
    try:
        return jenkins.Jenkins("http://%s:%d" % (server, port), username=username, password=password)

    except Exception as e:
        logger.error("Connection failed: %s" % e.message)
        return None

def check_build(conn, number):
    logger.info("Checking build number %d" % number)
    info = conn.get_build_info(name=options.job, number=number)

    if info["building"]:
        logger.info("Build %d is still in progress - skipping" % number)
        return False

    if info["result"] != "SUCCESS":
        logger.info("Build %d result is %s - skipping" % (number, info["result"]))
        return False

    branch = re.findall("\((.*)\)", info["displayName"])[0]
    if branch != options.branch:
        logger.info("Build %d is on branch %s - skipping" % (number, branch))
        return False

    logger.info("Found build %d" % number)
    print("%d" % number)
    return True

if __name__ == "__main__":
    parser = OptionParser()
    parser.add_option("", "--server", dest="server", default="10.0.0.3", help="Jenkins server ip address")
    parser.add_option("", "--port", dest="port", default=8080, type="int", help="Jenkins server port")
    parser.add_option("", "--username", dest="username", default="automation", help="Jenkins user name")
    parser.add_option("", "--password", dest="password", default="password", help="Jenkins password")
    parser.add_option("-j", "--job", dest="job", default="", type="string", help="Jenkins job name")
    parser.add_option("-b", "--branch", dest="branch", default="", type="string", help="Git branch for the job")
    parser.add_option("-n", "--number", dest="number", type="int", help="Build number as a hint")
    (options, args) = parser.parse_args()

    conn = connect(options.server, options.port, options.username, options.password)
    if conn == None:
        sys.exit(2)

    if options.number:
        logger.info("Checking given build number %d" % options.number)
        found = check_build(conn, options.number)
        if found:
            sys.exit(0)
        else:
            logger.warn("The given build is not in the right branch - not using it.")

    logger.info("Looking for last successful job %s on branch %s" % (options.job, options.branch))

    logger.info("Getting builds for job %s" % options.job)
    job = conn.get_job_info(options.job)
    builds = job["builds"]
    for b in builds:
        found = check_build(conn, b["number"])
        if found:
            sys.exit(0)

    logger.error("Did not found any suitable build.")
    sys.exit(1)

The execution is simple as well:

1
./get_latest_successful_build.py --job "Builder" --branch "origin/master"

We can give the latest successful build as an hint and the script will check it first but it’s an optimization and not mandatory.

Run example from our CI lab (censored a bit):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ ./tools/get_latest_successful_build.py -j Builder -b origin/master -n 14151
2017-06-27 12:30:10,417 INFO    Connecting to Jenkins at 10.0.0.3:8080 (user: automation)
2017-06-27 12:30:10,417 INFO    Checking given build number 14151
2017-06-27 12:30:10,418 INFO    Checking build number 14151
2017-06-27 12:30:10,687 INFO    Build 14151 is on branch origin/feature1 - skipping
2017-06-27 12:30:10,688 WARNING The given build is not in the right branch - not using it.
2017-06-27 12:30:10,688 INFO    Looking for last successful job Builder on branch origin/master
2017-06-27 12:30:10,688 INFO    Getting builds for job Builder
2017-06-27 12:30:10,822 INFO    Checking build number 14154
2017-06-27 12:30:10,978 INFO    Build 14154 is still in progress - skipping
2017-06-27 12:30:10,978 INFO    Checking build number 14153
2017-06-27 12:30:11,132 INFO    Build 14153 is still in progress - skipping
2017-06-27 12:30:11,132 INFO    Checking build number 14152
2017-06-27 12:30:11,335 INFO    Found build 14152

Feel free using and modifying this script.
It can be downloaded from my github repository.

– Alexander