Jenkins : Create a new Plugin with a custom build Step

Setup project structure

Jenkins uses Maven for both the core and all plugins. Custom plugins are used to generate code and configuration and help developers get a productive environment.

For this tutorial, we will not use the hpi:create maven goal that creates a plugin skeleton, so that we can better describe all elements of the plugin structure.

pom.xml

The project descriptor has nothing surprising if you're used to Maven. It just extends the common jenkins plugin parent POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.jenkins-ci.plugins</groupId>
    <artifactId>plugin</artifactId>
    <version>1.400</version>
  </parent>

  <artifactId>graven</artifactId>
  <packaging>hpi</packaging>
  <name>Jenkins GRaveN Plugin</name>
  <version>0.1-SNAPSHOT</version>
  <url>http://wiki.jenkins-ci.org/display/JENKINS/GRaveN+Plugin</url>
</project>

The hpi packaging is the custom format used by Jenkins to distribute plugins.

The version of the parent POM defines the minimal version of Jenkins this plugin will require. Unless you require some new API features that are available with more recent Jenkins releases, you should avoid forcing users of your plugin to run bleeding-edge Jenkins releases.

The POM <url> points to the plugin wiki page on the Jenkins wiki. This will be used by the update center to create a link to your plugin.

Directory structure

Your project will follow the standard maven directory structure, including a src/main/webapp folder for static resources that the plugin will contribute to Jenkins UI.

pom.xml
|_ src
   |_ main
   |   |_ java
   |   |_ resources
   |   |_ webapp
   |_ test        (...)

Create the plugin class

Jenkins uses an auto-discovery of components, so we only have to create a class with adequate convention for Jenkins to add it to available components.

Builder

As we are designing a plugin to support a build tool, we will create a Builder. Depending on the plugin goal you may extend Publisher, SCM, or another base class of the API (TODO link).

package hudson.plugins.graven;

import hudson.tasks.Builder;

import org.kohsuke.stapler.DataBoundConstructor;

/**
 * @author <a href="mailto:nicolas.deloof@cloudbees.com">Nicolas De loof</a>
 */
public class GravenBuilder extends Builder {

    private final String task;

    @DataBoundConstructor
    public GravenBuilder(String task) {
        this.task = task;
    }

    public String getTask() {...}
}

Our graven integration will allow user to configure a graven task to be invoked, so we define an attribute to collect the configured task. It will be populated as the user hit the "save" button on job configuration using the class constructor, as we have used the annotation @DataBoundConstructor. Stapler, the web framework used by Jenkins, will collect user data from configuration page, serialize that data as JSON and automagically handle data binding to convert them to java objects using the @DataBoundConstructors.

Descriptor

The GravenBuilder class instances will match our job build steps, we now need to declare them and give Jenkins a way to manage them. This is the role of the Descriptor which acts as a factory and as a centralized configuration to store metadata for all instances. By convention, Descriptors are declared as inner classes in the component class (but this is not a requirement). They just need an @Extension annotation for Jenkins to discover them at runtime.

public class GravenBuilder extends Builder {

    (...)

    @Extension
    public static class Descriptor extends BuildStepDescriptor<Builder> {

        @Override
        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
            return FreeStyleProject.class.isAssignableFrom(jobType);
        }

        @Override
        public String getDisplayName() {
            return "execute GRaveN task";
        }
    }
}

As we are writting a descriptor for a Builder, we extend BuildStepDescriptor for convenience. We then have to implement two methods :

  • isApplicable will be used to ask the descriptor if the component it manages can be used by the current project. You may want your plugin to apply only to FreeStyle projects but not Maven or Matrix ones
  • getDisplayName allow us to declare the label used in the Build Steps select box when user will configure the job. Remember that when we will talk about internationalization bellow
View

We have a component, a Descriptor that will manage it's lifecycle, we now need a view for user to interact with the component (and descriptor). Jenkins uses jelly templating langage to build views (you can also use groovy with recent jenkins releases). For our component hudson.plugin.graven.GravenBuilder, Jenkins will search for a config.jelly file in fully qualified class name path, i.e. hudson/plugin/graven/GravenBuilder/config.jelly.

Lets create hudson.plugin.graven.GravenBuilder package in scr/main/resources and put a simple jelly template :

<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
  <f:entry title="${%GRaveN task}" field="task">
        <f:textbox/>
  </f:entry>
</j:jelly>

The namespace declaration allows to include a large set of jelly tags to handle various view building requirements, either standard ones from the jelly project (jelly:core) or custom ones packaged within Jenkins or plugins as resources ("/lib/form").

The tag based approach may not feel pleasant compared to your favorite web framework, but they are simple and do the job !

The field attribute enables data-binding with the component. When set, the textbox value will be used to populate the associated class attribute (using the matching DataBoundConstructor parameter). When editing an existing build step, the associated getter will be used to populate the textfield.

The title attribute uses a special syntax that enables jelly internationalization, see bellow.

Help

We can provide a help file for the Builder, to explain usage and constraints, by adding a help.html in our hudson.plugins.graven.GravenBuilder resource package. This file must contain definition for a <div> html fragment and will be included in the page as tooltip.

<div>
    This build step can be used to invoke <a href="http://www.graven.org">GRaveN</a> tasks
</div>

Adding help tips for each form input follows same pattern : we just have to provide a simple help-<fieldName>.html file aside the jelly file in src/main/resources, so help-task.html :

<div>
    Configure the GRaveN task to execute
</div>

We can also include a general description of the plugin, that will appear in update center as a description or our plugin, using an index.jelly file at resource root. Don't miss that one, as this will be the first thing user will see about your plugin before installing ;)

<div>
  This plugin add support for GRaveN, the most advanced build tool ever.
</div>

Run it!

We now want to test our plugin. Jenkins offers an automated test setup that you can invoke using mvn hpi:run. This will launch Jenkins with your plugin pre-installed, using a local $basedir/work as JENKINS_HOME. In this mode, your plugin is not packaged as a HPI file and you can edit your views and test the result of changes by hitting the browser refresh button. Code changes are not dynamically reloaded anyway, until you enable JRebel support.

Note: running hpi:run includes launching the maven build phases that are defined by the parent POM and are required to process the @Extension annotations and few other custom build steps. 

Test it!

TODO describe the testing framework

Internationalization (i18n)

Jenkins uses resource bundle to support internationalization. Don't forget to configure your IDE with the adequate plugin to enable the native2ascii conversion if your language uses non ASCII characters

To i18n our plugin, we create a new bundle Messages.properties in the plugin resources, under plugin package (hudson.plugins.graven). We will put there text messages used in our code and jelly templates.

Messages.properties
GravenBuilder.Task=execute GRaveN task

During maven build process, this bundle will be converted to a Messages class that we can use from code in replacement for strings. MessageFormat patterns are also supported when the message requires dynamic parameters.

        @Override
        public String getDisplayName() {
            return Messages.GravenBuilder_Task();
        }

If you are using Intellij Idea as IDE, you can use the stapler plugin to extract hard-coded message and refactor to bundle.

The resource bundle that is associated with our View is a set of config_LOCALE.properties next to the jelly file. The message key is simply the text within the pattern, with space character escaped :

config_fr.properties
GRaveN\ task=t\u00e2che Graven

Help file can also be i18n by adding the corresponding locale extension, so help-task.html -> help-task_fr.html