Jenkins插件开发(2025最新)

183 阅读9分钟

 今天给大家分享一个Jenkins插件开发教程,获取更多运维相关知识可关注微信公众号运维日常手记

本次分享的插件主要完成以下两个需求:

1、在Jenkins触发构建的时候向飞书机器人发送消息;

2、插件支持自由风格的流水线和pipeline流水线;

环境准备

JDK版本:21

Maven:3.9.9

编辑器:IDEA

操作系统:windows

初始化项目

1、配置setting文件,在%USERPROFILE%.m2\目录下创建settings.xml文件。

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                              http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <pluginGroups>
    <pluginGroup>org.jenkins-ci.tools</pluginGroup>
  </pluginGroups>
  <profiles>
    <profile>
      <id>jenkins</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <repositories>
        <repository>
          <id>repo.jenkins-ci.org</id>
          <url>https://repo.jenkins-ci.org/public/</url>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <id>repo.jenkins-ci.org</id>
          <url>https://repo.jenkins-ci.org/public/</url>
        </pluginRepository>
      </pluginRepositories>
    </profile>
  </profiles>
</settings>

2、cmd控制台下执行如下命令进入交互式初始化项目,按提示选择4初始化一个helloworld项目,artifactId输入feishu,其他的回车使用默认值即可。

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

3、切换到上述步骤创建的项目,执行命令下载依赖。

cd feishu
mvn verify

4、运行helloworld项目,cmd控制台执行命令启动项目后,在浏览器访问http://localhost:8080即可访问到项目。

mvn hpi:run

5、配置IDEA,注意Maven home要配置成maven的安装目录,配置完成即可通过IDEA启动helloworld项目。

​编辑

目录结构

把helloworld的代码通通删除,然后开始开发我们自己的代码,最后完整的目录结构如下。

​编辑

插件开发要点

Jenkins为用户提供了各种扩展点,插件开发的本质其实就是去实现这些扩展点,加入我们自定义的逻辑,可以在这个文档找到各种扩展点Extension Points defined in Jenkins Core,每个扩展点都有相应的描述和插件代码示例,比如Notifier这个扩展点的作用就是在构建完成后执行相关操作,Implementations展示的是实现了该扩展点的社区插件。

​编辑

自由风格流水线

下面正式进入插件开发,我们的插件需要在自由风格和pipeline流水线上都可以使用,先来适配自由风格流水线,我们的需求是在Jenkins触发构建和构建完成时都能收到飞书通知,因此需要实现两个扩展点,一个是Notifier,还有一个是RunListener,Notifier的作用上面已经说过,RunListener的作用是可以在流水线启动和结束时触发对应的逻辑。

先实现发送消息到飞书的逻辑
逻辑比较简单,定义了一个send方法,它的作用是发送一个POST请求到飞书,jsonPayload定义的是一个飞书卡片消息结构,color用户定义卡片标题的颜色,title是卡片的标题,content是内容,jenkinsUrl是一个链接,用于点击卡片按钮跳转到Jenkins。

package io.jenkins.plugins.feishu.service;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;

public class FeishuService {
    private static final Logger LOG = Logger.getLogger(FeishuService.class.getName());
    private final String webHookUrl;

    public FeishuService(String webHookUrl) {
        this.webHookUrl = webHookUrl;
    }

    public void send(String color, String title, String content, String jenkinsUrl) throws Exception {
        // 构建卡片消息内容
        String jsonPayload = String.format(
                "{ "msg_type": "interactive", "card": { " + ""config": {"wide_screen_mode": true}, "
                        + ""header": {"template":"%s", "title": {"tag": "plain_text", "content": "%s"}}, "
                        + ""elements": [{"tag": "markdown", "content": "%s"}, "
                        + "{"tag":"action","actions":[{"tag":"button","text":{"tag":"plain_text","content":"查看日志"},"
                        + ""type":"primary","multi_url":{"url":"%s"}}]}]"
                        + "}}",
                color, title, content, jenkinsUrl);

        // 创建 HttpClient
        HttpClient client = HttpClient.newHttpClient();

        // 创建 POST 请求
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(webHookUrl))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(jsonPayload, StandardCharsets.UTF_8))
                .build();

        try {
            // 发送请求并获取响应
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            // 输出响应状态码与响应内容
            LOG.info("发送消息到飞书机器人成功, " + "响应状态码: " + response.statusCode() + "," + "响应内容: " + response.body());
        } catch (Exception e) {
            LOG.warning("发送消息到飞书机器人失败, " + "错误: " + e.getMessage());
            throw e;
        }
    }
}

