准备工作
因为
Jenkins
是基于Java
开发的,插件开发首先需要安装JDK
和Maven
Maven
安装完成后,需要修改配置文件,内容如下
<settings>
<pluginGroups>
<pluginGroup>org.jenkins-ci.tools</pluginGroup>
</pluginGroups>
<profiles>
<!-- Give access to Jenkins plugins -->
<profile>
<id>jenkins</id>
<activation>
<activeByDefault>true</activeByDefault> <!-- change this to false, if you don't like to have it on per default -->
</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>
<mirrors>
<mirror>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
<mirrorOf>m.g.o-public</mirrorOf>
</mirror>
</mirrors>
</settings>
插件工程初始化
1、创建插件
找一个目录存放你想要方式
Jenkins
插件源码的目录,在此处打开命令行终端,键人如下命令,这条命令可以让我们使用与Jenkins
相关的原型项目中的一个并生成
mvn -U archetype:generate -Dfilter=io.jenkins.archetypes:
执行日志如下,根据提示依次输入选择原型,版本,输出文件名称,版本号,确认
[INFO] Scanning for projects...
Downloading 。。。。。。
[INFO]
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:3.2.0:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:3.2.0:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO]
[INFO]
[INFO] --- maven-archetype-plugin:3.2.0:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Interactive mode
[WARNING] No archetype found in remote catalog. Defaulting to internal catalog
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: local -> io.jenkins.archetypes:empty-plugin (Skeleton of a Jenkins plugin with a POM and an empty source tree.)
2: local -> io.jenkins.archetypes:global-configuration-plugin (Skeleton of a Jenkins plugin with a POM and an example piece of global configuration.)
3: local -> io.jenkins.archetypes:global-shared-library (Uses the Jenkins Pipeline Unit mock library to test the usage of a Global Shared Library)
4: local -> io.jenkins.archetypes:hello-world-plugin (Skeleton of a Jenkins plugin with a POM and an example build step.)
5: local -> io.jenkins.archetypes:scripted-pipeline (Uses the Jenkins Pipeline Unit mock library to test the logic inside a Pipeline script.)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 4
Choose io.jenkins.archetypes:hello-world-plugin version:
1: 1.1
2: 1.2
3: 1.3
4: 1.4
5: 1.5
6: 1.6
7: 1.7
8: 1.8
9: 1.9
10: 1.10
Choose a number: 10: 10
Downloading 。。。。。
[INFO] Using property: groupId = unused
Define value for property 'artifactId': JenkinsPluginDemo
Define value for property 'version' 1.0-SNAPSHOT: : 1.0
[INFO] Using property: package = io.jenkins.plugins.sample
[INFO] Using property: hostOnJenkinsGitHub = true
Confirm properties configuration:
groupId: unused
artifactId: JenkinsPluginDemo
version: 1.0
package: io.jenkins.plugins.sample
hostOnJenkinsGitHub: true
Y: : y
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: hello-world-plugin:1.10
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: unused
[INFO] Parameter: artifactId, Value: JenkinsPluginDemo
[INFO] Parameter: version, Value: 1.0
[INFO] Parameter: package, Value: io.jenkins.plugins.sample
[INFO] Parameter: packageInPathFormat, Value: io/jenkins/plugins/sample
[INFO] Parameter: version, Value: 1.0
[INFO] Parameter: package, Value: io.jenkins.plugins.sample
[INFO] Parameter: groupId, Value: unused
[INFO] Parameter: hostOnJenkinsGitHub, Value: true
[INFO] Parameter: artifactId, Value: JenkinsPluginDemo
[WARNING] Don't override file D:\IDEA WorkSpace\JenkinsPluginsDemo\JenkinsPluginDemo
[WARNING] Don't override file D:\IDEA WorkSpace\JenkinsPluginsDemo\JenkinsPluginDemo\src\main\resources\io\jenkins\plugins\sample
[WARNING] Don't override file D:\IDEA WorkSpace\JenkinsPluginsDemo\JenkinsPluginDemo\src\main\java\io\jenkins\plugins\sample
[WARNING] Don't override file D:\IDEA WorkSpace\JenkinsPluginsDemo\JenkinsPluginDemo\src\test\java\io\jenkins\plugins\sample
[INFO] Executing META-INF/archetype-post-generate.groovy post-generation script
[version:1.0, package:io.jenkins.plugins.sample, groupId:unused, hostOnJenkinsGitHub:true, artifactId:JenkinsPluginDemo]
[INFO] Project created from Archetype in dir: D:\IDEA WorkSpace\JenkinsPluginsDemo\JenkinsPluginDemo
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 16:53 min
[INFO] Finished at: 2021-10-14T15:23:42+08:00
[INFO] ------------------------------------------------------------------------
正常执行后,当前目录下方会生成名称为刚才输入的
artifactId
的名称相同的目录,其中添加了工作插件的基本结构。进入该目录,执行如下
Maven
的命令
mvn verify
这一操作会校验是否构建成功,如果需要的一些依赖包本地仓库没有,这一步还会下载很多的依赖,然后遍历配置的构建周期,包括静态分析(
FindBugs
)和测试,直到日志内容出现下面的信息
[INFO] BugInstance size is 0
[INFO] Error size is 0
[INFO] No errors/warnings found
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:05 min
[INFO] Finished at: 2021-10-15T09:54:04+08:00
[INFO] ------------------------------------------------------------------------
使用
IDEA
打开这个原型工程,等待工程初始化,最后项目结构如下
2、可能出现的问题
Ⅰ、提示远程目录没有这个原型
[INFO]
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:3.2.0:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:3.2.0:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO]
[INFO]
[INFO] --- maven-archetype-plugin:3.2.0:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Interactive mode
[WARNING] No archetype found in remote catalog. Defaulting to internal catalog
[INFO] Your filter doesn't match any archetype, so try again with another value.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.091 s
[INFO] Finished at: 2021-10-14T14:25:17+08:00
[INFO] ------------------------------------------------------------------------
笔者的解决办法为下载
archetype-catalog.xml
文件放到Maven
本地仓库的根目录下方(非安装目录),点击链接下载
Ⅱ、依赖下载失败
日志如下
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:24 h
[INFO] Finished at: 2021-10-14T16:59:06+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project JenkinsPluginDemo: Could not resolve dependencies for project io.jenkins.plugins:JenkinsPluginDemo:hpi:1.0-SNAPSHOT: Could not transfer artifact org.jenkins-ci.main:jenkins-war:war:2.277.1 from/to repo.jenkins-ci.org (https://repo.jenkins-ci.org/public/): GET request of: org/jenkins-ci/main/jenkins-war/2.277.1/jenkins-war-2.277.1.war from repo.jenkins-ci.org failed: Connection reset -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException
部分依赖下载可能特别缓慢,甚至如日志那样超时失败,笔者没有找到比较好的解决办法,我直接将下载不了的依赖链接复制下来,然后手动用下载工具(如
IDM
)下载下来后放在本地仓库,然后重新构建。(整个过程持续了一个下午加一个晚上,早上到公司发现下载又卡住了,可能一晚上都没下吧,七七八八又花了一个多小时才下完)
构建并运行插件
Maven HPI Plugin
用于构建和打包Jenkins
插件。它提供了一种便利的方式来运行一个已经包含了当前插件的Jenkins
实例,在插件工程根目录下输入如下命令
mvn hpi:run
这将在
http://localhost:8080/jenkins/
建立一个Jenkins
实例。等待以下控制台输出如下内容
hudson.util.Retrier#start: Performed the action check updates server successfully at the attempt #1
这一步也可以通过
IDEA
可视化窗口来做
这里创建的
Jenkins
实例主目录为当前插件工程根目录下的work/
目录,也就是说后续运行产生的数据都会存放在这个地方
这里我们创建一个自由风格的插件,命名为
HelloWorld
找到
Build
配置选项,输入你想输入的任何内容
然后点击构建可以看到输出日志如下
Started by user unknown or anonymous
Running as SYSTEM
Building in workspace D:\IDEA WorkSpace\JenkinsPluginsDemo\JenkinsPluginDemo\work\workspace\HelloWorld
Hello, 哈喽世界!
Finished: SUCCESS
尝试扩展插件
接下来我们做两个扩展点:
- 不单单是在构建日志中记录问候语中使用的名称,还要有适当的数据结构。
- 在构建中添加一个新页面,用来显示问候语中使用的名称。
1、记录名称
我们必须存储运行构建时使用的名称,而不是使用配置中的名称,因为配置随时可能会被更改。和
HelloWorldBuilder
同级目录新建一个HelloWorldAction
类,该类需要实现Action
接口。Action
下的实现类是Jenkins
中可扩展性的基本构建块,他们可以附加到许多模型对象上并被储存起来,并可以添加到他们的UI
中。
/**
* @author PengHuAnZhi
* @ProjectName JenkinsPluginDemo
* @Description TODO
* @time 2021/10/20 10:13
*/
public class HelloWorldAction implements Action {
@Override
public String getIconFileName() {
return null;
}
@Override
public String getDisplayName() {
return null;
}
@Override
public String getUrlName() {
return null;
}
}
因为我们需要存储问候中使用的名称,所以我们需要在这个
Action
中定义这个变量,并设置它的Getter
和以它为参数的构造方法
/**
* @author PengHuAnZhi
* @ProjectName JenkinsPluginDemo
* @Description TODO
* @time 2021/10/20 10:13
*/
public class HelloWorldAction implements Action {
private final String name;
public HelloWorldAction(String name) {
this.name = name;
}
public String getName() {
return name;
}
(...)
}
现在我们需要在构建步骤执行的时候创建这个
Action
的一个实例。这就需要扩展HelloWorldBuilder
类中的perform
方法来吧我们创建的动作实例添加到正在运行的构建中
(...)
@Override
public void perform(Run<?, ?> run, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException {
run.addAction(new HelloWorldAction(name));
if (useFrench) {
listener.getLogger().println("Bonjour, " + name + "!");
} else {
listener.getLogger().println("Hello, " + name + "!");
}
}
(...)
现在重启
Jenkins
(除了静态资源的修改不需要重启,其他都需要重启才能生效),我们也可以通过查看work/jobs/JOBNAME/builds/BUILDNUMBER/
下方的build.xml
文件来确认:
(...)
<actions>
<hudson.model.CauseAction>
<causeBag class="linked-hash-map">
<entry>
<hudson.model.Cause_-UserIdCause/>
<int>1</int>
</entry>
</causeBag>
</hudson.model.CauseAction>
<io.jenkins.plugins.sample.HelloWorldAction plugin="JenkinsPluginDemo@1.0-SNAPSHOT">
<name>哈喽世界</name>
</io.jenkins.plugins.sample.HelloWorldAction>
</actions>
(...)
第一个
CauseAction
表示的是构建原因(构建如何触发),这种情况下,是匿名用户开始的这次构建第二个
HelloWorldAction
便是我们创建的Action
,其中的name
是我们创建构建的时候使用的名称
2、添加一个显示名称的视图
第一步我们将名称存储下来了,现在我们让他在页面上面显示出来,也就是将我们正在存储的构建可视化
首先,我们回到
HelloWorldAction
类,并定义图标,标题和URL
名称,如下:
@Override
public String getIconFileName() {
return "document.png";
}
@Override
public String getDisplayName() {
return "Greeting";
}
@Override
public String getUrlName() {
return "greeting";
}
其中:
document.png
用于侧面板项目的图标,这是一个Jenkins
绑定的预定义图标之一Greeting
用于侧面板项目的标签greeting
用于此操作的URL
片段操作完毕后,重启
Jenkins
,就会发现在http://JENKINS/job/JOBNAME/BUILDNUMBER/
页面中出现一个新的Greeting
小图标
点开这个页面会链接到
URL
http://JENKINS/job/JOBNAME/BUILDNUMBER/greeting/
,不过现在还没有定义这个页面,所以是一个404
的页面
所以接下来需要定义出现在这个
URL
上的页面。为了在Jenkins
创建这样的视图,通常使用Apache Commons Jelly,Jelly
允许用XML
定义XML
和XHTML
输出,它支持如下功能
- 支持条件和循环
- 允许包含在其他地方定义的
view fragments
- 可用于定义可重用的
UI
组件现在我们在
src/main/resources/io/jenkins/plugins/sample/
目录下创建一个新的名为HelloWorldAction
的目录,如你所见,这个目录是和HelloWorldAction
对应的,它可以放置与其相关的资源。我们可以观察到
src/main/resources/io/jenkins/plugins/sample/HelloWorldAction/
目录便是HelloWorldBuilder
相关的资源目录,其中config.jelly
便是构建步骤配置表单,包含构建步骤配置的本地化的各种config*.properties
文件和为配置提供了 本地化的内联帮助help*.html
文件
在刚在创建的目录
HelloWorldAction
下创建名为index.jelly
的文件,这将会显示在http://JENKINS/job/JOBNAME/BUILDNUMBER/greeting/
链接下,添加如下内容
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:st="jelly:stapler">
<l:layout title="Greeting">
<l:main-panel>
<h1>
Name: ${it.name}
</h1>
</l:main-panel>
</l:layout>
</j:jelly>
其中:
layout
是Jenkins
核心中定义的可重用tag
,它提供了页眉,侧面板,主要内容区域和页脚的基本页面布局。- 为了使名称显示在内容区域(非侧面板),我们需要将输出包裹在
main-panel
标签中。- 在
main-panel
标签内部我们可以使用任何HTML
标签,并将它们用于输出。Name:${it.name}
是一个JEXL
表达式,引用视图所属的Java
对象(类似Java
中的this
),在本例中为HelloWorldAction
实例。it.name
等同于调用getName()
最后 重启
Jenkins
观察
由于这个界面和对应构建的版本相关,如图
所以我们需要显示对应版本的侧面板,为此我们首先需要获取的就是对我们动作中响应构建的引用,然后在动作视图中包含构建构建的侧面视图
fragment
,为了获取HelloWorldAction
所属的构建(Run
)引用,我们需要改变现有的类,Jenkins
为我们提供了新的接口RunAction2
,这个接口相较于Action
接口新增了两个方法:
- onAttached(Run):首次连接到构建回调方法
- onLoad(Run):从磁盘加载操作和运行分别都会调用的方法
/**
* @author PengHuAnZhi
* @ProjectName JenkinsPluginDemo
* @Description TODO
* @time 2021/10/20 10:13
*/
public class HelloWorldAction implements RunAction2 {
private final String name;
private transient Run<?, ?> run;
public HelloWorldAction(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String getIconFileName() {
return "document.png";
}
@Override
public String getDisplayName() {
return "Greeting";
}
@Override
public String getUrlName() {
return "greeting";
}
@Override
public void onAttached(Run<?, ?> run) {
this.run = run;
}
@Override
public void onLoad(Run<?, ?> run) {
this.run = run;
}
public Run<?, ?> getRun() {
return run;
}
}
这些一旦完成之后,我们需要将扩展这个视图来将
Run
的侧面板视图片段包含进来:
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:st="jelly:stapler">
<l:layout title="Greeting">
<l:side-panel>
<st:include page="sidepanel.jelly" it="${it.run}" optional="true" />
</l:side-panel>
<l:main-panel>
<h1>
Name: ${it.name}
</h1>
</l:main-panel>
</l:layout>
</j:jelly>
与
main-panel
类似,我们希望内容仅在侧面板中显示,因此我们需要将它们包裹在元素side-panel
中。
includes
另一个对象Run
的视图片段sidepanel.jelly
。 我们把它标记为可选的,所以如果这个视图片断不存在,就不会显示错误,因为抽象类Run
没有定义这样的视图,只有它的子类AbstractBuild
。至此,官方
Demo
扩展已经完成,重启检查一下