Tuesday, December 18, 2018

Jenkins 7: Pipeline Support

Next step in our Jenkins tutorials is to add support for pipeline builds.

Jenkins Tutorials

For a list of all jenkins related tutorials see Jenkins Tutorials Overview.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: Adjusting the code

Good news first: our code (including jelly) from the previous tutorial is ready to be used in pipeline without change. There are some considerations to be taken when writing pipeline plugins, but we already took care of this.

In pipeline each build step needs a name to be addressed. By default this would be the class name and a call would look like this:
step([$class: 'HelloBuilder', buildMessage: 'Hello from pipeline'])

When using the @Symbol annotation on the Descriptor we can provide a nicer name for our build step.

Step 2: Adding dependencies to the execution environment

Our current test target does not support pipeline jobs as we did not add the right dependencies to the pom file so far. We will first see how to add dependencies in general, in the next step we will fix the build afterwards.

To add support for pipeline, add following definition to your pom.xml:
 <dependencies>
  <dependency>
   <groupId>org.jenkins-ci.plugins.workflow</groupId>
   <artifactId>workflow-aggregator</artifactId>
   <version>2.6</version>
   <scope>test</scope>
  </dependency>
 </dependencies>

We always need groupId, artifactId and a version. To get these parameters you would typically look up a plugin on the Jenkins plugin site. Locate the link to the source code (Github) and open the pom.xml of the corresponding plugin. There you will find definitions for groupId and artifactId.

Available versions can be found on the Jenkins Artifactory server (we added this server to our pom already). There navigate to the public folder, then follow the structure down, first opening folder from the groupId followed by the artifactId. For the pipeline dependency we would open public/org/jenkins-ci/plugins/workflow/workflow-aggregator. Latest version at the time of writing is 2.6.

Setting the scope to test means that we do not have a build dependency on the plugin. Instead we need it only deployed and enabled on our test instance.

When adding build dependencies you should run
mvn -DdownloadSources=true -DdownloadJavadocs=true -DoutputDirectory=target/eclipse-classes eclipse:eclipse
like we did in the first tutorial. It will update the .classpath file in your eclipse project, automatically adding required libraries to the build path. Take care that also the .project file gets rewritten!

Step 3: Dependency Resolution

We added our dependency, so everything should be done, right?
Wrong! The maven-enforcer-plugin verifies plugin dependencies for us and will detect some incompatibilities, eg:
Require upper bound dependencies error for org.jenkins-ci.plugins:script-security:1.39 paths to dependency are:
+-com.codeandme:builder.hello:1.0-SNAPSHOT
  +-org.jenkins-ci.plugins.workflow:workflow-aggregator:2.6
    +-org.jenkins-ci.plugins.workflow:workflow-support:2.20
      +-org.jenkins-ci.plugins:script-security:1.39
and
+-com.codeandme:builder.hello:1.0-SNAPSHOT
  +-org.jenkins-ci.plugins.workflow:workflow-aggregator:2.6
    +-org.jenkins-ci.plugins.workflow:workflow-durable-task-step:2.22
      +-org.jenkins-ci.plugins:script-security:1.39
and
+-com.codeandme:builder.hello:1.0-SNAPSHOT
  +-org.jenkins-ci.plugins.workflow:workflow-aggregator:2.6
    +-org.6wind.jenkins:lockable-resources:2.3
      +-org.jenkins-ci.plugins:script-security:1.26
...
This is one of multiple dependency conflicts detected. It seems that maven at first resolves the dependency with the lowest version number. As some plugins need a newer version, we need to resolve the dependency by our own.

The simplest way I found is to check for the highest version of the required plugin (in the upper case: script-security) and add it to our dependencies sections in the pom.xml file. I was hoping for some maven help on this process, but failed. So I ended up adding required dependencies manually until the build was satisfied.

You might run into other build problems, eg after adding a test dependency to the symbol-annotation plugin, my build started to fail, not being able to resolve Symbol.class anymore. Reason is that symbol-annotation is actually a build dependency rather than a test dependency. By binding it to the test scope only we broke the build.

Once you sorted out all dependencies (see resulting pom.xml) your test instance will be able to run pipeline jobs.

Step 4: Testing the Build Step in Pipeline

On your test instance create a new Pipeline and add following script:
node {
    stage('Greetings') {
        greet buildDelay: 'long', buildMessage: 'Hello pipeline build'
    }
}
Coding the command line for our build step can either be done manually or by using the Pipeline Syntax helper. The link is available right below the Script section in your job configuration. Jenkins makes use of our previous jelly definitions to display a visual helper for the Hello Build step. We may set all optional parameters on the UI and let Jenkins create the command line to be used in the pipeline script.

Monday, December 3, 2018

Jenkins 6: Advanced Configuration Area

Our build is advancing. Today we want to move optional fields into an advanced section and provide reasonable defaults for these entries.

Jenkins Tutorials

For a list of all jenkins related tutorials see Jenkins Tutorials Overview.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: Advanced UI Section

To simplify the job setup we now move all parameters except the build message to an advanced section.
The only thing necessary in the config.jelly file is to create the section and move all affected input elements into it:
 <f:entry title="Custom build message" field="buildMessage">
  <f:textbox default="${descriptor.getDefaultBuildMessage()}" />
 </f:entry>

 <f:advanced>
  <f:entry title="Fail this build" field="failBuild">
   <f:checkbox />
  </f:entry>


  <f:entry title="Build Delay" field="buildDelay">
   <f:select />
  </f:entry>
 </f:advanced>

Afterwards the UI looks like this:


Step 2: Java Refactoring