发送到飞书的卡片消息长这样

​编辑

实现Notifier扩展点

我们定义了webHookUrl和message两个属性,这两个属性可以在Jenkins的UI上面配置,定义了一个send方法,send方法主要是定义发送消息的逻辑,onStarted和onCompleted方法则是调用send方法实现消息发送,注意到perform方法,perform是在流水线执行完成后自动触发的方法,因为我们希望流水线启动和完成都可以触发飞书通知,所以此处的perform方法不做任何处理直接返回true。

package io.jenkins.plugins.feishu;

import hudson.Extension;
import hudson.Launcher;
import hudson.model.*;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import io.jenkins.plugins.feishu.service.FeishuService;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import jenkins.tasks.SimpleBuildStep;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest2;

public class FeishuNotifier extends Notifier implements SimpleBuildStep {

    private static final Logger LOG = Logger.getLogger(FeishuNotifier.class.getName());

    private final String webHookUrl;
    private final String message;

    @DataBoundConstructor
    public FeishuNotifier(String webHookUrl, String message) {
        super();
        this.webHookUrl = webHookUrl;
        this.message = message;
    }

    public String getWebHookUrl() {
        return webHookUrl;
    }

    public String getMessage() {
        return message;
    }

    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    @SuppressWarnings("unchecked")
    public static FeishuNotifier getNotifier(AbstractProject project) {
        Map<Descriptor<Publisher>, Publisher> map = project.getPublishersList().toMap();
        for (Publisher publisher : map.values()) {
            if (publisher instanceof FeishuNotifier) {
                return (FeishuNotifier) publisher;
            }
        }
        return null;
    }

    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
            throws InterruptedException, IOException {
        //        return super.perform(build, launcher, listener);
        return true;
    }

    private boolean isEmpty(String s) {
        return s == null || s.trim().isEmpty();
    }

    private Result findPreviousBuildResult(AbstractBuild b) {
        do {
            b = b.getPreviousBuild();
            if (b == null || b.isBuilding()) {
                return null;
            }
        } while ((b.getResult() == Result.ABORTED) || (b.getResult() == Result.NOT_BUILT));
        return b.getResult();
    }

    private boolean isPreviousBuildSuccess(AbstractBuild build) {
        if (build.getResult() == Result.SUCCESS && findPreviousBuildResult(build) == Result.SUCCESS) {
            return true;
        }
        return false;
    }

    public void onStarted(AbstractBuild build, TaskListener listener) {
        if (getDescriptor().isDefaultSendNotificationOnStart()) {
            LOG.info("Prepare Feishu notification for build started...");
            send(build, listener, BuildPhase.STARTED);
        }
    }

    public void onCompleted(AbstractBuild build, TaskListener listener) {
        boolean notifyOnConsecutiveSuccesses = getDescriptor().isDefaultNotifyOnConsecutiveSuccesses();
        boolean previousBuildSuccessful = isPreviousBuildSuccess(build);
        if (notifyOnConsecutiveSuccesses || (!notifyOnConsecutiveSuccesses && !previousBuildSuccessful)) {
            LOG.info("Prepare Feishu notification for build completed...");
            send(build, listener, BuildPhase.COMPLETED);
        }
    }

