第一次做Jenkins插件开发,遂将笔记公开分享
插件名称: gettingCase
插件功能: 获取RallyDev上的某一个Test Case信息
0.配置.m2/settings.xml
请查阅本文最后的参考资料
1.Maven创建Jenkins插件项目
mvn -U org.jenkins-ci.tools:maven-hpi-plugin:create
第一次执行会比较慢,因为需要下载很多Maven插件.
这个创建项目的过程有2步互动:
第一步需要开发者输入Maven项目的groupId
Enter the groupId of your plugin [org.jenkins-ci.plugins]:
com.technicolor.qcs
第二步需要开发者输入Maven项目的artifactId
Enter the artifactId of your plugin (normally without '-plugin' suffix):
gettingCase
2.基于Hello World插件项目,修改以实现自己插件功能
2.1POM
修改pom.xml文件,增加REST访问RallyDev的工具包
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.jenkins-ci.plugins</groupId> <artifactId>plugin</artifactId> <version>1.509.3</version> <!-- which version of Jenkins is this plugin built against? --> </parent> <groupId>com.technicolor.qcs</groupId> <artifactId>gettingCase</artifactId> <version>1.0-SNAPSHOT</version> <packaging>hpi</packaging> <description>Gets Rally Test Cases</description> <developers> <developer> <id>feuyeux</id> <name>eric han</name> <email>[email protected]</email> </developer> </developers> <dependencies> <dependency> <groupId>com.rallydev.rest</groupId> <artifactId>rally-rest-api</artifactId> <version>2.0.4</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> <version>4.2.1</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.2.1</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient-cache</artifactId> <version>4.2.1</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpmime</artifactId> <version>4.2.1</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>fluent-hc</artifactId> <version>4.2.1</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.1</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.6</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.1</version> </dependency> </dependencies> <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> </project>
2.2 编写全局配置页面
/home/hanl/j-ci/gettingCase/src/main/resources/com/technicolor/qcs/gettingCase/GetCasesBuilder/global.jelly
<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="Getting Test Cases Builder"> <f:entry title="RallyDev User Name" field="userName"> <f:textbox /> </f:entry> <f:entry title="RallyDev Password" field="password"> <f:password/> </f:entry> <f:entry title="HTTP Proxy URL" field="proxyURL"> <f:textbox /> </f:entry> <f:entry title="RallyDev Proxy User Name" field="proxyUser"> <f:textbox /> </f:entry> <f:entry title="RallyDev Proxy Password" field="proxyPassword"> <f:password /> </f:entry> </f:section> </j:jelly>
2.3 编写JOB配置页面
1 app Hudson应用程序对象 ${app.displayName}. //应用的Display名称 2 it 当前UI所属的模型对象 ${it.name} 对应于builder的getName()方法 3 h 一个全局的工具类,提供静态工具方法
/home/hanl/j-ci/gettingCase/src/main/resources/com/technicolor/qcs/gettingCase/GetCasesBuilder/config.jelly
<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="Test Case No." field="testCaseId"> <f:textbox /> </f:entry> </j:jelly>
2.4 编写扩展点方法
一次构建过程通常包括: SCM checkout - check out出源码 Pre-build - 预编译 Build wrapper -准备构建的环境,设置环境变量等 Builder runs - 执行构建,比如调用calling Ant, Make Recording - 记录输出,如测试结果 Notification - 通知成员
/home/hanl/j-ci/gettingCase/src/main/java/com/technicolor/qcs/gettingCase/GetCasesBuilder.java
package com.technicolor.qcs.gettingCase; import hudson.Launcher; import hudson.Extension; import hudson.util.FormValidation; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.AbstractProject; import hudson.tasks.Builder; import hudson.tasks.BuildStepDescriptor; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.QueryParameter; import javax.servlet.ServletException; import java.io.IOException; import java.io.PrintStream; /** * author:feuyeux */ public class GetCasesBuilder extends Builder { public static final String RALLY_URL = "https://rally1.rallydev.com"; private final String testCaseId; @DataBoundConstructor public GetCasesBuilder(String testCaseId) { this.testCaseId = testCaseId; } public String getTestCaseId() { return testCaseId; } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) { PrintStream out = listener.getLogger(); final String userName = getDescriptor().getUserName(); final String password = getDescriptor().getPassword(); final String proxyURL = getDescriptor().getProxyURL(); final String proxyUser = getDescriptor().getProxyUser(); final String proxyPassword = getDescriptor().getProxyPassword(); out.println("RallyDev User Name =" + userName); out.println("HTTP Proxy=" + proxyUser + "@" + proxyURL); out.println("RallyDev Test Case =" + getTestCaseId()+"\n"); RallyClient rallyClient = null; try { rallyClient = new RallyClient(RALLY_URL, userName, password, proxyURL, proxyUser, proxyPassword); String caseInfo = rallyClient.getTestCases(testCaseId); out.println(caseInfo); } catch (Exception e) { out.println(e.getMessage()); } finally { try { rallyClient.close(); } catch (IOException e) { e.printStackTrace(); } } return true; } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Builder> { private String userName; private String password; private String proxyURL; private String proxyUser; private String proxyPassword; public FormValidation doCheckName(@QueryParameter String value) throws IOException, ServletException { if (value.length() == 0) return FormValidation.error("Please set a testCaseId"); return FormValidation.ok(); } public boolean isApplicable(Class<? extends AbstractProject> aClass) { return true; } public String getDisplayName() { return "Getting Test cases from Rally"; } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { userName = formData.getString("userName"); password = formData.getString("password"); proxyURL = formData.getString("proxyURL"); proxyUser = formData.getString("proxyUser"); proxyPassword = formData.getString("proxyPassword"); save(); return super.configure(req, formData); } public String getUserName() { return userName; } public String getPassword() { return password; } public String getProxyURL() { return proxyURL; } public String getProxyUser() { return proxyUser; } public String getProxyPassword() { return proxyPassword; } } }
2.5 编写RallyDev连接和访问方法
/home/hanl/j-ci/gettingCase/src/main/java/com/technicolor/qcs/gettingCase/RallyClient.java
package com.technicolor.qcs.gettingCase; import com.google.gson.JsonObject; import com.rallydev.rest.RallyRestApi; import com.rallydev.rest.request.GetRequest; import com.rallydev.rest.response.GetResponse; import com.rallydev.rest.response.Response; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; /** * author:feuyeux */ public class RallyClient { /*https://github.com/RallyTools/RallyRestToolkitForJavahttps://github.com/RallyTools/RallyRestToolkitForJava */ private final RallyRestApi restApi; public RallyClient(String rally_url, String userName, String password, String proxyURL, String proxyUser, String proxyPassword) throws URISyntaxException { restApi = new RallyRestApi(new URI(rally_url), userName, password); restApi.setProxy(new URI(proxyURL), proxyUser, proxyPassword); } public void close() throws IOException { restApi.close(); } public String getTestCases(String testCaseId) { /*https://rally1.rallydev.com/slm/doc/webservice/*/ StringBuilder result = new StringBuilder(); String version = restApi.getWsapiVersion(); result.append("RallyDev Rest Version=").append(version).append("\n"); final String query = "/testcase/"+testCaseId; GetRequest queryRequest = new GetRequest(query); GetResponse casesResponse = null; try { casesResponse = restApi.get(queryRequest); } catch (IOException e) { e.printStackTrace(); } printWarningsOrErrors(casesResponse, result); JsonObject caseJsonObject = casesResponse.getObject(); result.append("\n").append("Test Case Name: ").append(caseJsonObject.get("Name").getAsString()); result.append("\n").append("Test Case Type: ").append(caseJsonObject.get("Type").getAsString()); result.append("\n").append("Test Case URL: ").append(caseJsonObject.get("_ref").getAsString()); result.append("\n").append("Test Case Creation Time: ").append(caseJsonObject.get("CreationDate").getAsString()); result.append("\n").append("Test Case LastUpdate Time: ").append(caseJsonObject.get("LastUpdateDate").getAsString()); result.append("\n").append("Test Case's Project: ").append( caseJsonObject.get("Project").getAsJsonObject().get("_refObjectName") .getAsString()); result.append("\n").append("Test Case's Workspace: ").append( caseJsonObject.get("Workspace").getAsJsonObject().get("_refObjectName") .getAsString()); return result.toString(); } private void printWarningsOrErrors(Response response, StringBuilder result) { if (response.wasSuccessful()) { result.append("\nSuccess."); String[] warningList; warningList = response.getWarnings(); for (int i = 0; i < warningList.length; i++) { result.append("\twarning:\n" + warningList[i]); } } else { String[] errorList; errorList = response.getErrors(); if (errorList.length > 0) { result.append("\nError."); } for (int i = 0; i < errorList.length; i++) { result.append("\terror:\n" + errorList[i]); } } } }
3.调试Plugin程序
在终端/控制台,首先执行Maven变量配置命令
set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n
cd到插件项目目录,执行如下命令
hanl@hanl-ubuntu1204:~/j-ci/gettingCase$ mvn clean hanl@hanl-ubuntu1204:~/j-ci/gettingCase$ mvnDebug hpi:run
Maven将对8000端口执行监听,以便在IDE中进行断点调试
Preparing to Execute Maven in Debug Mode Listening for transport dt_socket at address: 8000
进入IDE(本例使用IntelliJ),选择Run菜单的Debug,添加一个8000端口的远程服务器
添加断点,点击左下角运行调试按钮
此时,终端将执行Maven构建,并启动Jetty服务器.hanl@hanl-ubuntu1204:~/j-ci/gettingCase$ mvnDebug hpi:run Preparing to Execute Maven in Debug Mode Listening for transport dt_socket at address: 8000 [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building gettingCase 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] >>> maven-hpi-plugin:1.95:run (default-cli) @ gettingCase >>> [INFO] [INFO] --- maven-hpi-plugin:1.95:validate (default-validate) @ gettingCase --- [INFO] [INFO] --- maven-enforcer-plugin:1.0.1:enforce (enforce-maven) @ gettingCase --- [INFO] [INFO] --- maven-enforcer-plugin:1.0.1:display-info (display-info) @ gettingCase --- [INFO] Maven Version: 3.0.4 [INFO] JDK Version: 1.7.0_25 normalized as: 1.7.0-25 [INFO] OS Info: Arch: amd64 Family: unix Name: linux Version: 3.2.0-54-generic [INFO] [INFO] --- maven-localizer-plugin:1.14:generate (default) @ gettingCase --- [INFO] [INFO] --- maven-resources-plugin:2.5:resources (default-resources) @ gettingCase --- [debug] execute contextualize [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 5 resources [INFO] [INFO] --- maven-compiler-plugin:2.5:compile (default-compile) @ gettingCase --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] <<< maven-hpi-plugin:1.95:run (default-cli) @ gettingCase <<< [INFO] [INFO] --- maven-hpi-plugin:1.95:run (default-cli) @ gettingCase --- [INFO] Generating ./work/plugins/gettingCase.hpl [INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/plugins/mailer/1.4/mailer-1.4.jar [INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/plugins/ant/1.1/ant-1.1.jar [INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/main/maven-plugin/1.509.3/maven-plugin-1.509.3.jar [INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/plugins/javadoc/1.0/javadoc-1.0.jar [INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/plugins/subversion/1.26/subversion-1.26.jar [INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/main/ui-samples-plugin/1.509.3/ui-samples-plugin-1.509.3.jar [INFO] Configuring Jetty for project: gettingCase 2013-10-10 18:10:55.444::INFO: Logging to STDERR via org.mortbay.log.StdErrLog [INFO] Context path = /jenkins [INFO] Tmp directory = /home/hanl/j-ci/gettingCase/target/work [INFO] Web defaults = jetty default [INFO] Starting jetty 6.1.1 ... 2013-10-10 18:10:55.587::INFO: jetty-6.1.1 Jenkins home directory: /home/hanl/j-ci/gettingCase/./work found at: System.getProperty("HUDSON_HOME") 2013-10-10 18:10:59.572::INFO: Started SelectChannelConnector @ 0.0.0.0:8080 [INFO] Started Jetty Server [INFO] Console reloading is ENABLED. Hit ENTER on the console to restart the context. Oct 10, 2013 6:10:59 PM jenkins.InitReactorRunner$1 onAttained INFO: Started initialization Oct 10, 2013 6:11:01 PM jenkins.InitReactorRunner$1 onAttained INFO: Listed all plugins Oct 10, 2013 6:11:01 PM jenkins.InitReactorRunner$1 onAttained INFO: Prepared all plugins Oct 10, 2013 6:11:01 PM jenkins.InitReactorRunner$1 onAttained INFO: Started all plugins Oct 10, 2013 6:11:01 PM jenkins.InitReactorRunner$1 onAttained INFO: Augmented all extensions Oct 10, 2013 6:11:04 PM jenkins.InitReactorRunner$1 onAttained INFO: Loaded all jobs Oct 10, 2013 6:11:05 PM org.jenkinsci.main.modules.sshd.SSHD start INFO: Started SSHD at port 54676 Oct 10, 2013 6:11:05 PM jenkins.InitReactorRunner$1 onAttained INFO: Completed initialization Oct 10, 2013 6:11:05 PM hudson.TcpSlaveAgentListener <init> INFO: JNLP slave agent listener started on TCP port 47342 Oct 10, 2013 6:11:05 PM hudson.WebAppMain$2 run INFO: Jenkins is fully up and running
4.测试
在浏览器中录入Jenkins地址,创建Job并按照上述的配置环节,完成配置.
执行Build Job 此时,断点应其作用,可以在IDE中进行调试和监控.当完成调试后,进入Jenkins构建结果页面,观察构建结果是否符合预期.
到此,获取并显示RallyDev中Test Case的Jenkins插件开发完毕.
IntelliJ IDE 插件:
https://wiki.jenkins-ci.org/display/JENKINS/IntelliJ+IDEA+plugin+for+Stapler
Stapler plugin for IntelliJ IDEA
参考资料
https://wiki.jenkins-ci.org/display/JENKINS/Plugin+tutorial
https://github.com/jenkinsci/hello-world-plugin
https://jenkins-ci.org/maven-site/jenkins-core/jelly-taglib-ref.html
ui-samples: http://localhost:8080/jenkins/ui-samples/
扩展点:https://wiki.jenkins-ci.org/display/JENKINS/Extension+points
<f:entry title="Test Station"> <select class="setting-input" name="AndroidBuilder.stationUrl"> <j:forEach var="inst" items="${descriptor.stations}"> <f:option selected="${inst.url==instance.stationUrl}">${inst.url}</f:option> </j:forEach> </select> </f:entry>