通常我们基于Maven来开发Jenkins插件。
修改Maven配置文件,添加:
<profile>
<id>jenkins</id>
<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
</pluginRepository>
</pluginRepositories>
</profile>
插件项目的父POM通常设置为:
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>3.43</version>
<relativePath />
</parent>
此POM为构件插件提供了合理的缺省配置, 修改属性以定制配置项:
<properties>
<jenkins.version>2.60.1</jenkins.version>
<java.level>8</java.level>
</properties>
这是一个Maven插件,用于构建Jenkins插件。
此目标用于创建一个新的插件项目骨架。目前已经废弃,应当考虑基于原型生成插件项目骨架
目标完整名称:org.jenkins-ci.tools:maven-hpi-plugin:3.7:hpi。构建Jenkins插件的WAR包。
此目标的必需参数:
| 参数 | 说明 |
|
minimumJavaVersion |
运行此插件需要的最低Java版本 |
此目标主要的可选参数:
| 参数 | 说明 |
| compatibleSinceVersion | 当前插件版本,在配置上兼容的最低插件版本号 |
| maskClasses |
空白符分隔的Java包前缀,插件不希望使用Jenkins Core提供的这些类 每个包前缀应当以 . 结尾 |
| globalMaskClasses |
类似于maskClasses,但是作用于Jenkins Core和所有插件的边界 主要供哪些提供JavaEE API的插件使用,例如提供JPA API的database插件。其它依赖于database插件的插件,亦可通过container类加载器看到JPA API |
| hpiName | 生成的HPI的名称 |
| pluginFirstClassLoader | 设置为true,改变类加载器优先级,让插件自带的类优先于插件依赖的其它插件的类加载 |
类似hpi:hpi,但是打包为Debug布局。
目标完整名称:org.jenkins-ci.tools:maven-hpi-plugin:3.7:run。使用当前插件来运行Jenkins。
此目标的必需参数:
| 参数 | 说明 |
|
minimumJavaVersion |
运行此插件需要的最低Java版本 |
此目标主要的可选参数:
| 参数 | 说明 |
| consoleForceReload | 如果设置为true,在输入控制台上输入新行,则Web上下文自动重启 |
| defaultPort | 默认HTTP端口 |
| hudsonHome jenkinsHome |
$JENKINS_HOME的位置,启动的Jenkins服务器以此目录为工作空间 |
| jenkinsCoreId | Jenkins Core 包的groupId:artifactId |
| reload | 可选值automatic | manual,设置为automatic则文件改变后自动reload,否则输入控制台上输入新行后reload |
| maskClasses | 参考HPI |
| pluginFirstClassLoader | 参考HPI |
可以通过mvnDebug进行Jenkins插件的单步跟踪。
mvnDebug hpi:run # 或者 export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n" mvn hpi:run
使用系统属性来调整Jenkins的HTTP端口,以及上下文路径:
mvn hpi:run -Djetty.port=8090 -Dhpi.prefix=/jenkins
现在可以通过Gradle JPI plugin,基于Gradle开发Jenkins插件。
plugins {
id 'org.jenkins-ci.jpi' version '0.33.0'
}
group = 'org.jenkins-ci.plugins'
version = '1.2.0-SNAPSHOT'
description = 'A description of your plugin'
jenkinsPlugin {
// 依赖的Jenkins核心版本
coreVersion = '1.420'
// 插件的标识符,默认为项目名去掉-plugin后缀
shortName = 'hello-world'
// 人类可读的名称
displayName = 'Hello World plugin built with Gradle'
// 在Jenkins Wiki上的插件页面
url = 'http://wiki.jenkins-ci.org/display/JENKINS/SomePluginPage'
// 可选的插件源码库地址
gitHubUrl = 'https://github.com/jenkinsci/some-plugin'
// 是否让插件Classloader优先于核心Classloader加载类
pluginFirstClassLoader = true
// 不希望看到的核心的类前缀
maskClasses = 'groovy.grape org.apache.commons.codec'
// 当前版本的配置和什么版本之后的兼容
compatibleSinceVersion = '1.1.0'
// 开发服务器的工作目录
workDir = file('/tmp/jenkins')
// 部署此插件到什么地方
repoUrl = 'https://repo.jenkins-ci.org/releases'
// 部署此插件的快照版本到什么地方调试插件
snapshotRepoUrl = 'https://repo.jenkins-ci.org/snapshots'
// 是否禁用测试的依赖注入,用于检查Jelly语法等
disabledTestInjection = false
// 本地化任务输出目录
localizerOutputDir = "${project.buildDir}/generated-src/localizer"
// 是否配置中心仓库、本地仓库、Jenkins中心仓库
configureRepositories = false
// 是否配置部署仓库
configurePublishing = false
// 插件扩展名,hpi或者jpi
fileExtension = 'hpi'
// 开发者列表
developers {
developer {
id 'abayer'
name 'Andrew Bayer'
email 'andrew.bayer@gmail.com'
}
}
// License信息
licenses {
license {
name 'Apache License, Version 2.0'
url 'https://www.apache.org/licenses/LICENSE-2.0.txt'
distribution 'repo'
comments 'A business-friendly OSS license'
}
}
}
dependencies {
// 依赖其它插件,目标插件的类在编译期可见
jenkinsPlugins 'org.jenkinsci.plugins:git:1.1.15'
// 可选依赖
optionalJenkinsPlugins 'org.jenkins-ci.plugins:ant:1.2'
// 测试依赖
jenkinsTest 'org.jenkins-ci.main:maven-plugin:1.480'
// 为server任务安装额外的插件
jenkinsServer 'org.jenkins-ci.plugins:ant:1.2'
}
plugins {
id 'groovy'
id "org.jenkins-ci.jpi" version "0.33.0"
}
sourceCompatibility = 1.8
group 'io.jenkins.plugins'
version '1.0-SNAPSHOT'
description = "This plugin provides pipeline steps for ALCM."
repositories {
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
maven { url 'https://repo.jenkins-ci.org/public/' }
mavenLocal()
}
jenkinsPlugin {
coreVersion = "2.164.1"
displayName = "PA ALCM Steps"
url = "http://wiki.jenkins-ci.org/display/JENKINS/ALCMSteps"
shortName = "alcm-steps"
workDir = file('/tmp/jenkins')
fileExtension = 'jpi'
pluginFirstClassLoader = true
}
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.3.11'
compile 'cc.gmem.yun.alcm:client:1.0-SNAPSHOT'
compile 'org.jenkins-ci.plugins:structs:1.19'
testCompile group: 'junit', name: 'junit', version: '4.12'
jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-step-api:2.19'
jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-cps:2.39'
jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-job:2.11.2'
jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-basic-steps:2.6'
jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-durable-task-step:2.13'
jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-api:2.30'
jenkinsTest 'org.jenkins-ci.plugins.workflow:workflow-support:3.3'
jenkinsTest 'org.jenkins-ci.plugins:script-security:1.39'
jenkinsTest 'org.jenkins-ci.plugins:scm-api:2.2.6'
}
| 任务 | 说明 |
| gradle jpi | 构建出插件 |
| gradle publishToMavenLocal | 构建并安装到本地Maven仓库 |
| gradle publish | 部署到Jenkins的Maven仓库,以便在Update Center可见 |
| gradle server | 启动本地Jenkins实例便于调试:-Djenkins.httpPort=8082 |
# 调试 ./gradlew server -Dorg.gradle.debug=true # 定制端口 /gradlew server -Dorg.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 # 禁用Jelly缓存 ./gradlew -Dstapler.jelly.noCache=false server
Jenkins提供了一系列插件的原型,我们可以根据基于这些原型生成自己的插件的骨架代码:
mvn -U -P jenkins archetype:generate -Dfilter="io.jenkins.archetypes:"
# 非交互式
mvn archetype:generate -P jenkins -B -DarchetypeGroupId=io.jenkins.archetypes \
-DarchetypeArtifactId=hello-world-plugin \
-DarchetypeVersion=1.5 \
-DgroupId=cc.gmem.yun.alcm -DartifactId=alcm-steps -Dversion=1.0-SNAPSHOT \
-Dpackage=cc.gmem.yun.alcm.pipeline.jenkins.steps
从列表中选择一个合适的原型,然后根据提示填写各项参数即可。
| 原型 | 说明 |
| io.jenkins.archetypes:empty-plugin | 仅仅包含POM和空白的源码树 |
| io.jenkins.archetypes:global-configuration-plugin | 全局配置插件的示例 |
| io.jenkins.archetypes:hello-world-plugin | 包含POM和一个示例的Build Step |
本章后续内容均针对基于hello-world-plugin生成的代码骨架。
从原型生成的插件目录结构如下:
├── alcm-steps.iml ├── pom.xml ├── src │ ├── main │ │ ├── java # 插件源代码 │ │ │ └── cc │ │ │ └── gmem │ │ │ └── yun │ │ │ └── alcm │ │ │ └── pipeline │ │ │ └── jenkins │ │ │ └── steps │ │ │ └── HelloWorldBuilder.java # 插件类 │ │ ├── resources # Jelly/Groovy视图文件 │ │ │ ├── index.jelly # 插件说明信息 │ │ │ ├── cc │ │ │ │ └── gmem │ │ │ │ └── yun │ │ │ │ └── alcm │ │ │ │ └── pipeline │ │ │ │ └── jenkins │ │ │ │ └── steps │ │ │ │ ├── HelloWorldBuilder │ │ │ │ │ ├── config.jelly # 插件配置页面模板 │ │ │ │ │ ├── config.properties # 模板变量值 │ │ │ │ │ ├── config_zh_CN.properties # 模板变量值(国际化) │ │ │ │ │ ├── help-name.html # 字段name的帮助信息 │ │ │ │ │ ├── help-name_zh_CN.html │ │ │ │ │ ├── help-useFrench.html │ │ │ │ │ └── help-useFrench_zh_CN.html │ │ └── webapp # 插件的静态资源
该插件会为Jenkins Job提供一个Step,此Step的配置界面如下:
Name字段的名称为name,点击后面的问号,显示的内容是help-name.html中的。
此类是插件的实现。它继承Builder,实现了SimpleBuildStep。
SimpleBuildStep是诸如Builder、Publisher之类的Step,可以在构建过程的任何时机调用多次。这些Step应当遵循:
从抽象类Builder继承的方法如下:
public abstract class Builder extends BuildStepCompatibilityLayer implements Describable<Builder>, ExtensionPoint {
public boolean prebuild(Build build, BuildListener listener) {
return true;
}
// 由于Builder通常不依赖于它前面的步骤的结果,因此默认返回NONE
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
public Descriptor<Builder> getDescriptor() {
return Jenkins.getInstance().getDescriptorOrDie(getClass());
}
public static DescriptorExtensionList<Builder,Descriptor<Builder>> all() {
return Jenkins.getInstance().<Builder,Descriptor<Builder>>getDescriptorList(Builder.class);
}
}
HelloWorldBuilder是插件的实现,实现插件不需要继承plugin类(这种方式已经废弃),推荐的方式是实现扩展点,并通过@hudson.Extension注解来注册(以便Jenkins自动发现)。
关于这种扩展的方式,需要注意:
package cc.gmem.yun.alcm.pipeline.jenkins.steps;
import hudson.Launcher;
import hudson.Extension;
import hudson.FilePath;
import hudson.util.FormValidation;
import hudson.model.AbstractProject;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.Builder;
import hudson.tasks.BuildStepDescriptor;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import javax.servlet.ServletException;
import java.io.IOException;
import jenkins.tasks.SimpleBuildStep;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundSetter;
public class HelloWorldBuilder extends Builder implements SimpleBuildStep {
private final String name;
private boolean useFrench;
// 绑定对象时使用此构造器
@DataBoundConstructor
public HelloWorldBuilder(String name) {
this.name = name;
}
public String getName() {
return name;
}
public boolean isUseFrench() {
return useFrench;
}
@DataBoundSetter
public void setUseFrench(boolean useFrench) {
this.useFrench = useFrench;
}
/**
* SimpleBuildStep的核心方法,执行此Step
* @param run 此Step所属的Build
* @param workspace 此Build的工作区,用于文件系统操作
* @param launcher 用于启动进程
* @param listener 用于发送输出
* @throws InterruptedException 如果此Step被中断
* @throws IOException 出现其它错误,更“礼貌”的错误是AbortException
*/
public void perform(Run<?, ?> run, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException {
if (useFrench) {
listener.getLogger().println("Bonjour, " + name + "!");
} else {
listener.getLogger().println("Hello, " + name + "!");
}
}
// 此注解为Jenkins扩展定义唯一的标识符,驼峰式大小写,尽量简短。在流水线脚本中引用此Step时即使用此标识符
// 标识符不需要全局唯一,只需要在一个扩展点内是唯一的即可
@Symbol("greet")
// 标记字段、方法或类,以便Huson能自动发现定位到ExtensionPoint的实现
@Extension
// Build / Publisher的描述符
// Descriptor是可配置实例(Describable)的元数据,也作为Describable的工厂
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
// 校验Project配置表单中的字段
// 下面的方法检查name字段,参数value注入name的值,可以用@QueryParameter注入其它参数
// 如果要校验useFrench字段,则需要实现doCheckUsdeFrench方法
public FormValidation doCheckName(@QueryParameter String value, @QueryParameter boolean useFrench)
throws IOException, ServletException {
if (value.length() == 0)
// 引用资源束
return FormValidation.error(Messages.HelloWorldBuilder_DescriptorImpl_errors_missingName());
if (value.length() < 4)
return FormValidation.warning(Messages.HelloWorldBuilder_DescriptorImpl_warnings_tooShort());
if (!useFrench && value.matches(".*[éáàç].*")) {
return FormValidation.warning(Messages.HelloWorldBuilder_DescriptorImpl_warnings_reallyFrench());
}
return FormValidation.ok();
}
// 判断当前Step是否能用于目标类型的项目
@Override
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
// 此Step的显示名称
@Override
public String getDisplayName() {
return Messages.HelloWorldBuilder_DescriptorImpl_DisplayName();
}
}
}
此文件是Project配置页面的模板:
<?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 />
</f:entry>
<f:advanced>
<f:entry title="${%French}" field="useFrench"
description="${%FrenchDescr}">
<f:checkbox />
</f:entry>
</f:advanced>
</j:jelly>
模板使用的资源束为config.properties。
这个类包含了单元测试:
package cc.gmem.yun.alcm.pipeline.jenkins.steps;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Label;
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;
public class HelloWorldBuilderTest {
// 标记此字段为TestRule
// JUnit的TestRule决定一个或一组测试方法如何运行、如何报告
@Rule
public JenkinsRule jenkins = new JenkinsRule();
final String name = "Bobby";
@Test
public void testConfigRoundtrip() throws Exception {
// 启动一个临时Jenkins服务器,并创建自由项目
FreeStyleProject project = jenkins.createFreeStyleProject();
// 添加一个构建步骤
project.getBuildersList().add(new HelloWorldBuilder(name));
// 加载配置页面,不经修改的提交
project = jenkins.configRoundtrip(project);
// 判断相等
jenkins.assertEqualDataBoundBeans(new HelloWorldBuilder(name), project.getBuildersList().get(0));
}
@Test
public void testConfigRoundtripFrench() throws Exception {
FreeStyleProject project = jenkins.createFreeStyleProject();
HelloWorldBuilder builder = new HelloWorldBuilder(name);
builder.setUseFrench(true);
project.getBuildersList().add(builder);
project = jenkins.configRoundtrip(project);
HelloWorldBuilder lhs = new HelloWorldBuilder(name);
lhs.setUseFrench(true);
jenkins.assertEqualDataBoundBeans(lhs, project.getBuildersList().get(0));
}
@Test
public void testBuild() throws Exception {
FreeStyleProject project = jenkins.createFreeStyleProject();
HelloWorldBuilder builder = new HelloWorldBuilder(name);
project.getBuildersList().add(builder);
// 执行构建,并断言构建成功
FreeStyleBuild build = jenkins.buildAndAssertSuccess(project);
// 断言本次构建的控制台输出包括指定字样
jenkins.assertLogContains("Hello, " + name, build);
}
@Test
public void testBuildFrench() throws Exception {
FreeStyleProject project = jenkins.createFreeStyleProject();
HelloWorldBuilder builder = new HelloWorldBuilder(name);
builder.setUseFrench(true);
project.getBuildersList().add(builder);
FreeStyleBuild build = jenkins.buildAndAssertSuccess(project);
jenkins.assertLogContains("Bonjour, " + name, build);
}
// 在流水线中使用此Step
@Test
public void testScriptedPipeline() throws Exception {
String agentLabel = "my-agent";
// 在本机创建一个新的Slave(Agent),等待其上线
jenkins.createOnlineSlave(Label.get(agentLabel));
// 定义一个流水线任务
WorkflowJob job = jenkins.createProject(WorkflowJob.class, "test-scripted-pipeline");
// 流水线定义,注意通过@Symbol指定的标识符来引用本插件定义的Step
String pipelineScript
= "node {\n"
+ " greet '" + name + "'\n"
+ "}";
// CPS,即continuation passing style,是运行Groovy脚本的一种方式
// 允许应用程序被随时暂停、重启继续
job.setDefinition(new CpsFlowDefinition(pipelineScript, true));
WorkflowRun completedBuild = jenkins.assertBuildStatusSuccess(job.scheduleBuild2(0));
String expectedString = "Hello, " + name + "!";
jenkins.assertLogContains(expectedString, completedBuild);
}
}
Jenkins插件的依赖注入,可以基于Google Guice(一个轻量级的IoC框架)实现。下面是一个例子:
// 需要被注入的接口及其实现
public interace MySvc {
public void myServiceMethod()
}
public class MySvcImpl implements MySvc {
public void myServiceMethod() {
System.out.println("It works!");
}
}
// IoC配置类
public class MyGuiceModule extends com.google.inject.AbstractModule {
@Override
public void configure() {
bind(MySvc.class).to(MySvcImpl.class).in(com.google.inject.Singleton.class);
}
}
下面的插件类会被注入MySvc:
import com.google.inject.Guice;
import com.google.inject.Inject;
public class MyPublisher extends Publisher {
private transient MySvc mySvc;
private String someJobConfig
@DataBoundConstructor
public MyPublisher(String someJobConfig) {
this.someJobConfig = someJobConfig;
}
@Inject
public void setMySvc(MySvc mySvc) {
this.mySvc = mySvc;
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) {
if (mySvc == null) {
// 进行注入
Guice.createInjector(new MyModule()).injectMembers(this);
}
mySvc.myServiceMethod();
}
}
每当构建时, perform(Build, Launcher, BuildListener)方法会被调用,你可以利用Launcher来执行任意命令:
launcher.launch(cmd, env, out, workDir)
// 启动进程
launcher.launch("dir", new String[0], listener.getLogger(), build.getProject().getWorkspace());
// 获取返回值
Proc proc = launcher.launch("dir", build.getEnvVars(), listener.getLogger(), build.getProject().getWorkspace());
int exitCode = proc.join();
Jenkins使用一种分布式机制来运行任务 —— 运行在Master节点上的线程,能够发送闭包到远程机器上,并且在闭包执行完毕后,取回结果。
支持分布式执行的闭包示例如下:
private static class GetSystemProperties extends MasterToSlaveCallable<Properties,RuntimeException> {
public Properties call() {
return System.getProperties();
}
private static final long serialVersionUID = 1L;
}
关键之处在于需要实现hudson.remoting.Callable,此接口声明了返回值、异常类型。
下面的调用将闭包发送给Slave执行:
Properties systemProperties = channel.call(new GetSystemProperties());
Jenkins提供了一些关键的抽象,将其分布式执行的特性隐藏起来。
| 抽象 | 说明 |
| hudson.FilePath | 用于代替java.io.File,可以指向位于Master或任何Slave上的目录或文件 |
| hudson.Launcher | 类似于 java.lang.ProcessBuilder,但是可以在远程JVM上启动进程 |
你需要把图片放在src/main/webapp下。在jelly或者Java页面上获取图片的方法是:
public String getIconPath() {
PluginWrapper wrapper = Hudson.getInstance().getPluginManager().getPlugin([YOUR-PLUGIN-MAIN-CLASS].class);
return Hudson.getInstance().getRootUrl() + "plugin/"+ wrapper.getShortName()+"/";
}
要在jelly页面添加空格,只需要: <st:nbsp/>
本章分析一些真实的Jenkins插件的代码。
Publisher可以在构建完成后,触发任意的动作。Recorder可以为每次构建记录一些统计信息。
Discard Old Build plugin是这种扩展的例子,用于在构建完毕后,检查并删除过期的构建历史。本节分析其代码:
package org.jenkinsci.plugins.discardbuild;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.*;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.util.RunList;
import org.kohsuke.stapler.DataBoundConstructor;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DiscardBuildPublisher extends Recorder {
// 插件参数
/**
* If not -1, history is only kept up to this days.
*/
private final int daysToKeep;
// 从表单绑定插件参数
@DataBoundConstructor
public DiscardBuildPublisher( String daysToKeep) {
this.daysToKeep = parse(daysToKeep);
}
private void deleteOldBuildsByDays(AbstractBuild<?, ?> build, BuildListener listener, int daysToKeep) {
ArrayList<Run<?, ?>> list = updateBuildsList(build, listener);
try {
Calendar cal = getCurrentCalendar();
cal.add(Calendar.DAY_OF_YEAR, -daysToKeep);
for (Run<?, ?> r : list) {
if (r.getTimestamp().before(cal)) {
discardBuild(r, "it is older than daysToKeep", listener); //$NON-NLS-1$
}
}
} catch (IOException e) {
e.printStackTrace(listener.error("")); //$NON-NLS-1$
}
}
private ArrayList<Run<?, ?>> updateBuildsList(AbstractBuild<?, ?> build, BuildListener listener) {
RunList<Run<?, ?>> builds = new RunList<Run<?, ?>>();
ArrayList<Run<?, ?>> list = new ArrayList<Run<?, ?>>();
// 获取本次Build对应的Job
Job<?, ?> job = (Job<?, ?>) build.getParent();
// 获取Job的所有构建
builds = (RunList<Run<?, ?>>) job.getBuilds();
list = discardLastBuilds(build, listener, builds);
return list;
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) {
listener.getLogger().println("Discard old builds..."); //$NON-NLS-1$
deleteOldBuildsByDays(build, listener, daysToKeep);
return true;
}
private void discardBuild(Run<?, ?> history, String reason, BuildListener listener) throws IOException {
listener.getLogger().printf("#%d is removed because %s\n", history.getNumber(), reason); //$NON-NLS-1$
history.delete();
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
// 描述符
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
@SuppressWarnings("rawtypes")
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
/**
* This human readable name is used in the configuration screen.
*/
public String getDisplayName() {
return Messages.DiscardHistoryBuilder_description();
}
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
}
这个扩展点用于触发某个Job的构建。
Files Found Trigger是这种扩展的例子,能够轮询一个或多个目录,当发现特定的文件后,自动触发构建。本节分析其代码。
package hudson.plugins.filesfoundtrigger;
import static hudson.Util.fixNull;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.kohsuke.stapler.DataBoundConstructor;
import com.google.common.collect.ImmutableList;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
import com.thoughtworks.xstream.mapper.Mapper;
import antlr.ANTLRException;
import hudson.Extension;
import hudson.model.BuildableItem;
import hudson.model.Item;
import hudson.triggers.Trigger;
import hudson.triggers.TriggerDescriptor;
import hudson.util.RobustReflectionConverter;
public final class FilesFoundTrigger extends Trigger<BuildableItem> {
private static final Logger LOGGER = Logger.getLogger(FilesFoundTrigger.class.getName());
private static final AtomicLong logCounter = new AtomicLong();
// 从什么Slave节点上检查文件是否存在,如果为空则检查master节点
private final String node;
// 检查的目录
private final String directory;
// 期望存在的文件的pattern
private final String files;
// 忽略的文件的pattern
private final String ignoredFiles;
// 仅仅当发现文件数量大于此数字时才触发
private final String triggerNumber;
// 额外的文件pattern
private final ArrayList<FilesFoundTriggerConfig> additionalConfigs;
/**
* 构造函数
*
* @param spec
* 轮询文件系统的Cron表达式
* @param configs
* 文件模式列表
* @throws ANTLRException
* 无法解析Cron表达式抛出此异常
*/
@DataBoundConstructor
public FilesFoundTrigger(String spec, List<FilesFoundTriggerConfig> configs) throws ANTLRException {
// Trigger天生基于Cron进行检查
super(spec);
ArrayList<FilesFoundTriggerConfig> configsCopy = new ArrayList<FilesFoundTriggerConfig>(fixNull(configs));
FilesFoundTriggerConfig firstConfig;
if (configsCopy.isEmpty()) {
firstConfig = new FilesFoundTriggerConfig(null, "", "", "", "1");
} else {
firstConfig = configsCopy.remove(0);
}
this.node = firstConfig.getNode();
this.directory = firstConfig.getDirectory();
this.files = firstConfig.getFiles();
this.ignoredFiles = firstConfig.getIgnoredFiles();
this.triggerNumber = firstConfig.getTriggerNumber();
if (configsCopy.isEmpty()) {
configsCopy = null;
}
this.additionalConfigs = configsCopy;
}
public List<FilesFoundTriggerConfig> getConfigs() {
ImmutableList.Builder<FilesFoundTriggerConfig> builder = ImmutableList.builder();
builder.add(new FilesFoundTriggerConfig(node, directory, files, ignoredFiles, triggerNumber));
if (additionalConfigs != null) {
builder.addAll(additionalConfigs);
}
return builder.build();
}
// Cron每次触发的逻辑
@Override
public void run() {
long counter = logCounter.incrementAndGet();
for (FilesFoundTriggerConfig config : getConfigs()) {
FilesFoundTriggerConfig expandedConfig = config.expand();
try {
FileSearch.Result result = FileSearch.perform(expandedConfig);
int triggerNumber = Integer.parseInt(expandedConfig.getTriggerNumber());
boolean triggerBuild = result.files.size() >= triggerNumber;
if (triggerBuild) {
// 触发构建
job.scheduleBuild(0, new FilesFoundTriggerCause(expandedConfig));
return;
}
} catch (NumberFormatException e) {
LOGGER.log(Level.FINE, "{0} - Result: Invalid trigger number (build not triggered)", counter);
} catch (InterruptedException e) {
LOGGER.log(Level.FINE, "{0} - Result: Thread interrupted (build not triggered)", counter);
Thread.currentThread().interrupt();
}
}
}
// 将当前插件注册为Trigger扩展
@Extension
public static final class DescriptorImpl extends TriggerDescriptor {
@Override
public boolean isApplicable(Item item) {
return item instanceof BuildableItem;
}
@Override
public String getDisplayName() {
return Messages.DisplayName();
}
}
}
Jenkins提供一系列JUnit周边工具来简化单元测试。支持的特性包括:
下面的例子示例了如何基于JenkinsRule进行单元测试:
import org.jvnet.hudson.test.JenkinsRule;
import org.apache.commons.io.FileUtils;
import hudson.model.*;
import hudson.tasks.Shell;
import org.junit.Test;
import org.junit.Rule;
public class AppTest {
@Rule public JenkinsRule j = new JenkinsRule();
@Test public void first() throws Exception {
// 自由风格项目
FreeStyleProject project = j.createFreeStyleProject();
// 添加一个构建步骤
project.getBuildersList().add(new Shell("echo hello"));
// 得到某个构建步骤
Shell shell = project.getBuildersList().get(Shell.class);
// 调度构建并获得结果
FreeStyleBuild build = project.scheduleBuild2(0).get();
System.out.println(build.getDisplayName() + " completed");
// 读取日志文件内容
String s = FileUtils.readFileToString(build.getLogFile());
assertThat(s, contains("+ echo hello"));
}
}
某些情况下,你希望在不依赖于完整Jenkins实例的前提下进行快速测试,这时可以使用仿冒:
AbstractBuild build = Mockito.mock(AbstractBuild.class); Mockito.when(build.getResult()).thenReturn(Result.FAILURE);
用于测试Jenkins生成的HTML页面:
HtmlPage page = j.createWebClient().goTo("computer/test/");
HtmlElement navbar = page.getElementById("left-top-nav");
assertEquals(1,navbar.selectNodes(".//a").size());
HtmlPage configPage = j.createWebClient().goTo("configure");
HtmlForm form = configPage.getFormByName("config");
form.submit((HtmlButton)last(form.getHtmlElementsByTagName("button")));
public void setEnvironmentVariables() throws IOException {
EnvironmentVariablesNodeProperty prop = new EnvironmentVariablesNodeProperty();
EnvVars envVars = prop.getEnvVars();
envVars.put("sampleEnvVarKey", "sampleEnvVarValue");
j.jenkins.getGlobalNodeProperties().add(prop);
}
下面的代码允许任何用户名登陆,只要输入的密码和用户名一致:
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
要实现一个一次性的Builder,可以选择继承TestBuilder:
FreeStyleProject project = j.createFreeStyleProject();
project.getBuildersList().add(new TestBuilder() {
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
// 向工作区中的文件中写入数据
build.getWorkspace().child("abc.txt").write("hello","UTF-8");
return true;
}
});
project.scheduleBuild2(0);
Jenkins中构建时异步发生的,如果需要在测试代码中进行同步,可以使用OneShotEvent:
final OneShotEvent buildStarted = new OneShotEvent();
FreeStyleProject project = j.createFreeStyleProject();
project.getBuildersList().add(new TestBuilder() {
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher,
BuildListener listener) throws InterruptedException, IOException {
// 构建真正开始后,发起信号
buildStarted.signal();
return true;
}
});
project.scheduleBuild2(0);
// 阻塞直到信号到达
buildStarted.block();
如果希望使用其它Jenkins插件提供的类,只需要将目标插件声明为Maven依赖即可。很多插件专门提供基础功能,供其它插件依赖,例如Git Client Plugin。
应当尽量依赖插件(而不是自己引入jar依赖),因为hpi打包时,依赖的插件(以及这些插件的传递性依赖)会自动排除,可以减小压缩包的大小。
共享库没有ClassLoader隔离机制,Jenkins Core的同名类会优先使用,从而导致冲突。
解决办法是,将存在冲突的部分代码封装到插件中,然后共享库调用插件。
插件具有类隔离机制,通过适当的配置即可避免和Jenkins Core的类冲突。但是,在基于JenkinsRule进行测试时,这种类隔离机制不生效,需要修改JenkinsRule的源码。
Leave a Reply