    private void send(AbstractBuild build, TaskListener listener, BuildPhase phase) {
        String wb = isEmpty(webHookUrl) ? getDescriptor().getWebHookUrl() : webHookUrl;
        String content = isEmpty(message) ? getDescriptor().getMessage() : message;

        String jenkinsUrl = Jenkins.get().getRootUrl() + build.getConsoleUrl();


        long startTimeInMillis = build.getStartTimeInMillis();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String startTime = sdf.format(new Date(startTimeInMillis));

        String jobName = build.getProject().getName();

        String title = "";
        String color = "";

        if (phase == BuildPhase.STARTED) {
            title = "触发构建";
            color = "yellow";
            if (isEmpty(content)) {
                content = "流水线名称: " + jobName + "\n触发时间: " + startTime;
            }
        }
        if (phase == BuildPhase.COMPLETED) {
            long duration = build.getDuration();
            if (isEmpty(content)) {
                content = "流水线名称: " + jobName + "\n发布时间: " + startTime + "\n耗时: " + duration + "ms";
            }
            Result result = build.getResult();
            if (result == Result.SUCCESS) {
                title = "发布成功";
                color = "green";
            } else if (result == Result.ABORTED) {
                title = "发布取消";
                color = "grey";
            } else if (result == Result.FAILURE || result == Result.UNSTABLE) {
                title = "发布失败";
                color = "red";
            }
        }

        try {
            FeishuService client = new FeishuService(wb);
            client.send(color, title, content, jenkinsUrl);
        } catch (Exception e) {
            listener.getLogger().println("发送飞书通知失败");
        }
    }

    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {

        private String webHookUrl;

        private String message;
        private boolean defaultSendNotificationOnStart;
        private boolean defaultNotifyOnConsecutiveSuccesses = true;

        public DescriptorImpl() {
            super(FeishuNotifier.class);
            load();
        }

        @Override
        public String getDisplayName() {
            return "Feishu Notifier";
        }

        @Override
        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            return true;
        }

        @Override
        public boolean configure(StaplerRequest2 req, JSONObject formData) throws FormException {
            req.bindJSON(this, formData);
            save();
            return true;
        }

        public String getWebHookUrl() {
            return webHookUrl;
        }

        public void setWebHookUrl(String webHookUrl) {
            this.webHookUrl = webHookUrl;
        }

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

        public boolean isDefaultSendNotificationOnStart() {
            return defaultSendNotificationOnStart;
        }

        public boolean isDefaultNotifyOnConsecutiveSuccesses() {
            return defaultNotifyOnConsecutiveSuccesses;
        }

        public void setDefaultSendNotificationOnStart(boolean defaultSendNotificationOnStart) {
            this.defaultSendNotificationOnStart = defaultSendNotificationOnStart;
        }

        public void setDefaultNotifyOnConsecutiveSuccesses(boolean defaultSendNotificationsOnConsecutiveSuccesses) {
            this.defaultNotifyOnConsecutiveSuccesses = defaultSendNotificationsOnConsecutiveSuccesses;
        }
    }
}

实现RunListener扩展点

RunListener扩展点有两个方法,onStarted方法在流水线启动时触发,我们在onStarted方法调用上一步FeishuNotifier里面定义的onStarted方法,实现消息发送,onCompleted方法在流水线执行完成后触发,也是一样调用FeishuNotifier里面定义的onCompleted方法实现消息发送。

package io.jenkins.plugins.feishu;

import hudson.Extension;
import hudson.model.AbstractBuild;
import hudson.model.TaskListener;
import hudson.model.listeners.RunListener;

@Extension
public class BuildListener extends RunListener<AbstractBuild> {

    @Override
    public void onStarted(AbstractBuild abstractBuild, TaskListener listener) {
        FeishuNotifier trigger = FeishuNotifier.getNotifier(abstractBuild.getProject());

        if (trigger == null) {
            return;
        }

        trigger.onStarted(abstractBuild, listener);
    }

    @Override
    public void onCompleted(AbstractBuild abstractBuild, TaskListener listener) {
        FeishuNotifier trigger = FeishuNotifier.getNotifier(abstractBuild.getProject());

        if (trigger == null) {
            return;
        }

        trigger.onCompleted(abstractBuild, listener);
    }
}