Basically we do not need to change anything in the Java code to make this work. However we want to prepare a little for pipeline builds, so we remove non-required parameters from the constructor and create separate setters for them. To make Jenkins aware of these setters, use the @DataBoundSetter annotation:
public class HelloBuilder extends Builder implements SimpleBuildStep {

 private boolean fFailBuild = false;

 private String fBuildMessage;

 private String fBuildDelay = "none";

 @DataBoundConstructor
 public HelloBuilder(String buildMessage) {
  fBuildMessage = buildMessage;
 }

 @DataBoundSetter
 public void setFailBuild(boolean failBuild) {
  fFailBuild = failBuild;
 }
 
 @DataBoundSetter
 public void setBuildDelay(String buildDelay) {
  fBuildDelay = buildDelay;
 }
}

Whenever a parameter is not required, remove it from the constructor and use a setter for it.

Jenkins 5: Combo Boxes

Combo boxes are the next UI element we will add to our builder.

Jenkins Tutorials

For a list of all jenkins related tutorials see Jenkins Tutorials Overview.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: UI Definition

In the config.jelly file we simply define that we want to use a combo box:
 <f:entry title="Build Delay" field="buildDelay">
  <f:select />
 </f:entry>
The definition does not contain entries to select. These will be populated by the Descriptor class.

Step 2: Item Definition

Jenkins will look for a method called doFill<field>Items in our Descriptor class to populate the combo. We are doing a first approach now to understand the scheme:
  public ListBoxModel doFillBuildDelayItems() {
   ListBoxModel model = new ListBoxModel();
   
   model.add(new Option("None", "none"));
   model.add(new Option("Short", "short"));
   model.add(new Option("Long", "long"));
   
   return model;
  }
ListBoxModel is basically an ArrayList of Option instances. The first string represents the text visible to the user, the second one the value that will actually be stored in our variable (see next step).

If we would populate the combo this way, the first item would always be selected by default, even if we re-open a job that was configured differently. The Option constructor allows for a third parameter defining the selected state. We then just need to know the value that got stored with the job definition. Therefore we can inject the desired query parameter into our method parameters:
  public ListBoxModel doFillBuildDelayItems(@QueryParameter String buildDelay) {
   ListBoxModel model = new ListBoxModel();

   model.add(new Option("None", "none", "none".equals(buildDelay)));
   model.add(new Option("Short", "short", "short".equals(buildDelay)));
   model.add(new Option("Long", "long" , "long".equals(buildDelay)));

   return model;
  }
Now buildDelay contains the value that got stored by the user when the build step was originally configured. By comparing its string representation we can set the right option in the combo. Typically combo options could be populated from an Enum. To reduce the risk of typos we could write a small helper to create our Options:
 public static Option createOption(Enum<?> enumOption, String jobOption) {
  return new Option(enumOption.toString(), enumOption.name(), enumOption.name().equals(jobOption));
 }

Step 3: Glueing it all together

Finally we need to extend our constructor with the new parameter. Then we can use it in our build step:
public class HelloBuilder extends Builder implements SimpleBuildStep {

 private String fBuildDelay;

 @DataBoundConstructor
 public HelloBuilder(boolean failBuild, String buildMessage, String buildDelay) {
  fBuildDelay = buildDelay;
 }

 @Override
 public void perform(Run<?, ?> run, FilePath workspace, Launcher launcher, TaskListener listener)
   throws InterruptedException, IOException {
  listener.getLogger().println("This is the Hello plugin!");
  listener.getLogger().println(getBuildMessage());

  switch (getBuildDelay()) {
  case "long":
   Thread.sleep(10 * 1000);
   break;

  case "short":
   Thread.sleep(3 * 1000);
   break;

  case "none":
   // fall through
  default:
   // nothing to do
  }

  if (isFailBuild())
   throw new AbortException("Build error forced by plugin settings");
 }

 public String getBuildDelay() {
  return fBuildDelay;
 }
}

Jenkins 4: Unit Tests

Now that our builder plugin is working we should start writing some unit tests for it.

Jenkins Tutorials

For a list of all jenkins related tutorials see Jenkins Tutorials Overview.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: Wrinting a simple test case

Jenkins tests can be written as JUnit tests. The test instance needed for execution tests can be created using a JUnit Rule.

Create a new JUnit Test Case com.codeandme.jenkins.builder.HelloBuilderTest in the src/test/java folder:
public class HelloBuilderTest {

 @Rule
 public JenkinsRule fJenkinsInstance = new JenkinsRule();
 
 @Test
 public void successfulBuild() throws Exception {
  HelloBuilder builder = new HelloBuilder(false, "JUnit test run");
  
  FreeStyleProject job = fJenkinsInstance.createFreeStyleProject();
  job.getBuildersList().add(builder);
  FreeStyleBuild build = fJenkinsInstance.buildAndAssertSuccess(job);
  
  fJenkinsInstance.assertLogContains("JUnit test run", build);
 }
}
In line 4 we create a test instance for our unit test. This instance is used from line 10 onwards to create and run our test job. The instance provides a set of assertion commands which we use to check the build result and the log output of the job execution.

You can run these tests as JUnit tests right from Eclipse or you can execute them via maven by running
mvn test

Step 2: A test expecting an execution fail

We use the same approach as before. To check for a failed build we need to run the build job a little bit different:
 @Test
 public void failedBuild() throws Exception {
  HelloBuilder builder = new HelloBuilder(true, "JUnit test fail");
  
  FreeStyleProject job = fJenkinsInstance.createFreeStyleProject();
  job.getBuildersList().add(builder);
  QueueTaskFuture<FreeStyleBuild> buildResult = job.scheduleBuild2(0);
  
  fJenkinsInstance.assertBuildStatus(Result.FAILURE, buildResult);
  fJenkinsInstance.assertLogContains("JUnit test fail", buildResult.get());
 }