CI/CD #03. Jenkins: using Pipeline and proper Bash script to run Pytest.
Be Hai Nguyen
Posted on February 6, 2023
We write a proper and generic Bash script file to ⓵ create a virtual environment, ⓶ run editable install, and ⓷ run Pytest for all tests. Then we write a generic Jenkins Pipeline which will ⓵ clone a Python project GitHub repository, ⓶ call the Bash script file to do all the works.
To recap, I am running Jenkins 2.388 LTS on my Ubuntu 22.10, the name of this machine is HP-Pavilion-15
. I am accessing Jenkins and HP-Pavilion-15
from my Windows 10 machine using FireFox and ssh
, respectively. And Pipeline is the Jenkins documentation page which I have been working through.
I'm discussing converting the “Free style shell script” in CI/CD #01. Jenkins: manually clone a Python GitHub repo and run Pytest to a proper Bash script (file), and calling this Bash script file in a Jenkins Pipeline using the sh
step.
Jenkins is running under user jenkins
, and the group is also named jenkins
. This is apparent if we list contents of Jenkins' work directory, which is /var/lib/jenkins/workspace/
.
To make managing permissions easier, I will create the Bash script file under user jenkins
. To do that, we need to be able to log in, and ssh
to HP-Pavilion-15
under user jenkins
. We need to set its password:
$ sudo passwd jenkins
Once the password is set, we can use it to log in, via:
C:\>ssh jenkins@hp-pavilion-15
The original “Free style shell script” code we are converting to proper Bash script code is:
PYENV_HOME=$WORKSPACE/venv
# Delete previously built virtualenv
if [ -d $PYENV_HOME ]; then
rm -rf $PYENV_HOME
fi
# Create virtualenv and install necessary packages
virtualenv $PYENV_HOME
. $PYENV_HOME/bin/activate
$PYENV_HOME/bin/pip install -e .
$PYENV_HOME/bin/pytest
ssh
into the Ubuntu machine with:
C:\>ssh jenkins@hp-pavilion-15
The home directory of user jenkins
is /var/lib/jenkins
. Create the new sub-directory scripts/
under home directory, and create the pytest.sh
Bash script file under this /var/lib/jenkins/scripts
.
Content of /var/lib/jenkins/scripts/pytest.sh:
#!/bin/bash
#
# 04/01/2023.
#
# A generic script which create a clean virtual environment and run pytest.
#
# Intended to be used in a Jenkins GitHub project Pipeline as:
#
# sh("${JENKINS_HOME}/scripts/pytest.sh ${WORKSPACE}")
#
# The repo is cloned automatically by Jenkins. This script does the followings:
#
# 1. If virtual directory ${WORKSPACE}/venv exists, remove all of it.
#
# 2. Create virtual environment ${WORKSPACE}/venv, then activate it.
#
# 3. Run editable install for the project.
#
# 4. Finally run pytest.
#
if [ -z $1 ]; then
echo "Usage: ${0##*/} <dir>"
exit 1
fi
if [ ! -d $1 ]; then
echo "$1 does not exist."
exit 1
fi
virtual_dir=$1/venv
echo "Virtual directory $virtual_dir."
if [ -d $virtual_dir ]; then
rm -rf $virtual_dir
echo "$virtual_dir removed."
fi
virtualenv $virtual_dir
cd $1
. $virtual_dir/bin/activate
$virtual_dir/bin/pip install -e .
$virtual_dir/bin/pytest
Param $1
to pytest.sh
is basically the project directory: Jenkins environment variable ${WORKSPACE}
. Let's walk through the code:
- Lines 23-26 -- there must be a directory parameter supplied, otherwise exit with error code 1.
- Lines 28-31 -- the directory passed in must exist, otherwise exit with error code 1.
- Line 33 -- assemble the virtual directory absolute path under the passed in project directory. The name of the actual virtual directory is
venv
. - Line 35 -- print out the absolute virtual directory path.
- Lines 37-40 -- if the virtual directory exists already, remove it.
- Line 42 -- create the virtual environment using the absolute virtual directory.
- Line 44 -- move to the passed in directory. This directory is the parent of the actual virtual directory
venv
. - Line 46 -- activate the virtual environment.
- Line 47 -- run editable install.
- Line 48 -- run Pytest.
We have to set the Execute permission for the owner:
$ chmod u+x pytest.sh
This script is generic enough to do Pytest for any Python project via a Jenkins Pipeline. The documentation also states that, an advantage of this approach is that we can test, such as this Bash script, in isolation. And also, reducing the Pipeline code, making it easier to manage and follow.
Following is a generic Jenkins Pipeline code, which uses the above script:
pipeline {
agent any
stages {
stage('Pytest') {
steps {
sh("${JENKINS_HOME}/scripts/pytest.sh ${WORKSPACE}")
}
}
stage('Email Notification') {
steps {
mail(body: "${JOB_NAME}, build ${BUILD_NUMBER} Pytest completed.", subject: 'Pytest completed.', to: 'behai_nguyen@hotmail.com')
}
}
}
}
- Line 7 -- environment variable
${JENKINS_HOME}
is userjenkins
home directory,${WORKSPACE}
is the project directory. - Lines 13 -- send out a simple email whose body has the project name and the build number. This is just a simple email, and is discussed in CI/CD #02. Jenkins: basic email using your Gmail account.
Updated on 07/06/2023 -- Starts
The above Jenkins Pipeline code will work ONLY with Pipeline script from SCM
, discussed later. It does NOT ALWAYS work with GitHub project
, also discussed later.
GitHub project
will not clone the repo automatically! I made the mistake of thinking that it does: possibly because I already had the repo cloned on the local Jenkins workspace.
We need to include the stage to clone the repo Clone Git repo
. The Jenkins Pipeline code for GitHub project
, hence, is:
pipeline {
agent any
stages {
stage('Clone Git repo') {
steps {
git(
url: "https://github.com/behai-nguyen/bh_apistatus.git",
branch: "main",
changelog: false,
poll: false
)
}
}
stage('Pytest') {
steps {
sh("${JENKINS_HOME}/scripts/pytest.sh ${WORKSPACE}")
}
}
stage('Email Notification') {
steps {
mail(body: "${JOB_NAME}, build ${BUILD_NUMBER} Pytest completed.", subject: 'Pytest completed.', to: 'behai_nguyen@hotmail.com')
}
}
}
}
Also, in /var/lib/jenkins/scripts/pytest.sh
, the comment:
...
# The repo is cloned automatically by Jenkins. This script does the followings:
...
is no longer true. It should be changed to:
...
# This script does the followings:
...
Updated on 07/06/2023 -- Ends
On double quotes (""
) and single quotes (''
)
In the above Pipeline script, line 7 uses double quotes (""
). The first part of line 13 uses double quotes (""
), while the rest uses single quotes (''
). In Jenkins, this is known as String interpolation. There are security implications when it comes to credentials, the document recommends using single quotes. But if I used single quotes, the value of the environment variables would not come out, instead everything within the single quotes comes out as a literal string. I.e.:
'${JOB_NAME}, build ${BUILD_NUMBER} Pytest completed.'
The output is a literal string:
${JOB_NAME}, build ${BUILD_NUMBER} Pytest completed.
I don't quite understand this yet. I need to keep this in mind.
Let's do a Jenkins Pipeline to test this. The repo I am using is https://github.com/behai-nguyen/bh_apistatus.git/. The name of the new Pipeline project should be bh_apistatus
, on the Configure page, check GitHub project
, then fill in the address of the repo, as seen:
Scroll down to Pipeline, and select Pipeline script
, the content is the Pipeline script code above:
Please note that, if we save the above Pipeline script code to a file at the root level of the project, for example Jenkinsfile
, then check in this file as part of the source code, then we could select Pipeline script from SCM
, and fill in the rest of the fields as:
Script Path is a free type field, and is defaulted to Jenkinsfile
, if we use some other name, ensure to enter it correctly.
I have tested both implementations on more than one repo. I did not have any problem.
It's clearly better to store the Pipeline script code in a file, so that we can use our preference IDE to edit it, and also keeping a history of bug fixings and etc.
This post is yet another smaller step to understand Jenkins better. I did enjoy investigating this. I hope you find the information useful. Thank you for reading and stay safe as always.
Posted on February 6, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.