添加UI界面

在resource目录下添加global.jelly和config.jelly文件,这些文件是用来实现配置的可视化的。

global.jelly:

<?jelly escape-by-default='true'?>
<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="Feishu Notification">

        <f:entry
                title="Webhook Url"
                help="/plugin/feishu/help-awsAccessKey.html"
                field="webHookUrl">

            <f:textbox />
        </f:entry>

        <f:entry
                title="Message"
                help="/plugin/feishu/help-message.html"
                field="message">

            <f:textbox />
        </f:entry>



        <f:entry
                title="Send notification also on start of build?"
                help="/plugin/feishu/help-defaultSendNotificationOnStart.html"
                field="defaultSendNotificationOnStart">

            <f:booleanRadio />
        </f:entry>

        <f:entry
                title="Send notifications on every build?"
                help="/plugin/feishu/help-defaultNotifyOnConsecutiveSuccesses.html"
                field="defaultNotifyOnConsecutiveSuccesses">

            <f:booleanRadio />
        </f:entry>
    </f:section>
</j:jelly>

config.jelly:

<?jelly escape-by-default='true'?>
<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="Webhook Url (overrides global default)"
            field="webHookUrl">

        <f:textbox />
    </f:entry>

    <f:entry
            title="Message (overrides global default)"
            field="message">

        <f:textbox />
    </f:entry>

</j:jelly>

在webapp目录下新件html文件添加参数帮助信息

help-webHookUrl.html:

<div>
    The default behavior is to send a notification on every build. If you would only like to receive notifications
    only on failures and when the build has returned to a successful state, disable this option.
</div>

help-message.html:

<div>
    <p>
        The feishu message.
    </p>
</div>

help-defaultSendNotificationOnStart.html:

<div>
    <p>
    If enabled, an feishu notification will not only be sent on build completion but also
    in addition when the build job is started. You might want to use the variable
    <tt>BUILD_PHASE (STARTED | COMPLETED)</tt> in your string templates.
    </p>
</div>

help-defaultNotifyOnConsecutiveSuccesses.html:

<div>
    The default behavior is to send a notification on every build. If you would only like to receive notifications
    only on failures and when the build has returned to a successful state, disable this option.
</div>

完成后启动项目在Jenkins的界面上就能看到如下配置

全局配置:

​编辑

创建一个自由风格的流水线测试

​编辑

添加构建后操作选择飞书插件,输入webhook地址,message可以不填

​编辑

保存后立即构建,将在飞书群里收到如下一条信息

​编辑

适配pipeline流水线

要让插件在pipeline里面也能使用需要实现step扩展点,pipeline需要我们显示的调用,所以定义了多个属性,核心逻辑是start方法,getFunctionName的返回值是插件在pipeline里面的语法。

package io.jenkins.plugins.feishu;

import hudson.Extension;
import hudson.model.Run;
import hudson.model.TaskListener;
import io.jenkins.plugins.feishu.service.FeishuService;
import java.io.Serial;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.workflow.steps.Step;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
import org.jenkinsci.plugins.workflow.steps.StepExecution;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.springframework.lang.NonNull;

public class FeishuStep extends Step {
    private static final Logger LOG = Logger.getLogger(FeishuNotifier.class.getName());
    private String webHookUrl;
    private String message;
    private String color;
    private String title;

    @DataBoundConstructor
    public FeishuStep(String webHookUrl,  String color, String title, String message) {
        this.webHookUrl = webHookUrl;
        this.message = message;
        this.color = color;
        this.title = title;
    }

    public String getWebHookUrl() {
        return webHookUrl;
    }

    @DataBoundSetter
    public void setWebHookUrl(String webHookUrl) {
        this.webHookUrl = webHookUrl;
    }

    public String getMessage() {
        return message;
    }

    @DataBoundSetter
    public void setMessage(String message) {
        this.message = message;
    }

