Part 1 – So You Want to Develop a Jenkins Plugin
I’ve been using Jenkins for a while now both at work and at home. While there are many ‘new kids on the block’ in terms of alternative CI solutions, nothing I’ve found is a comprehensive alternative to Jenkins.
If you want to host your own CI system without worrying about endless upkeep, Jenkins is most certainly the right choice.
However, as an engineer that primarily develops for Apple platforms, I’ve found Jenkins to be somewhat lacking in plugins for certain iOS development tasks.
With that in mind, I did what any good engineer would do in the face of a challenge – I decided to build a plugin of my own.
When Jenkins is Good It’s Very, Very Good
The Jenkins plugin community is one of the healthiest I’ve seen. It is a testament to the stability of Jenkins that it is in such wide usage, but the fact that some plugins have not been updated for 3+ years and continue to work is a testament to the architecture of the system itself.
Jenkins is engineered to allow significant extension to virtually all parts of the software. Thanks to this “freedom to extend” approach, it doesn’t just have a plugin for everything; the plugins deeply integrate with the native functionality of Jenkins.
To attest to this truth, huge chunks of the native functionality of Jenkins are now developed as plugins. In fact, the base Jenkins installation is virtually featureless without the small number of plugins that come pre-installed: this includes SCM plugins, basic build steps such as shell script execution and maven runners, and various build environment tweaks.
Nothing is out of bounds. You can create your own build steps, build report, job types, system configuration options, dashboards, and so much more.
In a lot of ways, Jenkins is the perfect
When Jenkins is Bad, It’s Horrid
So where do we start if we want to build a new plugin? Well … there is some documentation on the Jenkins wiki, however I’ve found the documentation to be less than optimal for a variety of reasons:
- Plugin pages are split across a legacy listing in the wiki and a new plugins-specific listing on jenkins.io.
- There’s a plugin tutorial page but much of the information found on it is years old. While the architecture of Jenkins hasn’t changed, other tooling such as Maven and IDEs have moved on a lot.
- A lot of the documentation links out to other tools that Jenkins plugins use such as Stapler and Jelly. While this is interesting information, it’s largely unuseful when trying to dive into plugin development.
Ultimately, It’s very difficult to get off the ground with plugin development. It’s a strange situation because all of the tooling and examples are out there, they just aren’t very easy to find.
Maven is your friend
The quickest way to get started is to use the HelloWorld example plugin. This can be created from a maven template by running
mvn archetype:generate -Dfilter=io.jenkins.archetypes:
This will run you through a quick step-by-step setup routine that lets you create a basic plugin.
If you’re not familiar with Maven then we are pretty much in the same boat. I used it extensively in a job I had a few years ago, but due to it’s convention-over-configuration paradigm, I rarely felt like I understood the things it was doing.
If you want to learn more about Maven there are swathes of tutorials and docs out there, but I won’t be covering that here.
IntelliJ is your Friend
There are half a dozen commonly used Java IDEs out there and it doesn’t really matter what you use to this end. That said, I’m using IntelliJ and my examples here will reference that.
I won’t be making any references to IDE functionality unless I have to. I’d strongly recommmend using IntelliJ but if you want to use another IDE, the minimum requirements are:
- It is able to run Maven goals
- It is able to debug Maven projects
This allows for use of IDEs such as Netbeans and Eclipse.
This is not an Install Script
We’re here to talk about plugins. I’m not going to describe how to setup the above tools – again, there exist dozens of tutorials and docs describing exactly this.
Once you have your “Hello World” plugin open in your IDE, move on to the next part of the tutorial.
Part 2 – Developing Your Plugin
Once you’re all setup, you should have a plugin project open in your IDE that compiles and has passing tests.
Debugging your Plugin
Debugging the plugin is pretty straight-forward. Assuming you don’t have any debug configuration you’ll need to create one.
In IntelliJ this is done by creating a new debug configuration. In the Debug Configurations screen, you should add a maven configuration with a goal of “hpi:run”.
HPI is the format used to package Jenkins plugins. This is not particularly relevant other than to say that this is the package type that you will use to distribute your plugin. More on that later.
The “hpi:run” goal tells Jenkins to run a debug instance of your plugin. This is where things get kind of interesting. The goal will boot up a full running instance of Jenkins with your plugin pre-installed.
Awesome! This means we can test our plugin exactly as we would on a real Jenkins instance.
Open your debug instance in a web browser (it should launch at http://localhost:8080/jenkins) and create a new job. In the job configuration, add a “Hello World” build step.
Finally, save your job and run it. Your output should look something like this:
Started by user anonymous Building in workspace /Users/jack/dev/java/github-coverage-reporter/work/jobs/Test/workspace Hello, World! Finished: SUCCESS
Great, time to start distributing our plugin to users all over the world!
Just kidding. Now we want to make our plugin do something interesting.
Changing the outcome of your build
The most common output of a build step is to mark the build as either successful or failing. For what better purpose does Jenkins serve than to give engineers a facial tick every time they see a grey ,pulsing traffic light turn solid red!
Let’s change the code: open the file called HelloWorldBuildStep (TODO: Check this) and find a method called “perform”. The full signature of this method is:
public void perform(Run<?,?> run, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException
This is the method that will be called when your step is reached during a job execution. Various arguments are passed but the two most useful are “run” and “listener”.
First we can log some data to our build log using the following:
listener.getLogger().println("Setting build result ...");
This will appear directly in the build log. If you want to log something explicitly as an error, you should use the following:
listener.error("Oh no! Something's gone horribly wrong!");
Finally you should set a build result. Set the build to fail using the following:
run.setResult(Result.FAILURE);
Part 3 – Adding UI elements
Jenkins use Jelly for UI elements. While Jelly has lots of reference material, there’s little in the way of guides or comprehensive examples. Here’s a few resources that are invaluable once you’re comfortable with how data binding works (we’ll go into that further down):
- Jenkins Taglib Reference – This is a reasonably thorough list of all available controls and how to use them. Most controls are named conventionally although there are a few exceptions. For example, ‘select’ has an obvious purpose if you’re familiar with HTML; but might be confused with ‘dropdownList’ without reading these docs.
- UI Samples Plugin – This is a far-from-complete set of examples of how to use various UI elements. Notably missing are the ‘select’ control and the ‘radioBlock’ control, both of which are frequently used in the most common plugins. It will still give you a good idea of how to get a control configured.
I’ve worked with a fair number of UI libraries in a variety of development environments, but Jelly really had me stumped for the first few days of attempting to use it.
Most developers are used to UI controls following the same conventions everywhere:
- They have attributes for defining how the control is displayed.
- They have one or more properties for handling the delegation of events such as user interactions or internal values changing.
Jenkins controls don’t necessarily eschew these conventions; rather, they are taken a step further.
Let’s take the example of a select control. The jelly for a select control will look something like this:
<f:entry title="${%Greetings}" field="greetings"> <f:select /> <f:helplink /> </f:entry>
There are three components to the above control:
- The entry tag is used to wrap most controls as well as groups of controls. It provides data bindings via the field tag and also allows for a title that’s displayed above the control(s).
- The select tag describes the interactive control that will be displayed on the screen.
- Finally the helplink tag indicates that the control should display a help link. This is a question mark icon displayed to the right of the control. When clicked, it opens a drawer containing some helpful information about the
Add the select control above to the config.jelly file for the HelloWorldBuildStep class (you’ll find this in the resources folder in a subfolder named HelloWorldBuildStep).
We should also add the field that we are binding the control to in the build step class. Add the following to the top of the class:
private String greeting; public String getGreeting(){ return coverageXmlType; } @DataBoundSetter public void setGreeting(String greeting) { this.greeting = greeting; }
Also add it to the existing constructor for the class:
@DataBoundConstructor public HelloWorldBuildStep(String greeting) throws IOException { this.greeting = greeting; }
Finally we also need a way to populate our control with options. Again Jenkins favours convention over configuration here, requiring the declaration of a method with a specific name in the descriptor for our plugin.
Let’s take a look at the descriptor.
Part 4 – The Descriptor
You’ll find the descriptor for your plugin as a nested class in your build step class, usually with the name DescriptorImpl.
The descriptor is how we communicate information about our plugin back to the Jenkins instance. It allows declarations of methods for providing information such as a display name for the plugin or dynamic data for UI controls.
To provide selectable items for our select control, we need to implement a method in the descriptor name doFillGreetingItems. The convention here is that the method name is derived from the field attribute of the select control. The implementation can looks something like this:
public ListBoxModel doFillGreetingItems() { ListBoxModel model = new ListBoxModel(); model.add(":-)", "Hello, I'm having a nice day!"); model.add(":-|", "Hello, I'm having an OK day"); model.add(":-(", "Hello, I'm having a bad day..."); return model; }
Debug the plugin to see the populated list. It should contain the smiley faces we provided as the first argument to add when creating our ListBoxModel. But what about the second argument? This is the value of the greetings field provided when our plugin runs.
It’s also possible to add validation for the field and return an error if a selection is considered invalid. Let’s assume that we don’t want people to select the sad face in our select options. We can implement the following:
public FormValidation doCheckSonarProject(@QueryParameter String value){ if (value.contains("bad day")) { return FormValidation.error("Cheer up friend!"); } return FormValidation.ok(); }
Great! Now our plugin will show a message to the user if they select the sad face.
Part 5 – Global Settings Descriptor
Now that we have a way of storing the configuration of our plugin within a build job we can react to configuration changes when the user runs a build. However, what if we want a piece of configuration to be shared between all instances of our plugin across all build jobs.
For this we need to provide a new descriptor for global configuration.
Create a class named PluginConfiguration in your java package, and create a matching folder in the resources. Inside the resources folder, create a folder named global.jelly.
Global configuration is declared very similarly to the descriptor for our build step with a few minor differences. Namely, the values of the configuration fields are initialised by overriding the configure method and extracting their values through a JSON object (JSON is ultimately how this configuration is stored).
@Override public boolean configure(StaplerRequest req, JSONObject json)throwsFormException{ fullName = json.getString("fullname"); return super.configure(req, json); }
Once declared, your configuration will be visible in the Settings page.
Conclusion
For the most part writing Jenkins plugins is fairly painless and concise, but it’s convention-heavy approach means that it can often be difficult to discern exactly how to configure parts of your plugin. I found the learning curve to be very steep in the early stages of development but eased off quickly.
Ultimately the best approach is to stick to the rules laid out above and study existing open-source plugins to see how they are structured.
The following are recommended:
And of course my very own plugin ?