Developing Custom Hudson Plugins: integrate with your own applications

Hudson is a very good Continuous Integration (CI) tool. Though such tools are not new, it clearly sets a higher standard in terms of quality and extensibility compared to first generation CI tools like Cruise-Control. And it’s open-source. (If you are reading this and you don’t know what Continuous Integration is, read this paper written by Martin Fowler for more information).

One of best features of Hudson is how easy is to extend it. As result there are many plug-ins developed by third parties that can perform a lot of tasks related to the activity of a development team. You can find the list of plug-ins available here. It’s a long list, but it does not mean they will cover everyone’s needs. If there is no plugin that covers your requirements then this blog post for you!

The development of a custom Hudson plug-in is not a daunting task. There are some tutorials out there that can help you to quick-start the development. The best ones are this and this in my humble opinion. You need some background in Maven and Web development to start. Using Maven you will be able to create the basic infrastructure in minutes, and you will be able to start coding and testing the plug-in inside Jetty.

On a recent project, we had to integrate Hudson with a Requirements Management and Bug Fixing application which our customer had developed in-house. The application satisfied their needs and it was out of the scope to change to another like Bugzilla or JIRA, which integrate almost seamlessly with Hudson.

Our customer wanted to change the status of some requirements and bug fixes when a Nightly Build and all the set of automatic unit, integration and acceptance tests were executed successfully. On success, Hudson should execute a stored procedure in the database that the application used, thereby triggering actions like measuring metrics of performance, reporting to quality assurance teams and others.

We didn’t want to reinvent the wheel, so in writing our plugin,we took the approach taken by others tools – when the user commits code to the SCM, they must write comments in a certain format which contain information for the relevant requirement/bug information (bug or requirement ID etc). On a successful build, the Hudson plugin retrieves the relevant information from all code comments and passes it to the requirement/bug system via the stored procedure.

Maven can create for you the default structure of directories and files thanks to the Hudson plug-in (see this). And the plug-in structure created by Maven is:
+ src
    + main
        + java
             +  full.package.name
                    +- MyClassPublisher.java
                    +- PluginImpl.java
                +  full.package.name
                                +- config.jelly
                                +- global.jelly
                +- index.jelly
        + webapp
            +- help-globalConfig.html
            +- help-projectConfig.html
+ src
    + test
        + java
        + resources
        +  full.package.name
We can identify here several key files and classes:

PluginImpl.java

is a class generated automatically by Maven, and it’s the class that registers the action to perform by the plug-in. The default type of action is of the class Builder, but we are more interested in performing an action when the building process is successful: so we use the type Publisher. We need to change the code in the method start to look like this:
    public void start() throws Exception {
        // plugins normally extend Hudson by providing custom implementations
        // of ‘extension points’. In this example, we’ll add one builder.
//        BuildStep.BUILDERS.add(HelloWorldBuilder.DESCRIPTOR);
        Publisher.PUBLISHERS.add(TSLPublisher.DESCRIPTOR);
    }
Do not modify the class, Hudson detects your plug-in (via its @plugin javadoc annotation), creates an instance, and invokes methods.

TSLPublisher.java

is a class that extends the Publisher class and it’s main purpose is to perform an action once the build process has been completed successfully. An example of Publishers are report generation tools or email sending. This class should look like this:

 
public class TSLPublisher extends Publisher {
 
    private static final Logger logger = Logger.getLogger(TSLPublisher.class.getName());
 
    private Boolean enable;
 
    @DataBoundConstructor
    public TSLPublisher(Boolean enable) {
        this.enable = enable;
    }
 
    /**
     * We’ll use this from the <tt>config.jelly</tt>.
     */
    public Boolean getEnable() {
        return enable;
    }
 
    public boolean needsToRunAfterFinalized() {
        return true;
    }
 
    /**
     * This method should have the logic of the plugin. Access the configuration
     * and execute the the actions.
     */
    public boolean perform(Build build, Launcher launcher, BuildListener listener) {
        logger.info(”Performing update…”);
        // TODO: WRITE THE LOGIC OF YOUR PLUGIN HERE!!!!
        return true;
    }
 
     public Descriptor<publisher> getDescriptor() {
         return DESCRIPTOR;
     }
 