    public String getColor() {
        return color;
    }

    @DataBoundSetter
    public void setColor(String color) {
        this.color = color;
    }

    public String getTitle() {
        return title;
    }

    @DataBoundSetter
    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public StepExecution start(StepContext context) throws Exception {
        return new FeishuStepExecution(this, context);
    }

    private static class FeishuStepExecution extends StepExecution {
        @Serial
        private static final long serialVersionUID = 1L;

        private final transient FeishuStep step;

        private FeishuStepExecution(FeishuStep step, StepContext context) {
            super(context);
            this.step = step;
        }

        private boolean isEmpty(String s) {
            return s == null || s.trim().isEmpty();
        }

        @Override
        public boolean start() throws Exception {
            StepContext context = this.getContext();
            LOG.info("Send feishu message.");

            try {
                Run<?, ?> run = getContext().get(Run.class);
                TaskListener listener = context.get(TaskListener.class);
                String jenkinsUrl = Jenkins.get().getRootUrl() + run.getConsoleUrl();
                String jobName = run.getParent().getDisplayName();
                long startTimeInMillis = run.getStartTimeInMillis();
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String startTime = sdf.format(new Date(startTimeInMillis));
                long duration = run.getDuration();
                String content = "流水线名称: " + jobName  +"\n发布时间: " + startTime + "\n耗时: " + duration + "ms";
                String msg = step.getMessage();
                if(!isEmpty(msg)){
                    content = msg;
                }
                String wb = step.getWebHookUrl();
                String color = step.getColor();
                String title = step.getTitle();
                FeishuService client = new FeishuService(wb);
                client.send(color, title, content, jenkinsUrl);
                listener.getLogger().println("webhook发送通知成功.");

                // 必须调用onSuccess或onFailure
                context.onSuccess(null);
                return true;
            } catch (Exception e) {
                context.onFailure(e);
                return false;
            }
        }
    }

    @Extension
    public static class DescriptorImpl extends StepDescriptor implements Serializable {

        public DescriptorImpl() {}

        @Override
        public Set<? extends Class<?>> getRequiredContext() {
            return new HashSet<>() {
                {
                    add(Run.class);
                    add(TaskListener.class);
                }
            };
        }


        @Override
        public String getFunctionName() {
            return "FeishuNotify";
        }

        @NonNull
        @Override
        public String getDisplayName() {
            return "Send Feishu message";
        }
    }
}

同样在resource的FeishuStep子目录创建config.jlly实现UI文档。

<?jelly escape-by-default='true'?>
<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="Webhook Url (overrides global default)"
            field="webHookUrl">

        <f:textbox />
    </f:entry>

    <f:entry
            title="Message (overrides global default)"
            field="message">

        <f:textbox />
    </f:entry>

    <f:entry
            title="Color (choice red, grey or green)"
            field="color">

        <f:textbox />
    </f:entry>

    <f:entry
            title="Title (feishu card message title)"
            field="title">

        <f:textbox />
    </f:entry>

</j:jelly>

创建一个pipeline流水线测试

​编辑

我们在流水线语法中可以找到插件的使用方法。

​编辑

​编辑

在pipeline script输入如下测试的pipeline脚本

pipeline {
    agent any
    stages {
        stage('Test Feishu') {
            steps {
                script {
                    try {
                        echo "Before FeishuNotify"
                        FeishuNotify(
                            webHookUrl: 'https://open.feishu.cn/open-apis/bot/v2/hook/08c94157-xxxxxxxxxxxxxx', 
                            message: '',
                            title: '发布成功',
                            color: 'green'
                        )
                        echo "After FeishuNotify"
                    } catch (Exception e) {
                        echo "Error in FeishuNotify: ${e.toString()}"
                    }
                }
            }
        }
    }
}

运行pipeline后将收到飞书通知

​编辑

至此完结,码字不易,喜欢文章请给个关注。

插件源码地址: feishu-plugin: Jenkins插件,用于向飞书机器人发送消息。