Jenkins pipeline

Jenkins Plugin as build step and pipeline compatible

If you are writing a Jenkins plugin and you want it to run as a build step and to work smoothly with pipelines, the following article shows you how to do it. I will show how I migrated a build step plugin to also work as pipeline plugin.

In a previous article, I gave a brief introduction on how to build a build step plugin for Jenkins. In this article, we will have a look on the steps needed to make it work also for Jenkins pipelines.

A Jenkins Pipeline is a collection of plugins which supports implementing and integrating continuous delivery pipelines into Jenkins. Pipelines consist of multiple steps to build, test, and deploy applications. The definition of a Jenkins Pipeline is typically defined in a Jenkinsfile which you can check in into your version control system alongside your code.

From build step to pipeline compatible

First, the main class has to implement a new interface, the SimpleBuildStep. A build step is one step of the whole build process. Build steps are instantiated when the user saves the job configuration.

Before:
public class ProjectBuilder extends Builder{
}

I also made it Serializable and added the @Extension annotation. By putting it on the class, Hudson will create an instance of it and register it to the extension list.

@Extension
public class ProjectBuilder extends Builder implements SimpleBuildStep, Serializable {
    private final String project;

    @DataBoundConstructor
    public SofitProjectBuilder(String project) {
        this.project = project;
    }


    public final String getProject() {
        return project;
    }
}

As we have added a new interface, the perform method has also changed. The AbstractBuild and BuildListener parameters are replaced with Run, FilePath, and TaskListener. A Run is basically a job execution.

Before:
@Override
public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) {
}

Wherever you used the build property, it has to be replaced with run, workspace.

The FilePath gives the location of the remote workspace. Instead of getting the workspace location with FilePath workspaceOnSlave = build.getWorkspace() you can simply use the workspace property passed in with the constructor.

The TaskListener replaces the BuildListener. This interface records the progress of the operation and receives events about failures, build information, and so on. Their APIs are pretty similar.

Optionally, I have added the @Nonnull assertation to this parameters. It verifies that the annotated element must not be null.

@Override
public void perform(@Nonnull Run<?, ?> run, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException {
}

To add this plugin as a build step to a pipeline, it has to extend the BuildStepDescriptor. It is reflected by config.jelly for the view. To register the Builder from the plugin, we put @Extension on the descriptor.

Before:
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
}

In the updated version, I have added a Symbol to the descriptor.

@Extension
@Symbol("integrationPlugin")
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {

By default, scripts making use of your plugin will need to refer to the class name of your extension. For example, if you defined the Java class as ProjectBuilder, you would call it with

step([$class: 'ProjectBuilder', project: 'some-project'])

By adding @Symbol to your Descriptor, you uniquely identify the plugin among the extensions of its type (here SimpleBuildStep). It can be now executed with this snippet.

integrationPlugin some-project

Pipeline example

The following snippet shows a minimal Jenkins pipeline example. In the second stage, it calls the plugin.

node('pipeline_example') { 
    stage('Clean workspace') {
        deleteDir()
    }
    
    stage('Run plugin') {
        integrationPlugin project: 'some-project'
    }

    stage('List directory') {
        sh "ls -a1"
    }
    
    stage('Execute scripts') {
        sh "my_script.cmd"
    }
}

Further notes

Make sure your Jenkins version is at least 1.580.1 (LTS). Also have a look at the Jenkins page with more guidelines on how to make your plugin pipeline compatible.

A minimal working example can be found in my Github repository.