Jenkins : SCM plugin architecture

This section describes the basic architecture of an SCM plugin.

This page was out-of-date, but is being updated! The pollChanges API has been deprecated in favor of a new API that can track multiple changes coming in while a build is waiting in the quiet period. You can find more info in the SCM javadoc hudson.scm.SCM and see Subversion Plugin for a sample implementation https://svn.jenkins-ci.org/trunk/hudson/plugins/subversion.

The SCM class

All SCM plugins are subclasses of hudson.scm.SCM. This class has a few methods that an SCM implementation will want to override:

  • public PollingResult compareRemoteRevisionWith(Job<?,?> project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException
  • public void checkout(Run<?,?> build, Launcher launcher, FilePath workspace, TaskListener listener, File changelogFile, SCMRevisionState baseline) throws IOException, InterruptedException
  • public SCMRevisionState calcRevisionsFromBuild(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException

Including createChangeLogParser which is abstract and has to be overridden:

  • public abstract ChangeLogParser createChangeLogParser()

The basic means of interaction between the plugin and the rest of hudson are the compareRemoteRevisionWith and checkout methods, which are used, respectively, to poll the SCM for changes and to check out updated files. The createChangeLogParser method is supposed to return something that can parse an XML file with change set information.

Create the configuration options as normal fields, by adding fields to the SCM class they will be persisted when Hudson is stopped and started. If you have a field that should not be persisted to XML it should be marked as transient.

public class TeamFoundationServerScm extends SCM {

    private boolean cleanCopy;
    private String server;
    private String project;
    private String workspaceName;

    private String username;
    private String password;
    private String domain;
    ...
}

Methods

The following sections describe the methods of the SCM class in more detail.

constructor

Create a constructor that takes non-transient fields, and add the annotation @DataBoundConstructor to it. Using the annotation helps the Stapler class to find which constructor that should be used when automatically copying values from a web form to a class.

@DataBoundConstructor
public TeamFoundationServerScm(String server, String project, boolean cleanCopy,
          String username, String password, String domain, String workspaceName) {
    // Copying arguments to fields
}

compareRemoteRevisionWith

The compareRemoteRevisionWith method is expected to return if there has been any changes on the SCM repository or not in the form of a PollingResult object. It is up to the plug-in developer to create a new SCMRevisionState from the remote repository, and compare this with the baseline SCMRevisionState to determine whether there are changes (which lets Jenkins know it should checkout and build or not). It is also up to the developer to extend the SCMRevisionState class with details specific to the SCM system, and to provide a way to compare objects of this class. This example shows what a very basic compareRemoteRevisionsWith might look like if SCMRevisionState is extended by class MyCustomRevisionState which has method isNewerThan for comparing two MyCustomRevisionState.

@Override
public PollingResult compareRemoteRevisionWith(Job<?,?> project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState _baseline) throws IOException, InterruptedException
    ...
    MyCustomRevisionState currentRemoteState = new MyCustomRevisionState(remoteVersionNumberOrSomething);

    // Compare cached state with latest polled state
    Boolean changes = currentRemoteState.isNewerThan(baseline);

    // Return a PollingResult to tell Jenkins whether to checkout and build or not
    return new PollingResult(baseline, currentRemoteState, changes ? Change.SIGNIFICANT : Change.NONE);
}

checkout

The checkout method is expected to check out modified files into the project workspace. As a checkout is generally the first step for a build, the build object is made available. Also, the former SCMRevisionState, baseline is made available.

@Override
public void checkout(Run<?,?> build, Launcher launcher, FilePath workspace, TaskListener listener, File changelogFile, SCMRevisionState baseline) throws IOException, InterruptedException

While calcRevisionsFromBuild is designed to generate the SCMRevisionState, there is also the option of having checkout generate this state and having calcRevisionsFromBuild returning null. This is particularly useful if the generation of the SCMRevisionState overlaps with the checkout process and you want to optimize performance.

Simply set calcRevisionsFromBuild to return null, and add your SCMRevisionState to build during checkout:

@Override
public void checkout(Run<?,?> build, Launcher launcher, FilePath workspace, TaskListener listener, File changelogFile, SCMRevisionState baseline) throws IOException, InterruptedException
{
   // Checkout Stuff
   // Create your own SomethingRevisionState() object (make sure to create a class that extends SCMRevisionState)
   LRTRevisionState newBaselineState = new MyRevisionState(stateOfWorkspace);

   // Add your SCMRevisionState to your build as an action. Jenkins stores this value, so next time {{compareRemoteRevisionWith}} executes, _baseline will be this object
   build.addAction(newBaselineState);
}

@Override
public SCMRevisionState calcRevisionsFromBuild(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException
{
        return null;
}

Since you have access to your build, you can even add build parameters to your build during checkout. This is useful if you want to pass some information (like a version number) acquired during the polling process to a build job.

// add a line like this to checkout()
build.addAction(new ParametersAction(new StringParameterValue("myParameterName", "myParameterValue")));

calcRevisionsFromBuild

This method is called after checkout and generates and returns a SCMRevisionState, presumably based on the state of the workspace. Alternatively, this method can be set to return null and checkout can add SCMRevisionState to the build as an action.

createChangeLogParser

The checkout method should, besides checking out the modified files, write a changelog.xml file that contains the changes for a certain build. The changelog.xml file is specific for each SCM implementation, and the createChangeLogParser returns a parser that can parse the file and return a ChangeLogSet. See below section for more information.

getDescriptor

Returns the ScmDescriptor<?> for the SCM object. The ScmDescriptor is used to create new instances of the SCM. For more information see next section.

@Override
public SCMDescriptor<TeamFoundationServerScm> getDescriptor() {
    return PluginImpl.TFS_DESCRIPTOR;
}

The Descriptor class

The relationship of Descriptor and SCM (the describable) is akin to class and object. What this means is that the descriptor is used to create instances of the describable. Usually the Descriptor is an internal class in the SCM class named DescriptorImpl. The Descriptor should also contain the global configuration options as fields, just like the SCM class contains the configurations options for a job. The @Extension annotation tells Jenkins to register the descriptor.

public class TeamFoundationServerScm extends SCM {

    ...
    @Extension
    public static class DescriptorImpl extends SCMDescriptor {
        private String tfExecutable;

        protected DescriptorImpl() {
            super(TeamFoundationServerScm.class, null);
            load();
        }
        ...
    }
}

Methods

Methods that will be overriden

  • public String getDisplayName()
  • public boolean configure(StaplerRequest req) throws FormException

getDisplayName

Returns the name of the SCM, this is the name that will show up next to CVS and Subversion when configuring a job.

@Override
public String getDisplayName() {
    return "Team Foundation Server";
}

configure

The method is invoked when the global configuration page is submitted. In the method the data in the web form should be copied to the Descriptor's fields. To persist the fields to the global configuration XML file, the save() method must be called. Data is defined in the global.jelly page.

@Override
public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
    tfExecutable = Util.fixEmpty(req.getParameter("tfs.tfExecutable").trim());
    save();
    return true;
}

Jelly files

The jelly files for the Descriptor go into the src/main/resources/hudson/plugins/$plugin-name/$PluginScm/ folder where $plugin-name is the name of your plugin and $PluginScm is the plugin's SCM class implementation. For the Team Foundation Server plugin this path is /src/main/resources/hudson/plugins/tfs/TfsScm. The jelly files are the configuration view for the SCM class.

global.jelly

Contains global configuration that is displayed in system configuration page. Typical configuration parameters that is going to be used by all Hudson jobs, for example the path to the SCM tool.

<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="Team Foundation Server">
    <f:entry title="TF command line executable"  help="/plugin/tfs/tfExecutable.html">
      <f:textbox name="tfs.tfExecutable" value="${descriptor.tfExecutable}"
                 checkUrl="'${rootURL}/scm/TeamFoundationServerScm/executableCheck?value='+escape(this.value)"/>
    </f:entry>
  </f:section>
</j:jelly>

The field tfs.tfExecutable will be populated with the string from DescriptorImpl.getTfExecutable(). The method should return "tfs" by default.

public String getTfExecutable() {
    if (tfExecutable == null) {
        return "tfs";
    } else {
        return tfExecutable;
    }
}

The field tfs.tfsExecutable will also validate the entered tool path through checkUrl. When the user has entered a path (and moves the focus away from field) Hudson will call DescriptorImpl.doExectuableCheck to validate that the path can be found.

public FormValidation doExecutableCheck(@QueryParameter String value) {
    return FormValidation.validateExecutable(value);
}

config.jelly

Contains configuration for one Hudson job. Typical configuration parameters that used in a job, such as server URL, etc. The fields in the jelly file should correspond to the fields in the SCM class.

<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 title="Server URL" help="/plugin/tfs/server.html">
        <f:textbox name="tfs.server" value="${scm.server}"/>
    </f:entry>

    <f:entry title="Name of project" help="/plugin/tfs/project.html">
        <f:textbox name="tfs.project" value="${scm.project}"
             checkUrl="'/fieldCheck?errorText=${h.jsStringEscape(h.encode('%Project is mandatory.'))}&amp;value='+encode(this.value)"/>
    </f:entry>

    <f:advanced>

        <f:entry title="Clean copy">
            <f:checkbox name="tfs.cleanCopy" checked="${scm.cleanCopy}"/>
                If checked, Hudson will delete the directory and all its contents before downloading the files
                from the repository for every build.
        </f:entry>

        <f:entry title="Workspace name" help="/plugin/tfs/workspacename.html">
            <f:textbox name="tfs.workspaceName" value="${scm.workspaceName}"/>
        </f:entry>

    </f:advanced>
</j:jelly>

As with the Descriptor the web form fields are populate from the SCM object through properties, the value for "tfs.server" is retrieved from the TeamFoundationServerScm.getServer() method.

public String getServer() {
    return server;
}

public boolean isCleanCopy() {
    return cleanCopy;
}
....

The tfs.project field is mandatory and will display an error text below it if the field is empty. The error text is defined by the checkUrl attribute, and can be also used to validate numbers. The checkUrl can be used for more advanced validating such as testing the server if it exists and needs credentials.

HTML Help files

For each entry in a jelly file that has a help attribute Hudson will display a help icon to the right. When the icon is clicked the named HTML page will be inlined just below the entry field. The HTML page must reside in the src/main/webapp folder.

server.html
<div>
  <p>
	The name or URL of the team foundation server. If the server has been registered on the machine
	then it is only necessary to enter the name.
  </p>
</div>