第十三章:插件开发

开发自定义 Jenkins 插件,包括项目结构、API 使用、打包部署。

最后更新: 2024-01-15
页面目录

Jenkins 插件开发

本章介绍如何开发自定义 Jenkins 插件,满足特定业务需求。

插件开发概述

为什么开发插件?

  • 扩展 Jenkins 核心功能
  • 集成内部系统
  • 自动化特定工作流程
  • 提供定制化体验

插件架构

┌─────────────────────────────────────────────────────────────────┐
│                     Jenkins 插件架构                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   Jenkins Core                                                   │
│   ├── Extension Points (接口定义)                                 │
│   ├── Plugin Manager                                             │
│   └── Security                                                   │
│                                                                  │
│   ┌─────────────────────────────────────────────────────────┐    │
│   │                    Plugin                               │    │
│   │   ┌─────────────────────────────────────────────────┐   │    │
│   │   │  Extension Point Implementation                  │   │    │
│   │   │  • Builder (构建步骤)                            │   │    │
│   │   │  • Publisher (后处理)                            │   │    │
│   │   │  • Trigger (触发器)                             │   │    │
│   │   │  • SCM (源码管理)                                │   │    │
│   │   │  • NodeProperty (节点属性)                       │   │    │
│   │   └─────────────────────────────────────────────────┘   │    │
│   │                                                          │    │
│   │   ┌─────────────────────────────────────────────────┐   │    │
│   │   │  Jelly/Groovy Views                             │   │    │
│   │   │  • config.jelly (配置页面)                       │   │    │
│   │   │  • global.jelly (全局配置)                       │   │    │
│   │   └─────────────────────────────────────────────────┘   │    │
│   └─────────────────────────────────────────────────────────┘    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

开发环境准备

1. 安装依赖

# Ubuntu/Debian
sudo apt update
sudo apt install -y openjdk-11-jdk maven git

# macOS
brew install openjdk@11 maven

2. JDK 版本要求

Jenkins 版本 JDK 要求
Jenkins 2.357+ JDK 8, JDK 11
Jenkins 2.426.1+ JDK 11, JDK 17
Jenkins 2.463+ JDK 11, JDK 17, JDK 21

3. Maven 配置

<!-- ~/.m2/settings.xml -->
<settings>
  <pluginGroups>
    <pluginGroup>org.jenkins-ci.tools</pluginGroup>
  </pluginGroups>
  
  <profiles>
    <profile>
      <id>jenkins</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <properties>
        <jenkins.version>2.440.3</jenkins.version>
        <java.level>11</java.level>
      </properties>
    </profile>
  </profiles>
</settings>

创建第一个插件

1. 使用 Maven Archetype

# 创建插件项目
mvn -B archetype:generate \
    -DgroupId=com.example \
    -DartifactId=jenkins-plugin-tutorial \
    -DarchetypeGroupId=org.jenkins-ci.tools \
    -DarchetypeArtifactId=jenkins-plugin-archetype \
    -DarchetypeVersion=1.13

# 或者使用网易源
mvn -B archetype:generate \
    -DarchetypeGroupId=org.jenkins-ci.tools \
    -DarchetypeArtifactId=jenkins-plugin-archetype \
    -DarchetypeVersion=1.13 \
    -DgroupId=com.example \
    -DartifactId=hello-world-plugin \
    -Dpackage=com.example.helloworld \
    -Dversion=1.0-SNAPSHOT

2. 项目结构

hello-world-plugin/
├── pom.xml                    # Maven 配置
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/
│   │   │       └── helloworld/
│   │   │           ├── HelloWorldBuilder.java      # 构建器实现
│   │   │           └── HelloWorldBuilder.DescriptorImpl  # 描述符
│   │   └── resources/
│   │       └── com/example/
│   │           └── helloworld/
│   │               ├── HelloWorldBuilder/
│   │               │   ├── config.jelly            # 配置页面
│   │               │   └── help-name.html
│   │               └── index.jelly                 # 插件主页
│   └── test/
│       └── java/
│           └── com/example/
│               └── helloworld/
│                   └── HelloWorldBuilderTest.java  # 单元测试
└── target/                   # 编译输出

