Writing a Jenkins Plugin – 编写一个Jenkins插件

最后修改: 2018年 1月 17日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Jenkins is an open-source Continuous Integration server, which enables to create a custom plugin creation for particular task/environment.

Jenkins是一个开源的持续集成服务器,它能够为特定的任务/环境创建一个自定义的插件。

In this article, we’ll go through the whole process of creating an extension which adds statistics to the build output, namely, number of classes and lines of code.

在这篇文章中,我们将经历创建一个扩展的全过程,该扩展将统计数字添加到构建输出中,即类的数量和代码行的数量。

2. Setup

2.设置

The first thing to do is to set up the project. Luckily, Jenkins provides convenient Maven archetypes for that.

要做的第一件事是设置项目。幸运的是,Jenkins为此提供了方便的Maven原型

Just run the command below from a shell:

只需从壳中运行下面的命令。

mvn archetype:generate -Dfilter=io.jenkins.archetypes:plugin

We’ll get the following output:

我们将得到以下输出。

[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart
  (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: remote -> io.jenkins.archetypes:empty-plugin (Skeleton of
  a Jenkins plugin with a POM and an empty source tree.)
2: remote -> io.jenkins.archetypes:global-configuration-plugin
  (Skeleton of a Jenkins plugin with a POM and an example piece
  of global configuration.)
3: remote -> io.jenkins.archetypes:hello-world-plugin
  (Skeleton of a Jenkins plugin with a POM and an example build step.)

Now, choose the first option and define group/artifact/package in the interactive mode. After that, it’s necessary to make refinements to the pom.xml – as it contains entries such as <name>TODO Plugin</name>.

现在,选择第一个选项,在交互模式下定义组/工件/包。之后,有必要对pom.xml进行完善–因为它包含诸如<name>TODO Plugin</name>等条目。

3. Jenkins Plugin Design

3.Jenkins插件的设计

3.1. Extension Points

3.1.扩展点

Jenkins provides a number of extension points. These are interfaces or abstract classes which define contracts for particular use-cases and allow other plugins to implement them.

Jenkins提供了许多扩展点。这些是接口或抽象类,它们为特定的用例定义了合同,并允许其他插件实现它们。

For example, every build consists of a number of steps, e.g. “Checkout from VCS”, “Compile”, “Test”, “Assemble”, etc. Jenkins defines hudson.tasks.BuildStep extension point, so we can implement it to provide a custom step which can be configured.

例如,每个构建都由若干步骤组成,例如“从VCS签出”“编译”“测试”“组装”等等。Jenkins定义了hudson.tasks.BuildStep扩展点,所以我们可以实现它来提供一个可以配置的自定义步骤。

Another example is hudson.tasks.BuildWrapper – this allows us to define pre/post actions.

另一个例子是hudson.tasks.BuildWrapper–这允许我们定义前/后的动作。

We also have a non-core Email Extension plugin that defines the hudson.plugins.emailext.plugins.RecipientProvider extension point, which allows providing email recipients. An example implementation is available in here: hudson.plugins.emailext.plugins.recipients.UpstreamComitterRecipientProvider.

我们还有一个非核心的电子邮件扩展插件,它定义了hudson.plugins.emailext.plugins.RecipientProvider扩展点,它允许提供电子邮件收件人。这里有一个实现的例子。hudson.plugins.emailext.plugins.records.UpstreamComitterRecipientProvider

Note: there is a legacy approach where plugin class needs to extend hudson.Plugin. However, it’s now recommended to use extension points instead.

注意:有一种传统的方法,即插件类需要扩展hudson.Plugin。然而,现在建议使用扩展点来代替。

3.2. Plugin Initialization

3.2.插件初始化

It’s necessary to tell Jenkins about our extension and how it should be instantiated.

有必要告诉Jenkins关于我们的扩展以及它应该如何被实例化。

First, we define a static inner class within the plugin and mark it using the hudson.Extension annotation:

首先,我们在插件中定义一个静态的内部类,并使用hudson.Extension注解来标记它。

class MyPlugin extends BuildWrapper {
    @Extension
    public static class DescriptorImpl 
      extends BuildWrapperDescriptor {

        @Override
        public boolean isApplicable(AbstractProject<?, ?> item) {
            return true;
        }

        @Override
        public String getDisplayName() {
            return "name to show in UI";
        }
    }
}

Secondly, we need to define a constructor to be used for plugin’s object instantiation and mark it by the org.kohsuke.stapler.DataBoundConstructor annotation.

其次,我们需要定义一个用于插件对象实例化的构造函数,并通过org.kohsuke.stapler.DataBoundConstructor注解来标记它。

It’s possible to use parameters for it. They’re shown in UI and are automatically delivered by Jenkins.

它可以使用参数。它们会显示在用户界面中,并由Jenkins自动提供。

E.g. consider the Maven plugin:

例如,考虑Maven插件

@DataBoundConstructor
public Maven(
  String targets,
  String name,
  String pom,
  String properties,
  String jvmOptions,
  boolean usePrivateRepository,
  SettingsProvider settings,
  GlobalSettingsProvider globalSettings,
  boolean injectBuildVariables) { ... }

It’s mapped to the following UI:

它被映射到以下用户界面。

maven settings

It’s also possible to use org.kohsuke.stapler.DataBoundSetter annotation with setters.

也可以使用org.kohsuke.stapler.DataBoundSetter注解的设置器。

4. Plugin Implementation

4.插件的实施

We intend to collect basic project stats during a build, so, hudson.tasks.BuildWrapper is the right way to go here.

我们打算在构建过程中收集基本的项目统计信息,因此,hudson.tasks.BuildWrapper是这里的正确做法。

Let’s implement it:

让我们来实施它。

class ProjectStatsBuildWrapper extends BuildWrapper {

    @DataBoundConstructor
    public ProjectStatsBuildWrapper() {}

    @Override
    public Environment setUp(
      AbstractBuild build,
      Launcher launcher,
      BuildListener listener) {}

    @Extension
    public static class DescriptorImpl extends BuildWrapperDescriptor {

        @Override
        public boolean isApplicable(AbstractProject<?, ?> item) {
            return true;
        }

        @Nonnull
        @Override
        public String getDisplayName() {
            return "Construct project stats during build";
        }

    }
}

Ok, now we need to implement the actual functionality.

好了,现在我们需要实现实际的功能。

Let’s define a domain class for the project stats:

让我们为项目统计定义一个域类。

class ProjectStats {

    private int classesNumber;
    private int linesNumber;

    // standard constructors/getters
}

And write the code which builds the data:

并编写构建数据的代码。

private ProjectStats buildStats(FilePath root)
  throws IOException, InterruptedException {
 
    int classesNumber = 0;
    int linesNumber = 0;
    Stack<FilePath> toProcess = new Stack<>();
    toProcess.push(root);
    while (!toProcess.isEmpty()) {
        FilePath path = toProcess.pop();
        if (path.isDirectory()) {
            toProcess.addAll(path.list());
        } else if (path.getName().endsWith(".java")) {
            classesNumber++;
            linesNumber += countLines(path);
        }
    }
    return new ProjectStats(classesNumber, linesNumber);
}

Finally, we need to show the stats to end-users. Let’s create an HTML template for that:

最后,我们需要向终端用户展示统计信息。让我们为之创建一个HTML模板。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>$PROJECT_NAME$</title>
</head>
<body>
Project $PROJECT_NAME$:
<table border="1">
    <tr>
        <th>Classes number</th>
        <th>Lines number</th>
    </tr>
    <tr>
        <td>$CLASSES_NUMBER$</td>
        <td>$LINES_NUMBER$</td>
    </tr>
</table>
</body>
</html>

And populate it during the build:

并在构建过程中对其进行填充。

public class ProjectStatsBuildWrapper extends BuildWrapper {
    @Override
    public Environment setUp(
      AbstractBuild build,
      Launcher launcher,
      BuildListener listener) {
        return new Environment() {
 
            @Override
            public boolean tearDown(
              AbstractBuild build, BuildListener listener)
              throws IOException, InterruptedException {
 
                ProjectStats stats = buildStats(build.getWorkspace());
                String report = generateReport(
                  build.getProject().getDisplayName(),
                  stats);
                File artifactsDir = build.getArtifactsDir();
                String path = artifactsDir.getCanonicalPath() + REPORT_TEMPLATE_PATH;
                File reportFile = new File("path");
                // write report's text to the report's file
            }
        };
    }
}

5. Usage

5.使用方法

It’s time to combine everything we’ve created so far – and see it in action.

现在是时候把我们迄今为止所创造的一切结合起来了–并看到它在行动。

It’s assumed that Jenkins is up and running in the local environment. Please refer to the installation details otherwise.

假设Jenkins已经在本地环境中启动并运行。否则请参考安装细节

5.1. Add the Plugin to Jenkins

5.1.向Jenkins添加插件

Now, let’s build our plugin:

现在,让我们来建立我们的插件。

mvn install

This will create a *.hpi file in the target directory. We need to copy it to the Jenkins plugins directory (~/.jenkins/plugin by default):

这将在target目录下创建一个*.hpi文件。我们需要把它复制到Jenkins的插件目录(~/.jenkins/plugin,默认)。

cp ./target/jenkins-hello-world.hpi ~/.jenkins/plugins/

Finally, let’s restart the server and ensure that the plugin is applied:

最后,让我们重新启动服务器并确保该插件被应用。

  1. Open CI dashboard at http://localhost:8080
  2. Navigate to Manage Jenkins | Manage Plugins | Installed
  3. Find our plugin

plugin enabled

5.2. Configure Jenkins Job

5.2.配置Jenkins作业

Let’s create a new job for an open-source Apache commons-lang project and configure the path to its Git repo there:

让我们为一个开源的Apache commons-lang项目创建一个新的作业,并在那里配置其Git repo的路径。

common lang git

We also need to enable our plugin for that:

我们还需要为此启用我们的插件。

enable for project

5.3. Check the Results

5.3.检查结果

We’re all set now, let’s check how it works.

我们现在都准备好了,让我们看看它是如何工作的。

We can build the project and navigate to the results. We can see that a stats.html file is available here:

我们可以建立该项目并浏览结果。我们可以看到,这里有一个stats.html文件。

commons lang build 1

Let’s open it:

让我们来打开它。

commons lang result

That’s what we expected – a single class which has three lines of code.

这就是我们所期望的–一个只有三行代码的类。

6. Conclusion

6.结论

In this tutorial, we created a Jenkins plugin from scratch and ensured that it works.

在本教程中,我们从头开始创建了一个Jenkins插件,并确保它能够工作。

Naturally, we didn’t cover all aspects of the CI extensions development, we just provided a basic overview, design ideas and an initial setup.

当然,我们并没有涵盖CI扩展开发的所有方面,我们只是提供了一个基本的概述、设计思路和初步的设置。

And, as always, the source code can be found over on GitHub.

而且,像往常一样,可以在GitHub上找到源代码