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.
The project descriptor has nothing surprising if you're used to Maven. It just extends the common jenkins plugin parent POM
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.
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.
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.
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.
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.
- 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
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 :
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.
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.
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 :
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 ;)
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.
TODO describe the testing framework
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.
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.
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 :
Help file can also be i18n by adding the corresponding locale extension, so help-task.html -> help-task_fr.html