     public Action getProjectAction(AbstractProject< ?, ?> project) {
         return null;
     }
 
     /**
     * Descriptor should be singleton.
     */
    public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
 
    /**
     * Descriptor for {@link TSLPublisher}. Used as a singleton.
     * The class is marked as public so that it can be accessed from views.
     *
     * <p>
     * See <tt>global.jelly</tt>
     * for the actual HTML fragment for the configuration screen.
     */
    public static final class DescriptorImpl extends Descriptor<publisher> {
        /**
         * To persist global configuration information,
         * simply store it in a field and call save().
         *
         * <p>
         * If you don’t want fields to be persisted, use <tt>transient</tt>.
         */
        private String field;
 
        protected DescriptorImpl() {
            super(TSLPublisher.class);
            load();
        }
 
        /**
         * This human readable name is used in the configuration screen.
         */
        public String getDisplayName() {
            returnThis is the TSL Sample Plugin”;
        }
 
        /**
         * Get the fields from the configuration form and persist them.
         */
        public boolean configure(HttpServletRequest req) throws FormException {
            uri = req.getParameter(”tsl.field);
            save();
            logger.fine(”Saved TSL configuration”);
            return super.configure(req);
        }
 
        /**
         * Creates a new instance of {@link TSLPublisher} from a submitted form.
         */
        public TSLPublisher newInstance(StaplerRequest req) throws FormException {
            logger.fine(New instance for a job”);
            return new TSLPublisher(req.getParameter(”tsl.enable)!=null);
        }
 
        public String getField() {
            return field;
        }
 
        public void setField(String field) {
            this.field = field;
        }
    }    
}
</p></publisher></p></publisher>
All the information I have found about plug-ins explains how to write a Builder. Publisher is similar. You have to modify:

  • The perform method do the real thing. To get the configuration parameters, use the DESCRIPTOR singleton to access the POJOs.
  • The inner class DescriptorImpl extends Descritor<Publisher>. It is instantiated once and accessed by a Singleton.
    • The configure() method reads the parameters and persists them.
    • The newInstance() should return an instance of the TSLPublisher to perform the action. I have implemented an enable check. When not enabled, there is no instance for this job.
    • All the fields can persist by default. Use transient if you don’t want to persist.

global.jelly

is a file generated automatically by Maven, and it’s the Jelly Script file to produce the global configuration option. It’s automatically added to the configuration pages of Hudson after deployment. It uses Jelly as scripting language (I think this is a bizarre decision, but it’s not important), you can find information about Jelly here. In our example the file looks like this:

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" 
         xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
   <f:section title="TSL Integration">
      <f:entry title="Field Test" help="${rootURL}/plugin/tsl/help-globalConfig.html" >
         <f:textbox name="tsl.field" value="${descriptor.field}" />
      </f:entry>
   </f:section>
</j:jelly>
The values in the form are stored in the ${descriptor.fieldname}, and the parameters can be referred with a unique name like ‘tsl.field’. It’s also possible to link help files as described in the example. Be sure that you are using the variable ‘descriptor’ to access the information.

config.jelly

is a file generated automatically by Maven, and it’s the Jelly Script file to produce the configuration option specific for the job. In our example the file looks like this:

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" 
         xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
	<f:entry description="Check to enable this publisher for the job">
		<f:checkbox name="tsl.enable" checked="${instance.enable}" />
	</f:entry>
</j:jelly>
The values in the form are stored in the ${instance.fieldname}, and the parameters can be referred with a unique name like ‘tsl.enabled’. It’s also possible to link help files as described in the example of global.jelly. Be sure that you are using the variable ‘instance’ to access the information.

And that’s all you need to develop a Publisher Hudson Plug-in. You can test it following the instructions in the Hudson wiki.

2 Comments on “Developing Custom Hudson Plugins: integrate with your own applications”

  1. #1 Erka
    on Jan 27th, 2009 at 4:53 pm

    This is a great article. I have followed the instructions above but the global settings are not persisting after restarting Hudson. What am I doing wrong? My configure method reads the variables and then saves them. However, I assume they should be read from XML after a Hudson restart??

    Any help would be appreciated.

  2. #2 Staplerschein
    on Oct 6th, 2010 at 12:39 pm

    thanks for this informations.. ;-)

Leave a Comment