3. pom.xml 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.jenkins-ci.plugins</groupId>
        <artifactId>plugin</artifactId>
        <version>4.77</version>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>hello-world-plugin</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>hpi</packaging>
    
    <name>Hello World Plugin</name>
    <description>A simple Hello World plugin for Jenkins</description>
    
    <properties>
        <jenkins.version>2.440.3</jenkins.version>
        <java.level>11</java.level>
    </properties>
    
    <dependencies>
        <!-- Jenkins Test Harness -->
        <dependency>
            <groupId>org.jenkins-ci.plugins</groupId>
            <artifactId>structs</artifactId>
            <version>1.25</version>
        </dependency>
        
        <!-- Annotation API -->
        <dependency>
            <groupId>org.jenkins-ci</groupId>
            <artifactId>annotation-indexer</artifactId>
            <version>1.17</version>
        </dependency>
        
        <!-- Stapler for Jelly views -->
        <dependency>
            <groupId>org.jenkins-ci</groupId>
            <artifactId> Stapler</artifactId>
            <version>1.320</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.jenkins-ci.tools</groupId>
                <artifactId>maven-hpi-plugin</artifactId>
                <version>3.51</version>
                <configuration>
                    <dependencies>
                        <dependency>
                            <groupId>org.jenkins-ci.plugins</groupId>
                            <artifactId>credentials</artifactId>
                            <version>2.6.1</version>
                        </dependency>
                    </dependencies>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

开发步骤

1. 定义扩展点实现

// HelloWorldBuilder.java
package com.example.helloworld;

import hudson.Launcher;
import hudson.Extension;
import hudson.model.AbstractProject;
import hudson.model.Descriptor;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.Builder;
import hudson.tasks.BuildStepDescriptor;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;

public class HelloWorldBuilder extends Builder {
    
    // 用户配置的属性
    private final String name;
    
    // 数据绑定构造函数
    @DataBoundConstructor
    public HelloWorldBuilder(String name) {
        this.name = name;
    }
    
    // Getter 方法
    public String getName() {
        return name;
    }
    
    // 构建步骤实现
    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher,
                          TaskListener listener) {
        // 获取输出流
        PrintStream logger = listener.getLogger();
        
        // 输出日志
        logger.println("Hello, " + name + "!");
        logger.println("Build number: " + build.getNumber());
        logger.println("Build workspace: " + build.getWorkspace());
        
        // 如果需要返回失败
        // return false;
        
        return true;
    }
    
    // 描述符实现
    @Extension
    @Symbol("helloWorld")
    public static class DescriptorImpl extends BuildStepDescriptor<Builder> {
        
        // 显示名称
        @Override
        public String getDisplayName() {
            return "Say Hello World";
        }
        
        // 判断是否适用于项目类型
        @Override
        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            return true;
        }
    }
}

2. 创建配置页面

<!-- src/main/resources/com/example/helloworld/HelloWorldBuilder/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="Name" field="name">
        <f:textbox default="World"/>
    </f:entry>
    
    <f:description>
        This plugin prints "Hello, World!" to the build log.
    </f:description>
</j:jelly>

3. 创建帮助页面

<!-- src/main/resources/com/example/helloworld/HelloWorldBuilder/help-name.html -->
<div>
    <p>
        Enter the name to greet. This will be printed in the build log.
    </p>
    <p>
        Example: <code>Jenkins User</code>
    </p>
</div>

常用扩展点

Builder (构建步骤)

public class MyBuilder extends Builder {
    
    @DataBoundConstructor
    public MyBuilder(String param1, boolean param2) {
        // ...
    }
    
    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher,
                          TaskListener listener) {
        // 实现构建逻辑
        return true;
    }
    
    @Extension
    public static class DescriptorImpl extends BuildStepDescriptor<Builder> {
        // ...
    }
}

Publisher (后处理步骤)

public class MyPublisher extends Publisher {
    
    @DataBoundConstructor
    public MyPublisher(String config) {
        // ...
    }
    
    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher,
                           TaskListener listener) {
        // 发布构建产物或通知
        return true;
    }
    
    @Extension
    public static class DescriptorImpl extends RecorderDescriptor<Publisher> {
        // ...
    }
}

Trigger (触发器)

public class MyTrigger extends Trigger<AbstractProject> {
    
    @DataBoundConstructor
    public MyTrigger(String cron) {
        super(cron);
    }
    
    @Override
    public void start(AbstractProject project, boolean newInstance) {
        // 启动触发器
    }
    
    @Extension
    public static class DescriptorImpl extends TriggerDescriptor<AbstractProject> {
        // ...
    }
}

Recorder (构建记录器)

public class MyRecorder extends Recorder {
    
    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher,
                          TaskListener listener) {
        // 记录构建信息
        return true;
    }
    
    @Extension
    public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
        // ...
    }
}

