Table of Contents
- 2.1 Introduction and demonstration scenario
- 2.2 The Groovy console
- 2.3 Demo: The Groovy console
- 2.4 Data types
- 2.5 Demo: Data types
- 2.6 Control structures
- 2.7 Demonstration: Control structures
- 2.8 Subroutines
- 2.9 Demonstration: Subroutines
- 2.10 Classes and objects
- 2.11 Demonstration: Classes and objects
- 2.12 Legacy
- 2.13 Demo: Legacy
- 2.14 Working with external packages
- 2.15 Demo: External packages
- 2.16 Module 2 Summary
- 3.1 How Jenkins and Groovy Work Together
- 3.2 Demo: Configuring the Groovy plugin
- 3.3 Understanding System and Standard Groovy steps
- 3.4 Demo: A Groovy System script
- 3.5 Script types summary
- 3.6 Run Groovy scripts on startup
- 3.7 Demo: Groovy startup scripts
- 3.8 Exception handling in Groovy
- 3.9 Demo: Exception handling
- 3.10 Importing external libraries with Grape
- 3.11 Module 3 Summary
- 4.1 The problem with builds
- 4.2 Demo: Exploring Jenkins Internals
- 4.3 The solution: the Jenkinsfile
- 4.4 Demonstration: Configure and build with a Jenkinsfile
- 4.5 Make the Jenkinsfile do real work
- 4.6 Demo: A real Jenkinsfile pipeline
- 4.7 Comparison with Azure DevOps
- 4.8 Module 4 Summary
- 5.1 Introduction
- 5.2 Reuse Groovy code in Jenkins
- 5.3 Demo: Global Shared Libraries
- 5.4 How Groovy is executed by Jenkins (CPS)
- 5.5 Demonstration: Correcting our library with @NonCPS
- 5.6 Integrate changes into Release Notes
- 5.7 Demo: Improving Release Notes
- 5.8 Shared Pipelines
- 5.9 Demo: Create a shared pipeline
- 5.10 Shared Libraries Summary
- 5.11 Working with plugins
- 5.12 Demo: Manage plugins by script
- 5.13 Script pattern for plugins
- 5.14 Demonstration: Enforcing a whitelist of plugins
- 5.15 Important warning
- 6.1 Introduction
- 6.2 Demonstration: Working with users
- 6.3 Working with credentials
- 6.4 Create credentials by script
- 6.5 Demonstration: Manage credentials by script
- 6.6 Course summary
1. Course Overview
Jenkins is a powerful build server, and the main reason for the existence of any build server is to automate processes. When you need to work outside of the standard use cases imagined by the Jenkins designers, the first tool of choice is Groovy: a powerful scripting language based on Java.
The main themes covered in this course are:
- The fundamentals of the Groovy language
- How Groovy works with Jenkins
- Automate plugins and credentials
- Share code between scripts and pipelines
At the end of this course, you will have the skills and knowledge to automate important tasks with Jenkins, and to leverage Groovy to version your builds in a version control system.
Prerequisites: Be familiar with Jenkins, build engineering and software development in general.
Recommended progression after this course: Advanced Jenkins administration, software deployment, advanced Jenkins pipelines.
2. Learning the fundamentals of the Groovy language
2.1 Introduction and demonstration scenario
This course is aimed at DevOps professionals who already master Jenkins and who want to automate repetitive or problematic tasks. The automation language of choice in Jenkins is Groovy, a Java scripting language that compiles to bytecode.
The main scenario of the training is that of a DevOps professional working for the fictitious company Globomantics, seeking to automate recurring Jenkins operations with Groovy.
2.2 The Groovy console
The first step to working with Groovy is to have a validation environment. Jenkins itself has an internal Groovy console, but it’s useful to have something even more lightweight — a console entirely separate from Jenkins and build engineering.
Apache (the organization that maintains Groovy) provides such a tool. This is a Java application, more precisely a Swing application (GUI toolkit for Java). It is available for download on the official Groovy website.
Advantages of the Groovy standalone console:
- Lightness: no need for Jenkins to test scripts
- Isolated development environment, ideal for learning
- Author’s metaphor: a “kiddie pool” to familiarize yourself with Groovy before going further
2.3 Demonstration: The Groovy console
Demonstration steps:
- Navigate to the Groovy distribution download link
- Extract and launch the console
- Write a simple Groovy script
- Run it and check the result
Example of first Groovy script:
def x = 5
x += 5
println x
assert x == 10
defmeans “define”: declares a variablexwith the value 5- We add 5 to this value
- We display the result and we
assertthat the value is indeed 10
If the value is not correct, an exception is thrown:
def x = 5
x += 5
println x
assert x == 11 : "Value wasn't eleven"
// Lance une exception : AssertionError: Value wasn't eleven
The launcher file on Windows is called groovyConsole.bat. The console can be launched from a command window in the installation directory.
2.4 Data types
Groovy is an optionally typed language: you can either use the defined Java primitives, or let the Groovy runtime deduce the type from the context, or declare a variable without any keyword.
The four essential data types to master first in any scripting language are:
| Type | Keyword | Description |
|---|---|---|
| String | String | Text |
| Whole | int | Integer |
| Decimal number | BigDecimal / float | Number with comma |
| Boolean | Boolean | True or false |
These four types account for about 90% of scripting work.
Automatic imports in Groovy:
Any Groovy script automatically benefits from the following imports (the equivalent of having them at the head of the file):
import java.io.*
import java.lang.*
import java.math.BigDecimal
import java.math.BigInteger
import java.net.*
import java.util.*
import groovy.lang.*
import groovy.util.*
2.5 Demonstration: Data Types
String name = "Chris B. Behrens";
int courseCount = 14;
BigDecimal salary = 99999999.99;
Boolean isProgrammer = true;
println name + " has created " + courseCount + " courses.";
println name + " is a programmer? " + isProgrammer.toString().capitalize();
println name + " wishes his salary was $" + salary;
Important points:
- String concatenation operator is
+, like in Java and JavaScript - Groovy runtime automatically coerces non-String types to String during concatenation
- For
Boolean, call.toString()to get “true”/“false”, then.capitalize()to get “True” - To control the format of
float, we can use theformatmethod of the String prototype - Scientific notation can be problematic with floats: use
BigDecimalfor precision
// Problème avec float
float salary = 99999999.99 // peut afficher en notation scientifique
// Solution : utiliser BigDecimal
BigDecimal salary = 99999999.99 // affichage correct
2.6 Control structures
To perform meaningful actions in a script, we need control structures allowing the code to make decisions.
If/else structure:
if (condition) {
// code
} else {
// code
}
Classic for loop:
for (int i = 0; i < 10; i++) {
println i
}
For-in loop (to iterate over a collection):
for (String singer : singers) {
println singer
}
Groovy’s for-in loop looks like Java, but differs from C#: the iterator and array are separated by :.
Each loop (shortcut):
// Avec une variable nommée
singers.each { x -> println(x) }
// Avec le mot-clé implicite 'it'
singers.each { println(it) }
The .each method is an elegant shortcut for looping over collections.
2.7 Demonstration: Control Structures
int courseCount = 14;
Boolean isProgrammer = true;
String[] singers = ["Bob", "George", "Jeff", "Roy", "Tom"]
// if/else
if (isProgrammer) {
println "He's a programmer, alright"
} else {
println "Not a programmer, tho"
}
// Boucle for classique
for (int i = 0; i < courseCount; i++) {
println "Chris made course " + (i+1) + "!!!"
}
// Boucle for-in
for (String singer : singers) {
println singer
}
// Méthode each avec variable nommée
singers.each { x -> println(x) }
// Méthode each avec 'it' implicite
singers.each { println(it) }
Console tip: In the Groovy console, you can:
- Select a portion of code and use Script > Run Selection to run only that selection
- Comment a line with
//(Java style) - Comment out a block with
/* ... */ - Use Edit > Comment to automatically comment on a selection
2.8 Subroutines
In any scripting work, you will quickly want to compartmentalize the code into subroutines, whether for reuse or simply to better delineate the code’s responsibilities.
In Groovy (as in Java and C#), there are two types of subroutines:
- Functions: return a value
- Methods: do not return a value (return type
void)
Groovy simply defines a method as a function with a void return type.
Example of use case: In builds, we want to generate sets of credentials from user names. We want to create a simple username: the first letter of the first name + the full last name.
2.9 Demonstration: Subroutines
Function returning a value:
String getUserName(String firstName, String lastName) {
return firstName.substring(0, 1).toLowerCase() + lastName.toLowerCase();
}
assert getUserName("Chris", "Behrens") == "cbehrens" : "getUserName isn't working"
println(getUserName("Chris", "Behrens"))
Display void method:
void printCredential(String cred) {
println("UserName is ${cred}");
}
printCredential(getUserName("Chris", "Behrens"));
The ${...} syntax (string interpolation): Groovy evaluates content between ${...} as code, avoiding concatenations with + and quotes.
Complete example with loop:
String getUserName(String firstName, String lastName) {
return firstName.substring(0, 1).toLowerCase() + lastName.toLowerCase();
}
assert getUserName("Chris", "Behrens") == "cbehrens" : "getUserName isn't working"
void printCredential(String cred) {
println("UserName is ${cred}");
}
String[] firstNames = ["Bob", "Jeff", "Roy", "George", "Tom"]
String[] lastNames = ["Dylan", "Lynne", "Orbison", "Harrison", "Petty"]
for (int i = 0; i < firstNames.size(); i++) {
printCredential(getUserName(firstNames[i], lastNames[i]));
}
2.10 Classes and objects
We quickly reach the limit of subroutines alone. When you have two parallel tables (first names, last names), you risk problems if the tables have different sizes. The solution is to encapsulate the data in a class.
Classes in Groovy are very similar to Java and C#:
- We declare a class with the keyword
class - We declare instance properties almost identically
- Members can be declared
private(invisible from the outside) andpublic
Why make getUserName private? To avoid the possibility of creating a User object and calling this method with arbitrary values from outside — which would produce confusing code. By making it private, we force the user to create a User object, enter its properties, then obtain the UserName via a public property.
2.11 Demonstration: Classes and objects
Version 1 — Class with parallel tables:
class User {
String lastName;
String firstName;
public String UserName() {
return getUserName(this.firstName, this.lastName);
}
private String getUserName(String firstName, String lastName) {
return firstName.substring(0, 1).toLowerCase() + lastName.toLowerCase();
}
}
String[] firstNames = ["Bob", "Jeff", "Roy", "George", "Tom"]
String[] lastNames = ["Dylan", "Lynne", "Orbison", "Harrison", "Petty"]
for (int i = 0; i < firstNames.size(); i++) {
User u = new User(firstName: firstNames[i], lastName: lastNames[i]);
println("UserName is ${u.UserName()}");
}
Version 2 — User object array:
class User {
String lastName;
String firstName;
public String UserName() {
return getUserName(this.firstName, this.lastName);
}
private String getUserName(String firstName, String lastName) {
return firstName.substring(0, 1).toLowerCase() + lastName.toLowerCase();
}
}
User[] users = [
new User(firstName: "Bob", lastName: "Dylan"),
new User(firstName: "Jeff", lastName: "Lynne"),
new User(firstName: "Roy", lastName: "Orbison"),
new User(firstName: "George", lastName: "Harrison"),
new User(firstName: "Tom", lastName: "Petty")
];
users.each(user -> println("UserName is ${user.UserName()}"));
Note on Groovy initialization syntax: new User(firstName: "Bob", lastName: "Dylan") uses Groovy map syntax to initialize the properties of an object.
2.12 Legacy
We can go one level deeper with legacy. Groovy fully supports traditional inheritance with interfaces, such as Java.
Abstract classes vs interfaces:
- An interface simply describes the form of the work (the contract) and relies on the implementer to provide the code
- An abstract class contains an actual implementation — code that executes — but cannot be instantiated directly
Analogy: Nobody has the abstract notion of a car — we have a concrete implementation of this abstract idea. A 2008 Pontiac Vibe and a Tesla Roadster are very different, but they both share the abstract idea of “car.”
Value of abstract classes: Placing common code between implementers in the abstract class avoids copy-pasting, which is a very harmful practice in programming.
Scenario: Globomantics has an entertainment division. We want to represent artists and producers, who share common properties (last name, first name, username), but also have their own characteristics.
2.13 Demo: Legacy
Abstract class with two implementors:
abstract class User {
String lastName;
String firstName;
public String UserName() {
return getUserName(this.firstName, this.lastName);
}
private String getUserName(String firstName, String lastName) {
return firstName.substring(0, 1).toLowerCase() + lastName.toLowerCase();
}
}
public class Artist extends User {
public String[] Songs;
}
public class Producer extends User {
public void Produce() {
println("Album COMPLETE");
};
}
Use with polymorphism:
User[] users = [
new Artist(firstName: "Bob", lastName: "Dylan", Songs: ["It's Alright Ma"]),
new Producer(firstName: "Jeff", lastName: "Lynne"),
new Artist(firstName: "Roy", lastName: "Orbison", Songs: ["Crying"]),
new Artist(firstName: "George", lastName: "Harrison", Songs: ["Wah Wah"]),
new Artist(firstName: "Tom", lastName: "Petty", Songs: ["Running Down the Dream"])
];
users.each { user ->
if (user instanceof Artist) {
println("UserName is ${user.UserName()}")
user.Songs.each {
println("${it}");
}
} else {
user.Produce();
}
};
Important observations:
- The array is of type
User[]but can containArtistandProducer(polymorphism) - The
extendskeyword allows inheritance instanceofallows you to check the real type of an object at runtime- Attempt to instantiate
Userdirectly → error: cannot create an instance from the abstract class User
2.14 Working with external packages
At this stage, we have mastered what we create ourselves in Groovy. But it lacks the ability to interact with external packages. If everything has to be written yourself, a scripting language is practically useless in the modern world.
Automatic Groovy imports:
Any Groovy script automatically includes the following packages:
import java.io.*
import java.lang.*
import java.math.BigDecimal
import java.math.BigInteger
import java.net.*
import java.util.*
import groovy.lang.*
import groovy.util.*
For packages outside of these defaults: we use a classic import instruction.
In Jenkins scripts (scripting console), we benefit from additional imports which expose the namespaces of Jenkins internals:
import jenkins.model.*
import hudson.model.*
// ... etc.
2.15 Demo: External packages
Scenario: Load a JSON file representing musical data (music.json), parse it with JsonSlurper, and loop over the data to display it.
Example of the music.json file:
[
{
"name": "Doctor Oidar",
"albums": [
{
"title": "The Queen of Lambs",
"songs": [
{ "title": "Onion", "length": "5:15" },
{ "title": "Good Evening, Mr Sparrow", "length": "4:41" },
{ "title": "Slowly He Turned", "length": "4:27" }
]
}
]
}
]
Loading and parsing script:
import groovy.json.JsonSlurper
String filePath = "C:\\Code\\GroovyJenkins\\music.json";
def jsonSlurper = new JsonSlurper()
ArrayList data = jsonSlurper.parse(new File(filePath))
println(data.getClass())
for (artist : data) {
println(artist.name);
for (album : artist.albums) {
println('\t' + album.title);
}
}
Key points:
- Without the
import, Groovy does not knowJsonSlurper→ errorunable to resolve class JsonSlurper JsonSlurperreturns anArrayList— we can usedefhere because the type is obviousprintln(data.getClass())shows the actual type of the result (useful for debugging)
2.16 Module 2 Summary
This module established the foundations of the Groovy language:
- The Groovy console as a lightweight testing environment
- Data types: String, int, BigDecimal, Boolean
- Control structures: if/else, for, for-in, each
- Subroutines: functions with return value, void methods
- Class definition with private/public properties and methods
- Inheritance with abstract classes and concrete classes
- external packages and imports
3. Jenkins and Groovy together
3.1 How Jenkins and Groovy work together
Jenkins is a Java application through and through. This means that Jenkins can run anywhere a JVM (Java Virtual Machine) is available.
Important clarification on the “Groovy runtime”: There is no “Groovy runtime” per se. Groovy is simply another way to generate bytecode for the Java runtime. Just as Java compiles Java code to bytecode, Groovy compiles Groovy code to bytecode via the Groovy compiler. This means that wherever Java can run, Groovy can run too — this is the basis of the Groovy/Jenkins interaction.
The Jenkins plugins template: Jenkins uses a plugin model: applications that conform to the plugin standard and implement extension points can extend the capabilities of Jenkins (run other compilers, modify the Jenkins UI, etc.).
Groovy support is implemented as a plugin to be installed and configured via the Jenkins interface.
3.2 Demonstration: Configuring the Groovy plugin
Steps:
- Log in as Jenkins administrator
- Go to Manage Jenkins > Manage Plugins
- In the Available tab, search for
Groovy - Install separate Groovy plugin (separate from built-in script console)
- Select Download now, and install after restart
The built-in script console: Available in Manage Jenkins > Script Console without additional plugin — this is a native Jenkins feature. You can run Groovy scripts directly there.
Example script in the Script Console (list installed plugins):
Jenkins.instance.pluginManager.plugins.each {
println "${it.shortName} - ${it.version}"
}
What the Groovy plugin adds: Without the plugin, no Groovy build step appears in the configuration of a freestyle job. After installing the plugin, two new build steps appear:
- Execute Groovy script (standard script)
- Execute system Groovy script (system script)
3.3 Understanding System and Standard Groovy steps
The distinction between the two types of steps is fundamental:
| Type | Execution | Access to Jenkins internals |
|---|---|---|
| System Groovy | In the JVM with Jenkins | ✅ Yes |
| Groovy Standard | In a separate (forked) JVM | ❌ No |
System scripts: Executed inside the same JVM as Jenkins, they have access to all Jenkins internals (plugins, jobs, build info, etc.).
Standard scripts: Useful for tasks that do not need access to Jenkins internals, for example transforming text into decisions in a build.
Additional imports in Script Console and script steps: In the script console and in the script steps, imports are automatically added which expose the Jenkins namespaces:
import jenkins.model.*
import hudson.model.*
// et autres namespaces Jenkins
Without these imports (in the external Groovy console), a command like Jenkins.instance will fail because the compiler does not know jenkins.model.Jenkins.
3.4 Demonstration: A Groovy System script
Example: attempt from external console (fails):
// Erreur : No such property: Jenkins
Jenkins.instance.pluginManager.plugins.each { println it }
Even with the import:
import jenkins.model.Jenkins
Jenkins.instance.pluginManager.plugins.each { println it }
// Erreur : unable to resolve class jenkins.model.Jenkins
The import is correct, but the class is not on the external console classpath.
From the Jenkins Console Script (works):
// L'import est implicite, le classpath est géré en arrière-plan
Jenkins.instance.pluginManager.plugins.each { println it }
Script to trigger another Jenkins build:
// Récupérer le job TEST et déclencher un build
Jenkins.instance.getJob("TEST").scheduleBuild2(0)
Configure from the script:
// Avec le nom du job en dur
def jobName = "TEST"
Jenkins.instance.getJob(jobName).scheduleBuild2(0)
Configure from the build parameters:
// Récupérer un paramètre du build en cours
def jobName = build.buildVariableResolver.resolve("JOB_NAME")
Jenkins.instance.getJob(jobName).scheduleBuild2(0)
Jenkins API Documentation: Available at javadoc.jenkins-ci.org. This doc is automatically generated from the source code (Javadoc). We can navigate to core to find the jenkins.model package and the Jenkins object.
Historical note: The hudson package is present for compatibility reasons — Hudson was the name for Jenkins before Oracle filed a copyright suit.
3.5 Summary of script types
Which type to use?
- A system script can do everything a standard script can do, plus access to Jenkins internals
- Without access to Jenkins internals, automation possibilities are very limited
- But: system scripts require administrator approval (high security risk)
- Rule: Use standard scripts if possible, as they carry far fewer security risks than the near-omnipotent permissions of a system script
3.6 Run Groovy scripts on startup
It can be very useful to have certain scripts run immediately after Jenkins starts. This allows you to have your configuration as code — a good practice that makes the configuration portable and applicable to multiple instances.
Mechanism: Jenkins automatically executes Groovy scripts that it finds in a directory named init.groovy.d. With multiple scripts in this directory, they are executed in alphabetical order.
Recommended naming convention:
init.groovy.d/
1_config.groovy
2_setup_plugins.groovy
3_create_users.groovy
3.7 Demo: Groovy Startup Scripts
Script 1_config.groovy:
import jenkins.model.*;
import java.util.logging.Logger
Logger logger = Logger.getLogger("")
logger.info "Executing init script"
Jenkins.instance.setDisableRememberMe(true)
Jenkins.instance.setSystemMessage('Jenkins Server - Automating Jenkins with Groovy')
Jenkins.instance.save()
logger.info "Init script complete"
What this script does:
- Create a logger to write to Jenkins logs
- Disables the “Remember me” checkbox on the login page
- Sets a system message identifying the server
- Save changes
Deployment (with Docker):
# Créer le répertoire init.groovy.d dans le conteneur Jenkins
docker exec jenkins mkdir -p /var/jenkins_home/init.groovy.d
# Copier le script depuis le système local vers le conteneur
docker cp 1_config.groovy jenkins:/var/jenkins_home/init.groovy.d/
# Redémarrer Jenkins (via l'URL)
# Naviguer vers http://jenkins-host:port/restart
Checking:
- The “Remember me” box no longer appears on the login page
- System message is visible after login
- In Manage Jenkins > System Log, search for
1_config.groovyto see the script logs
3.8 Exception handling in Groovy
Now that we’re working with real, useful scripts, we need to introduce high-level language features like exception handling.
Risk scenario: In a startup script that runs just after Jenkins is initialized, if something goes wrong (e.g. a configuration document on a network share is inaccessible), the script may crash and Jenkins cannot start.
Philosophical question: Should we prevent Jenkins from starting if the configuration document is missing, or is it better to start Jenkins and notify that the document is missing?
Basic principle:
“Exceptions must be exceptional”
If we represent the behavior of a function on an axis:
- Left: the desired state (traditional control flow: if/else, loops, return values)
- Right: Undesirable state (exceptional behavior)
Exceptions are made for truly unexpected situations, not for ordinary control flow.
Try-catch-finally structure:
try {
// code qui peut échouer
} catch (SpecificException ex) {
// gestion d'une exception spécifique
} catch (ex) {
// gestion générique de toute autre exception
} finally {
// code exécuté dans tous les cas
}
3.9 Demonstration: Exception handling
Basic example (bad practice — swallowing exception):
try {
def x = 1/0;
println(x);
} catch(ex) {
// bloc vide = exception "avalée" silencieusement = mauvaise pratique
}
Acceptable minimum:
try {
def x = 1/0;
println(x);
} catch(ex) {
println(ex);
// Affiche : java.lang.ArithmeticException: Division by zero
}
With stack trace:
try {
def x = 1/0;
} catch(ex) {
println(ex.getStackTrace());
}
Recommended pattern — Management at the correct stack level:
BigDecimal value;
try {
value = divide(1);
} catch (ArithmeticException ex) {
value = 0;
} catch (ex) {
value = 2;
}
BigDecimal divide(int x) {
throw new RuntimeException("other exception");
def y = x / 0;
return y;
}
Explanation: By moving exception handling to the calling level (rather than inside the function), the calling function can control what happens. If the exception is ArithmeticException, we return 0. If it is another exception, we return 2.
Throw your own exception:
throw new RuntimeException("Description de l'erreur")
3.10 Importing external libraries with Grape
For truly external libraries (not included in default imports), Groovy has Grape — a dependency manager analogous to npm (Node) or NuGet (.NET).
@Grab syntax:
@Grab(group='org.springframework', module='spring-orm', version='5.2.4.RELEASE')
import org.springframework.jdbc.core.JdbcTemplate
Sources:
- npm uses npmjs.org
- NuGet uses nuget.org
- Grape uses mvnrepository.com (mvn = Maven, tool parallel to Jenkins for application management)
Operation:
- When executing
@Grab, Groovy contacts mvnrepository.com - It resolves the package by
group,module, andversion - It downloads the binary
- The library is then available via
import
Security and connectivity implications: It is necessary to ensure that the execution environment has access to the internet, or configure an internal Maven mirror.
Important: @Grab does not work in Jenkins pipeline scripts. For external libraries in pipelines, a corresponding plugin must be installed via a startup script.
3.11 Module 3 Summary
In this module, we have:
- Learned to automatically trigger a Jenkins build via Groovy script
- Understood how to pass parameters into a script (hardcoded, from script, from build parameters)
- Discussed security considerations
- Seen running scripts at startup with
init.groovy.d - Created a useful startup script for server configuration
- Explored exception handling (try/catch/finally)
- Imported libraries with Grape (@Grab)
4. Create builds with Groovy
4.1 The problem with builds
If you run Jenkins like most organizations, there are several issues lurking in the background:
Problem A — Lack of build versioning:
After hundreds of hours automating processes with Jenkins, the builds contribute enormously to the organization. But are they in a version control system? Backups of the Jenkins system exist, and perhaps even the Job Configuration History plugin is installed (which allows you to see changes to a job over time) — but this is not true version control.
All important company intellectual property must be in a version control system, including builds.
Issue B — Environment Specific Builds:
Typical scenario:
- We are working on a new library on a branch feature
- A commit triggers a build to a dedicated test environment
- We must check the library in the build
- Editing the Jenkins build to include the new library causes issues if another developer triggers a build from their branch
The problem: the same build is used by everyone, but each developer has different needs at any given time.
The solution to both problems: store the build definition in a file in the code repository.
4.2 Demo: Exploring Jenkins Internals
Jenkins Home directory structure:
/var/jenkins_home/
├── config.xml # Configuration globale Jenkins
├── jobs/ # Dossier de tous les jobs
│ ├── TEST/
│ │ └── config.xml # Configuration du job TEST
│ └── TEST2/
│ ├── config.xml # Configuration du job TEST2
│ ├── nextBuildNumber
│ └── builds/
│ ├── 1/
│ │ ├── changelog.xml
│ │ └── log
│ └── 2/
└── ...
What config.xml contains:
An XML representation of the entire build configuration. It is this file that we modify via the Jenkins UI. The Job Configuration History plugin shows diffs of this file.
Key point: All this content is already in a format (or close to a format) that could be stored in a version control system. The key asset that drives everything is config.xml.
4.3 The solution: the Jenkinsfile
The solution is an alternative build format created specifically for this purpose: the Jenkinsfile. Its format is, unsurprisingly, Groovy.
Two types of Jenkinsfile:
| Type | Complexity | Flexibility |
|---|---|---|
| Declarative | Simpler | Less flexible |
| Scripted | More complex | More flexible ✅ |
The course focuses on the scripted format because it is more powerful.
Jenkinsfile scripted example:
node {
stage('Build') {
echo 'Building....'
}
stage('Test') {
echo 'Testing....'
}
stage('Deploy') {
echo 'Deploying....'
}
}
Anatomy of the Jenkinsfile:
node: Defines an executor and a workspace for Jenkins- Left blank = use any build agent
- Specify a label = use an agent with this label (ex:
node('windows'),node('linux')) stage: Organizes the pipeline stages in a linear (sequential) manner- Within each internship, real work is carried out
Multiple Build Agents:
If we have Windows (for .NET Framework builds) and Linux (for Android builds) agents, the label in node() allows execution to be restricted to an agent with the right capabilities.
Snippet Generator: In the Pipeline configuration, the Pipeline Syntax link opens a very useful snippet generator, which generates Groovy code for common steps (trigger a build, run a Windows batch, etc.).
4.4 Demonstration: Configure and build with a Jenkinsfile
Create a Jenkins Pipeline from GitHub:
- In Jenkins: New Item → Name the pipeline (ex:
HelloPipeline) → Select Pipeline → OK - Scroll down to the Pipeline section
- In Definition, select Pipeline script from SCM
- Select Git and enter the GitHub repo URL
- Specify the file name (
Jenkinsfile) - Save and trigger a build
Simple Jenkinsfile example in GitHub:
node {
stage('Build') {
echo 'Building....'
}
stage('Test') {
echo 'Testing....'
}
stage('Deploy') {
echo 'Deploying....'
}
}
The Jenkinsfile has no extension. The file is literally named Jenkinsfile.
More advanced scripted pipeline example (from documentation):
node {
stage('Preparation') {
checkout scm
}
stage('Build') {
if (isUnix()) {
sh "mvn -Dmaven.test.failure.ignore clean package"
} else {
bat "mvn -Dmaven.test.failure.ignore clean package"
}
}
stage('Results') {
junit '**/target/surefire-reports/TEST-*.xml'
archiveArtifacts 'target/*.jar'
}
}
4.5 Make the Jenkinsfile do real work
Automatic checkout from the SCM: When a pipeline is configured to trigger from an SCM, Jenkins automatically fetches all contents of the repo into the workspace. There is therefore generally no need to execute an explicit SCM command in the script.
Workspace behavior:
If we look at the workspace of a pipeline (eg: HelloPipeline) in the Jenkins directory, we find all the contents of the repo (not just the Jenkinsfile). This automatic recovery of the repo is carried out by the polled SCM.
Why sometimes do an explicit checkout?
- If the Jenkinsfile is in a different repo than the source code
- If you want to specify a particular branch
- If we need special credentials
Pipeline Steps Reference: The Pipeline Steps Reference page on jenkins.io lists all available steps with their documentation.
Important — @Grab does not work in pipelines:
⚠️
@Grab(Grape) does not work in Jenkins pipeline scripts. To use external libraries in a pipeline, one must ensure that a corresponding plugin is installed (via a startup script) and then reference it in the pipeline.
4.6 Demo: A real Jenkinsfile pipeline
Simple .NET Core application:
// Program.cs (ConsoleApp1)
static void Main(string[] args)
{
Console.WriteLine("I love Groovy");
}
Workflow:
- Create the application in Visual Studio 2019 (Community Edition, free)
- Edit message
- Commit and push to GitHub
Jenkinsfile to build a .NET Core application:
node {
stage('Build') {
dir('ConsoleApp1') {
if (isUnix()) {
sh 'dotnet build'
} else {
bat 'dotnet build'
}
}
}
stage('Test') {
echo 'Testing....'
}
stage('Deploy') {
echo 'Deploying....'
}
}
Problem in the @script directory:
The pipeline workspace contains a HelloPipeline@script directory which is locked during the build. Ideally, only the Jenkinsfile should be in this directory, and the source code should be in a separate subdirectory.
Error handling in Jenkinsfile:
node {
stage('Build') {
try {
dir('ConsoleApp1') {
if (isUnix()) {
sh 'dotnet build'
} else {
bat 'dotnet build'
}
}
} catch(ex) {
echo "Build failed: ${ex.getMessage()}"
throw ex // re-lancer pour marquer le build comme échec
}
}
}
4.7 Comparison with Azure DevOps
Azure DevOps (Microsoft’s build system) works almost exactly like the Jenkinsfile setup, with a few differences:
| Jenkins Scripted Pipeline | Azure DevOps |
|---|---|
| Groovy (Jenkinsfile) | YAML |
| Scripted or Declarative | Mainly declarative |
| Plugin model | Model extension |
What the two systems share: They are both focused on storing the build definition in a version control system. The traditional model where build files are inside Jenkins is simply untenable for the reasons discussed.
4.8 Summary of Module 4
This module covered:
- Problems with the traditional Jenkins build model (no versioning, no flexibility per branch)
- Exploring Jenkins internals and the structure of
config.xmlfiles - The solution: the Jenkinsfile scripted in Groovy
- The structure of a Jenkinsfile:
node,stage, and steps - An actual pipeline that builds a .NET Core application
- Unable to use
@Grabin pipelines
5. Shared libraries and plugins
5.1 Introduction
At this stage, we have a good mastery of the concepts and syntax. We will now move from principles to application, exploring concrete problems and their solutions — with two objectives:
- Have these solutions on hand for specific situations encountered
- Learn good habits by reading good code
5.2 Reusing Groovy code in Jenkins
We already have a basic implementation of reuse by referencing scripts via file rather than by copy and paste. But after a while, we need the shared scripts to do something slightly different. You must then create a layer separation:
- Separate business objects and file access from script logic
- Use Global Pipeline Libraries (global shared libraries)
Global Pipeline Libraries:
- Trusted code that runs with a higher level of trust than the rest
- Can use
@Grabsyntax for external libraries (because trusted) - Are code → belong to a version control system
- Pulled from a version control repo
⚠️ Critical Safety Warning: Anyone with access to the shared libraries repo has the build server keys. This repo must be particularly well secured. For course, repos are public — in the real world, this repo would be strictly locked.
5.3 Demonstration: Global Shared Libraries
Context: Create a script that lists the contents of a directory and persists this information in a releasenotes.txt file, then commit it to a separate repo, configure the library in Jenkins, and run it from a pipeline.
Imports required:
import groovy.io.FileType
import java.io.File
eachFileRecurse method:
Coming from groovy.io, it allows you to recursively browse all the files and directories in a folder.
Initial script (shared library releasenotes.groovy):
import groovy.io.FileType;
import java.io.File;
@NonCPS
def call(Map config = [:]) {
def dir = new File(pwd());
new File(dir.path + '/releasenotes.txt').withWriter('utf-8') { writer ->
dir.eachFileRecurse(FileType.ANY) { file ->
if (file.isDirectory()) {
writer.writeLine(file.name);
} else {
writer.writeLine('\t' + file.name + '\t' + file.length());
}
}
}
}
Configure library in Jenkins:
- Manage Jenkins > Configure System
- Scroll to Global Pipeline Libraries
- Click Add
- Complete:
- Name:
releasenotes - Default version:
master - Check Load implicitly
- Check Modern SCM
- Select GitHub and enter the repo URL
- Click Save
Structure convention for a shared library:
shared-library-repo/
└── vars/
└── releasenotes.groovy # Nom du fichier = nom de la fonction
Usage in Jenkinsfile:
@Library('releasenotes') _
node {
stage('Build') {
// ...
}
stage('Release Notes') {
releasenotes()
}
}
5.4 How Groovy is run by Jenkins (CPS)
When the library is executed, an unexpected error message may appear. By consulting the link indicated, we discover the following explanation from Jenkins:
Jenkins Pipeline uses a library called Groovy CPS to run Pipeline scripts. Although Pipeline uses the Groovy parser and compiler, unlike a regular Groovy environment, it runs the majority of the program in a special interpreter. This uses a Continuation-Passing Style (CPS) transformation to convert your code into a version that can save its current state to disk and continue to run even after a Jenkins restart.
What this means in practice:
To persist the script on disk during execution, Jenkins serializes the script into a file called program.dat. This CPS transformation can move content in the script in a way that breaks execution.
The problem: Complex types used in withWriter and eachFileRecurse (closures) cannot be serialized to disk.
Two possible solutions:
- Rewrite the script without closures (laborious)
- Annotate the function as
@NonCPS— it does not need the CPS transformation
program.dat file: This is the serialization file that Jenkins uses to persist pipeline state between restarts.
5.5 Demonstration: Correcting our library with @NonCPS
Solution: add @NonCPS to the call function:
import groovy.io.FileType;
import java.io.File;
@NonCPS
def call(Map config = [:]) {
def dir = new File(pwd());
new File(dir.path + '/releasenotes.txt').withWriter('utf-8') { writer ->
dir.eachFileRecurse(FileType.ANY) { file ->
if (file.isDirectory()) {
writer.writeLine(file.name);
} else {
writer.writeLine('\t' + file.name + '\t' + file.length());
}
}
}
}
Commit, push, restart pipeline → success!
Checking in Jenkins container:
# Naviguer vers le workspace
cd /var/jenkins_home/workspace/HelloPipeline
# Vérifier le fichier généré
cat releasenotes.txt
Advantage: This library is global. It can be reused in any pipeline. If we had external libraries to reference via @Grab, we could do so in a trusted shared library.
5.6 Integrate changes into Release Notes
QA teams need more information in the release notes:
- The revision data that triggered the build (changes, authors)
- The date, time and build number at the top of the file
Why not just read the changelog file? The changelog is stored in the build folder — we could simply copy it to the release notes, but that would avoid working with the SCM libraries, which offer much more flexibility.
5.7 Demo: Improving Release Notes
Version with date, time and build number:
import groovy.io.FileType;
import java.io.File;
import java.text.SimpleDateFormat
@NonCPS
def call(Map config = [:]) {
def dir = new File(pwd());
new File(dir.path + '/releasenotes.txt').withWriter('utf-8') { writer ->
def now = new Date();
def fmt = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
writer.writeLine("Date and Time IS: " + fmt.format(now));
writer.writeLine("Build Number is: ${BUILD_NUMBER}");
if (config.changes != "false") {
def changeLogSets = currentBuild.changeSets;
for (change in changeLogSets) {
def entries = change.items;
for (entry in entries) {
writer.writeLine("${entry.commitId} by ${entry.author} on ${new Date(entry.timestamp)}: ${entry.msg}");
for (file in entry.affectedFiles) {
writer.writeLine("${file.editType.name} ${file.path}");
}
}
}
}
dir.eachFileRecurse(FileType.ANY) { file ->
if (file.isDirectory()) {
writer.writeLine(file.name);
} else {
writer.writeLine('\t' + file.name + '\t' + file.length());
}
}
}
}
Use with option disabled (without SCM changes):
releasenotes(changes: "false")
Default usage (with SCM changes):
releasenotes()
Important points:
currentBuildis an object accessible in Jenkins pipelines, not in the external consoleBUILD_NUMBERis an automatically available environment variablechangeLogSetscontains all changes since last build- To develop outside of a real build, you would need to mock the
currentBuildobject
Configuration management via Map:
By passing a Map config = [:] as a parameter, we can make the library configurable:
// Appel avec configuration personnalisée
releasenotes(changes: "false")
// Dans la fonction, tester la configuration
if (config.changes != "false") {
// Inclure les données de changements
}
5.8 Shared pipelines
Scenario: We want to have the same four phases (SCM, Build, Test, Deploy) for all projects, without having to create a separate pipeline for each, and also to force developers to respect conventions (eg: having a test project).
Observation: Jenkins pipeline scripts and shared libraries are basically the same thing. Both are pipeline scripts.
Idea: Take the existing Jenkinsfile, save it in a new script file, wrap it in a call convention, and we can create a generic pipeline.
Genericity rule: If we look at the current Jenkinsfile, the only particularity is the name of the folder (ConsoleApp1). We can configure it.
5.9 Demo: Create a shared pipeline
Shared library repo structure:
shared-library-repo/
└── vars/
├── releasenotes.groovy
└── genericbuild.groovy # Pipeline partagé
File genericbuild.groovy:
@NonCPS
def call(Map config = [:]) {
node {
stage('Build') {
try {
dir(config.projectFolder) {
if (isUnix()) {
sh 'dotnet build'
} else {
bat 'dotnet build'
}
}
} catch(ex) {
echo "Build failed: ${ex.getMessage()}"
throw ex
}
}
stage('Test') {
echo 'Testing...'
}
stage('Release Notes') {
releasenotes()
}
stage('Deploy') {
echo 'Deploying...'
}
}
}
Important note: If the child library (releasenotes) is @NonCPS, the parent library (genericbuild) must also be @NonCPS. However, do not add @NonCPS to genericbuild if the pipeline is called from genericbuild — Jenkins automatically detects this. If we add it anyway, the script will crash. You must therefore leave @NonCPS absent from the shared pipeline or carefully follow the error message.
Configure as library in Jenkins:
- Manage Jenkins > Configure System > Global Pipeline Libraries
- Add a new library named
genericbuild - Same parameters (master, Load implicitly, Modern SCM, GitHub)
New ultra-simplified Jenkinsfile:
genericbuild(projectFolder: 'ConsoleApp1')
That’s it! A single function call replaces the entire Jenkinsfile.
5.10 Shared Libraries Summary
The value of code sharing in DevOps:
In addition to the usual advantage of sharing code (avoiding duplication), libraries shared in DevOps have an additional advantage: they enforce standards.
Examples of what we could do with a shared library:
- Check if a test project exists and skip step if not
- Detect the presence of a documentation folder and trigger a documentation build
- Compile web documentation
- Synchronize content with a Wiki
5.11 Working with plugins
We have already discussed the value of startup scripts to ensure configuration. Configuration as code becomes critical when working with containers. It’s easy to pull the latest image of a Jenkins container from Docker Hub, but if we want this latest version to work our way, we need to enforce our configuration.
The plugin is central to Jenkins:
Jenkins comes with a lot of plugins by default. All of these plugins have potential security, maintenance, and performance impacts. It is not uncommon to:
- Disable all default plugins
- Only have a whitelist of authorized plugins
5.12 Demonstration: Manage plugins by script
Basic script — list all installed plugins:
// Depuis la Script Console Jenkins
Jenkins.instance.pluginManager.plugins.each {
println "${it.shortName} - ${it.version}"
}
Enhanced version — alphabetically sorted list:
List<String> sortedPlugins = new ArrayList<String>(Jenkins.instance.pluginManager.plugins);
sortedPlugins = sortedPlugins.sort { x -> x.displayName.toLowerCase() }
for (plugin in sortedPlugins) {
println(plugin.displayName);
}
Check if a specific plugin is installed (by displayName):
Boolean isInstalled(String displayName) {
def plugin = new ArrayList<String>(Jenkins.instance.pluginManager.plugins)
.find { x -> x.displayName == displayName };
return plugin != null;
}
println(isInstalled("Job Configuration History Plugin") ? "Installed" : "Not Installed");
Check with minimum version number:
Boolean isInstalled(String displayName, BigDecimal version) {
def plugin = new ArrayList<String>(Jenkins.instance.pluginManager.plugins)
.find { x -> x.displayName == displayName };
return plugin != null && new BigDecimal(plugin.version) > version;
}
println(isInstalled("Job Configuration History Plugin", 2.0) ? "Installed" : "Not Installed");
Object-oriented PluginCheck class:
class PluginCheck {
String key;
BigDecimal pluginVersion;
def internalPlugin;
PluginCheck(String artifactId, String version) {
key = artifactId;
pluginVersion = new BigDecimal(version);
internalPlugin = new ArrayList<String>(Jenkins.instance.pluginManager.plugins)
.find { x -> x.shortName == this.key };
}
String shortName() { return internalPlugin.shortName; }
String displayName() { return internalPlugin.displayName; }
String installedVersion() { return internalPlugin.version; }
Boolean isInstalled() {
return internalPlugin != null && new BigDecimal(internalPlugin.version) > this.pluginVersion;
}
void install() {
Jenkins.instance.updateCenter.getPlugin(key).deploy();
}
}
// Utilisation : installer si pas installé
def pluginCheck = new PluginCheck("jobConfigHistory", "2.0");
if (!pluginCheck.isInstalled()) {
pluginCheck.install();
} else {
println(pluginCheck.isInstalled() ? "Installed" : "Not Installed");
println(pluginCheck.shortName());
println(pluginCheck.installedVersion());
}
5.13 Script pattern for plugins
Recommended pattern to enforce a plugin configuration:
- When starting Jenkins, a Groovy init script is triggered
- It loads the whitelist of authorized plugins
- It disables plugins installed but not in the whitelist
- It installs the plugins in the whitelist but not installed
- If necessary, it performs a scripted reboot
After reboot, the plugin configuration matches what is in the whitelist — without additional reboot.
5.14 Demonstration: Enforcing a plugin whitelist
The whitelist (whitelist.txt):
git
github
credentials
plain-credentials
ssh-credentials
workflow-aggregator
pipeline-model-definition
A simple text file with one artifactId per line. In practice, we would also like to check the versions.
Location: /var/jenkins_home/userContent/whitelist.txt
This path is served by Jenkins, accessible via http://jenkins-host:port/userContent/whitelist.txt.
Complete PluginManager class:
class PluginManager {
Set pluginIds;
PluginManager() {
File whiteList = new File("/var/jenkins_home/userContent/whitelist.txt");
pluginIds = whiteList.readLines();
}
Set installedPluginIds() {
return (Set)(Jenkins.instance.pluginManager.plugins.shortName);
}
// Plugins installés mais pas dans la whitelist
Set installedNotInWhitelist() {
return installedPluginIds().minus(pluginIds).sort();
}
// Plugins dans la whitelist mais pas installés
Set whitelistedNotInstalled() {
return pluginIds.minus(installedPluginIds());
}
void disableNotWhitelisted() {
for (pluginId in installedNotInWhitelist()) {
PluginCheck plugin = new PluginCheck(pluginId, ".9");
if (plugin.enabled) {
println('enabled, disabling');
plugin.disable();
}
}
}
void installWhitelist() {
for (pluginId in whitelistedNotInstalled()) {
PluginCheck plugin = new PluginCheck(pluginId, ".9");
plugin.install();
}
}
}
Class PluginCheck with disable:
class PluginCheck {
String key;
BigDecimal pluginVersion;
def internalPlugin;
PluginCheck(String artifactId, String version) {
key = artifactId;
pluginVersion = new BigDecimal(version);
internalPlugin = new ArrayList<String>(Jenkins.instance.pluginManager.plugins)
.find { x -> x.shortName == this.key };
}
Boolean getEnabled() { return internalPlugin.isEnabled(); }
void disable() { internalPlugin.disable(); }
String shortName() { return internalPlugin.shortName; }
String displayName() { return internalPlugin.displayName; }
String installedVersion() { return internalPlugin.version; }
Boolean isInstalled() {
return internalPlugin != null && new BigDecimal(internalPlugin.version) > this.pluginVersion;
}
void install() {
Jenkins.instance.updateCenter.getPlugin(key).deploy();
}
}
Full usage:
def pluginManager = new PluginManager();
// Désactiver les plugins hors whitelist
pluginManager.disableNotWhitelisted();
// Installer les plugins de la whitelist manquants
pluginManager.installWhitelist();
// Afficher l'état final
for (plugin in pluginManager.installedNotInWhitelist()) {
println(plugin);
}
Set logic (set operations):
installedPluginIds() = { A, B, C, D, E }
whitelist (pluginIds) = { C, D, E, F, G }
installedNotInWhitelist() = installedPluginIds - pluginIds = { A, B } → à désactiver
whitelistedNotInstalled() = pluginIds - installedPluginIds = { F, G } → à installer
5.15 Important Warning
⚠️ Attention! Assembling the whitelist is an important job that cannot be done in a few minutes.
You can’t just go through the list of installed plugins and choose the ones you think you need. These plugins are highly dependent on each other — disabling a plugin that seems unnecessary can accidentally disable a chain of plugins you need.
Scripted restart reference: The script from samrocketman on GitHub provides a good starting point for implementing a scripted restart after modifying plugins.
6. User and credential management
6.1 Introduction
Important distinction: Users vs Credentials
| Concept | Management | Usage |
|---|---|---|
| Users | → Jenkins | Connection and permissions in Jenkins |
| Credentials | Jenkins → | Jenkins authenticates to external resources (Git, Docker Hub, AWS) |
Jenkins credentials are typically managed by connecting to an organizational directory rather than manually. But sometimes, we need to manage users by script.
6.2 Demonstration: Working with Users
List all users (simple version):
import hudson.model.User;
// Afficher tous les IDs utilisateurs
User.getAll().each { println it.id }
Basic UserManager class:
import hudson.model.User;
class UserManager {
Set allUsers() {
return User.getAll();
}
}
def mgr = new UserManager();
for (user in mgr.allUsers()) {
println(user.id);
}
Create user:
import jenkins.model.*
import hudson.security.*
void createUser(userName, password) {
def instance = Jenkins.getInstance()
def hudsonRealm = new HudsonPrivateSecurityRealm(false)
hudsonRealm.createAccount(userName, password)
instance.setSecurityRealm(hudsonRealm)
instance.save()
}
Complete UserManager class with create, delete and userExists:
import hudson.model.User;
import hudson.security.*
class UserManager {
Set allUsers() {
return User.getAll();
}
void createUser(String userName, String password) {
if (!this.userExists(userName)) {
Jenkins instance = Jenkins.getInstance();
def realm = new HudsonPrivateSecurityRealm(false);
realm.createAccount(userName, password);
instance.setSecurityRealm(realm);
instance.save();
}
}
Boolean userExists(userName) {
return User.get(userName) != null;
}
void deleteUser(String userId) {
if (this.userExists(userId)) {
User u = User.get(userId);
u.delete();
}
}
}
// Utilisation
def mgr = new UserManager();
mgr.createUser("test", "user");
mgr.deleteUser("test");
println(mgr.userExists("cbehrens"));
for (user in mgr.allUsers()) {
println(user.id);
}
Notes:
HudsonPrivateSecurityRealm(false)—falsecorresponds toallowSignup- An
unknownuser may appear in the list — this is a fallback user created to avoidnull users, never to be deleted - Check
userExistsincreateUseravoids duplication
Pattern whitelist for users: We could create an initialization script which:
- Loads a list of users from a configuration file
- Create missing users
- Delete unlisted users
6.3 Working with credentials
Credentials allow Jenkins to authenticate to external resources. The Credentials plugin must be installed (usually installed by default).
GitHub Security Agreement — Personal Access Token (PAT):
To access a private GitHub repo from Jenkins:
- Create a Personal Access Token on GitHub (Settings > Developer settings > Fine-grained personal access tokens)
- Configure permissions:
- Expiration: 90 days (avoid 30 days, too short)
- Only select repositories: select only the necessary repositories
- Repository permissions > Contents: Read-only (Jenkins generally does not need writing)
- Copy the token immediately — it is only visible once
6.4 Create credentials by script
Inject credentials into a pipeline:
// credspipeline.groovy
node {
stage('Github') {
git credentialsId: 'githubcreds', url: 'https://github.com/FeynmanFan/needscredentials.git'
}
}
Create credentials via Jenkins UI:
- Manage Jenkins > Manage Credentials
- Select the appropriate domain (eg: Global)
- Click Add Credentials
- Type: Username with password
- ID:
githubcreds(will match thecredentialsIdin the pipeline) - Username: your GitHub username
- Password: the PAT copied previously
- Description: explanatory text
6.5 Demonstration: Manage credentials by script
Imports required:
import com.cloudbees.plugins.credentials.*;
import com.cloudbees.plugins.credentials.common.*;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
Class CredentialManager:
import com.cloudbees.plugins.credentials.*;
import com.cloudbees.plugins.credentials.common.*;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
class CredentialManager {
Set getAll() {
return CredentialsProvider.lookupCredentials(
StandardUsernameCredentials.class,
Jenkins.instance,
null, // authentication (null = tout)
null // domains (null = tous)
);
}
void changePassword(String credentialId, String password) {
def creds = CredentialsProvider.lookupCredentials(
StandardUsernameCredentials.class,
Jenkins.instance,
null,
null
);
def credential = creds.findResult { it.id == credentialId ? it : null };
if (credential != null) {
def credentials_store = jenkins.model.Jenkins.instance.getExtensionList(
'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
)[0].getStore()
def success = credentials_store.updateCredentials(
com.cloudbees.plugins.credentials.domains.Domain.global(),
credential,
new UsernamePasswordCredentialsImpl(
credential.scope,
credential.id,
credential.description,
credential.username,
password
)
)
if (!success) {
throw new RuntimeException("Changing password failed.");
}
println("password change complete");
}
}
}
def mgr = new CredentialManager()
// Lister les credentials
for (cred in mgr.getAll()) {
println("${cred.id} - ${cred.description}")
}
// Changer le mot de passe
mgr.changePassword("githubcreds", "new_password");
Notes:
StandardUsernameCredentials.classfilters credentials (excludes certificates)null, nullas authentication and domains = match allchangePasswordmethod usesupdateCredentialsfromcredentials_store- Scope, id and description are preserved from existing credentials
6.6 Course Summary
This course covered a lot of ground. The educational philosophy was to introduce new language features gradually and unobtrusively — the way programmers actually learn, by seeing something unfamiliar in a familiar context.
What was covered:
| Module | Content |
|---|---|
| 2 | Groovy Fundamentals: Types, Control, Classes, Inheritance |
| 3 | Groovy + Jenkins: plugins, startup scripts, exceptions |
| 4 | Jenkinsfile scripted, pipeline as code |
| 5 | Shared libraries, CPS/@NonCPS, plugin management |
| 6 | User and credential management |
Final recommendations:
- Groovy is a solid language, very similar to Java/JavaScript. If you know one of these popular languages, Groovy will be intuitive (only notable quirk:
definstead ofvar) - If you are still working with classic Jenkins builds, migrate to pipelines — with the Snippet Generator, the learning curve is short and you will never go back
- Jenkins automation with Groovy is an investment that quickly pays off in efficiency and standardization
7. Appendix: Reference Code Files
A.1 Structure of exercise files
jenkins-groovy-automating/
├── 02/demos/ # Module 2 - Fondamentaux Groovy
│ ├── datatypes.groovy
│ ├── controlstructures.groovy
│ ├── subroutines-1.groovy
│ ├── subroutines-2.groovy
│ ├── classes-1.groovy
│ ├── classes-2.groovy
│ ├── abstract.groovy
│ ├── abstract-2.groovy
│ ├── abstract-3.groovy
│ ├── imports.groovy
│ ├── music.json
│ ├── controlstructures (fichier)
│ └── jenkinsfile (fichier)
├── 03/demos/ # Module 3 - Jenkins + Groovy
│ ├── 1_config.groovy
│ ├── exceptions_1.groovy
│ └── exceptions_2.groovy
├── 04/demos/ # Module 4 - Builds avec Groovy
│ ├── jenkinsfile.txt
│ └── jenkinsfile1.groovy
├── 05/demos/ # Module 5 - Bibliothèques partagées
│ ├── getplugins.groovy
│ ├── pluginInstalled.groovy
│ ├── pluginInstalled_1.groovy
│ ├── pluginInstalled_2.groovy
│ ├── pluginInstalled_3.groovy
│ ├── pluginmanager.groovy
│ ├── pluginmanager_1.groovy
│ ├── pluginmanager_2.groovy
│ ├── pluginmanager_3.groovy
│ ├── pluginmanager_4.groovy
│ ├── releasenotes.groovy
│ ├── releasenotes_2.groovy
│ ├── releasenotes_3.groovy
│ └── releasenotes_4.groovy
└── 06/demos/ # Module 6 - Utilisateurs et credentials
├── createuser.groovy
├── createuser_1.groovy
├── createuser_2.groovy
├── credentialmanager.groovy
├── credspipeline.groovy
└── usermanager_1.groovy
A.2 Key concepts and shortcuts
| Concept | Syntax |
|---|---|
| Declare a variable (dynamic typing) | def x = 5 |
| Typed declaration | String name = "Chris" |
| String interpolation | "Hello ${name}" |
| Loop each (lambda) | list.each { x -> println(x) } |
| Loop each (implicit) | list.each { println(it) } |
| Abstract class | abstract class MyClass {} |
| Legacy | class Child extends Parent {} |
| Import | import groovy.json.JsonSlurper |
| External library | @Grab(group='...', module='...', version='...') |
| Disable CPS Serialization | @NonCPS |
| Try/catch | try { } catch(Exception ex) { } |
| Assertion | assert x == 10: "message" |
| Pipeline steps | node { stage('Build') { ... } } |
| Shared Library | @Library('name') _ or Load implicitly |
A.3 Useful Docker Commands for Jenkins
# Créer le répertoire init.groovy.d
docker exec jenkins mkdir -p /var/jenkins_home/init.groovy.d
# Copier un script init vers le conteneur
docker cp 1_config.groovy jenkins:/var/jenkins_home/init.groovy.d/
# Naviguer vers le workspace
docker exec -it jenkins bash
cd /var/jenkins_home/workspace/HelloPipeline
# Afficher le contenu d'un fichier
cat releasenotes.txt
A.4 Important Referrer URLs
| Resource | URL |
|---|---|
| Jenkins API Documentation (Javadoc) | javadoc.jenkins-ci.org |
| Official Groovy documentation | groovy-lang.org |
| Pipeline Steps Reference | jenkins.io (Pipeline section > Pipeline Steps Reference) |
| Maven Repository (packages for @Grab) | mvnrepository.com |
| Groovy Console (download) | groovy-lang.org/download.html |
Search Terms
automating · jenkins · groovy · ci/cd · git · devops · demonstration · script · shared · jenkinsfile · libraries · plugins · credentials · external · types · builds · classes · console · control · data · exception · handling · important · legacy