单元测试

创建测试类

// src/test/java/com/example/helloworld/HelloWorldBuilderTest.java
package com.example.helloworld;

import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.FreeStyleProject;
import hudson.tasks.Builder;
import org.jenkins-ci.plugins.structs.describable.UninstantiatedDescribable;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.HudsonTestCase;

import java.io.IOException;

import static org.junit.Assert.*;

public class HelloWorldBuilderTest {
    
    @Rule
    public JenkinsRule j = new JenkinsRule();
    
    @Test
    public void testConfigRoundtrip() throws Exception {
        // 创建项目
        FreeStyleProject project = j.createFreeStyleProject();
        
        // 添加构建步骤
        project.getBuildersList().add(new HelloWorldBuilder("Test User"));
        
        // 验证配置
        j.configRoundtrip(project);
        
        // 验证构建步骤
        HelloWorldBuilder builder = project.getBuildersList().get(HelloWorldBuilder.class);
        assertEquals("Test User", builder.getName());
    }
    
    @Test
    public void testBuilderExecution() throws Exception {
        // 创建项目
        FreeStyleProject project = j.createFreeStyleProject();
        project.getBuildersList().add(new HelloWorldBuilder("Jenkins"));
        
        // 运行构建
        FreeStyleBuild build = j.buildAndAssertSuccess(project);
        
        // 验证日志输出
        j.assertLogContains("Hello, Jenkins!", build);
    }
}

运行测试

# 运行所有测试
mvn test

# 运行单个测试
mvn test -Dtest=HelloWorldBuilderTest

# 运行带调试
mvn test -Dtest=HelloWorldBuilderTest -X

# 跳过测试
mvn clean package -DskipTests

打包与部署

打包插件

# 打包为 .hpi 文件
mvn package

# 查看输出
ls -la target/*.hpi

本地安装

# 复制到 Jenkins 插件目录
cp target/hello-world-plugin.hpi /var/lib/jenkins/plugins/

# 重启 Jenkins
sudo systemctl restart jenkins

通过 Web 上传

系统管理 → 插件管理 → 高级
    上传插件: 选择 .hpi 文件

国际化 (i18n)

添加翻译文件

# src/main/resources/com/example/helloworld/HelloWorldBuilder.properties
# 默认 (英文)
DisplayName=Say Hello World
Name=Name
Name.help=Enter the name to greet

# 中文翻译
# src/main/resources/com/example/helloworld/HelloWorldBuilder_zh_CN.properties
DisplayName=\u8BF4\u4E16\u754C\u4F60\u597D
Name=\u59D3\u540D
Name.help=\u8F93\u5165\u8981\u6253\u62DB\u547C\u7684\u59D3\u540D

发布到 Jenkins 插件仓库

1. 准备发布

# 更新版本号
mvn versions:set -DnewVersion=1.0

# 确保没有 SNAPSHOT
mvn release:prepare

2. 发布

# 发布到 Maven Central
mvn release:perform

3. Jenkins Wiki 文档

https://wiki.jenkins-ci.org/display/JENKINS/Hello+World+Plugin

常见 API 使用

1. 获取构建信息

// 获取环境变量
EnvVars envVars = build.getEnvironment(listener);
String branch = envVars.get("BRANCH_NAME");

// 获取构建参数
Map<String, String> params = build.getBuildVariables();

// 获取工作区
FilePath workspace = build.getWorkspace();

2. 使用凭据

import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;

public boolean perform(AbstractBuild build, TaskListener listener) {
    // 获取凭据
    List<StandardUsernamePasswordCredentials> credentials = 
        CredentialsProvider.lookupCredentials(
            StandardUsernamePasswordCredentials.class,
            build.getProject(),
            build.getCause(hudson.model.UserIdCause.class)
        );
    
    // 使用凭据
    String username = credentials.get(0).getUsername();
    String password = credentials.get(0).getPassword().getPlainText();
    
    return true;
}

3. 发送通知

// 发送邮件
String recipient = "user@example.com";
String subject = "Build " + build.getNumber() + " completed";
String body = "Build result: " + build.getResult();

try {
    Mailer.descriptor().sendMail(
        recipient, subject, body, 
        build.getEnvironment().get("DEFAULT_RECIPIENTS")
    );
} catch (Exception e) {
    listener.getLogger().println("Failed to send email: " + e.getMessage());
}

下一步

恭喜您完成了 Jenkins 权威教程的全部内容!

👉 返回教程首页


📚 继续学习