在jenkins中存在两种类型的pipeline, 之前我们讲述的都是普通类型的pipeline。而多分支pipeline作为一个非常重要的类型重要为我们完成持续集成的第一个步骤—打通gitlab的通信。这样研发再push代码后通知jenkins运行我们预先定义的pipeline完成整个持续集成流程。要做到这样的效果需要分别在jenkins和gitlab中做如下准备工作。
安装gitlab插件
在安全设置中添加jenkins 凭据
类型选择:Gitlab API Token (获取方式:在gitlab中使用自己的账户登录,在User settings中找到Access Tokens。在这里创建一个token)
复制这个token保存到上面说的jenkins 凭据中。
在jenkins中创建一个job,选择类型为多分支pipeline。在git中填写研发的repo地址,jenkins 凭据以及要监控的分支。如下:
到研发的repo中,添加一个跟jenkins通信的webhook。需要进入settings->integration→添加webhook。中间要填写jenkins job的url 以及 勾选push event和merge event。如下:
注意:jenkins job 的url的格式是:http://JENKINS_URL/project/PROJECT_NAME
通过上面的配置,我们就打通了jenkins 与 gitlab的通信。一旦有研发在提交代码和提交merge的时候就会触发这个多分支pipeline运行。
多分支pipeline的规则是打通了研发repo中所有分支的事件。可以理解为它监控了repo中的所有分支的代码变动。所以它不准在job的脚本框中编写pipeline,我们需要在研发的分支中添加jenkinsfile来保存我们的pipeline。
注意:多分支pipeline在创建后就会扫描研发repo中所有的分支并寻找jenkinsfile文件。所以我们要在所有需要执行pipeline的分支中都要编写一份pipeline。如果jenkins找不到jenkinsfile就不会监控此分支。jenkinsfile中的pipeline脚本与普通的pipeline语法一致,没有区别。
根据上面的配置, 多分支pipeline扫描了研发的分支后决定跟踪哪些分支的事件, 这里有两个规则, 一个是它只会监控有jenkinsfile的分支,如果没有就不跟踪, 这跟上面说的一样, 第二个规则是在job可以根据正则表达式来配置都跟踪哪些分支。当配置生效后, 多分支pipeline会定期的去扫描研发的repo去获取最新的要跟踪的分支信息。然后会根据分支名字作为job名字。当跟踪了多个分支后就会在多分支pipeline下创建多个pipeline job出来, 就像上面的图, 只不过因为我们这个项目走的是主线开发模型, 所以我配置的只跟踪master分支。
在第四范式我们提倡测试的透明化和可视化。我们希望每个项目都有一个质量看板,能够实时的反应出当前的bug,测试用例,覆盖率等相关信息。举个例子, 我们最近开展的sdk项目中,我制作了如下的看板。
为了达到上面的效果, 我们采取了如下的工具链:
allure:不论是java,python还是js语言来做测试, 选取的测试报告框架一律都是allure,这个框架在兼容各个主流语言的同时,同时会暴露出http接口可以供我们抓取测试结果。而详尽的用例分类展示功能,也可以让我们统计每个模块的测试用例数据。这也是上面我能制作出模块级别的测试用例变化趋势图的原因。
jenkins shared library:利用之前介绍的这个功能,我们编写了统一的库。可以让各个项目对接, 只要用了allure作为report的,都可以无缝对接。
metabase:这是一个开源的BI软件,特点是能对接各种数据库, 可以在UI上很方便的配置出各种图标, 上述的效果都是用metabase制作的。
/** * Created by sungaofei on 19/3/1. */ @Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7') @Grab(group = 'org.jsoup', module = 'jsoup', version = '1.10.3') import org.jsoup.Jsoup import groovyx.net.http.HTTPBuilder import static groovyx.net.http.ContentType.* import static groovyx.net.http.Method.* import groovy.transform.Field //可以指定maven仓库 //@GrabResolver(name = 'aliyun', root = 'http://maven.aliyun.com/nexus/content/groups/public/') //加载数据库连接驱动包 //@Grab('mysql:mysql-connector-java:5.1.25') //@GrabConfig(systemClassLoader=true) //global variable @Field jenkinsURL = "http://auto.4paradigm.com" @Field int passed @Field int failed @Field int skipped @Field int broken @Field int unknown @Field int total @Field Map<String, Map<String, Integer>> map = new HashMap<>() @NonCPS def getResultFromAllure() { def reportURL = "" if (env.BRANCH_NAME != "" && env.BRANCH_NAME != null) { reportURL = "/view/API/job/${jobName}/job/${env.BRANCH_NAME}/${BUILD_NUMBER}/allure/" } else { reportURL = "/view/API/job/${JOB_NAME}/${BUILD_NUMBER}/allure/" } // reportURL = "/view/API/job/sage-sdk-test/185/allure/" HTTPBuilder http = new HTTPBuilder(jenkinsURL) //根据responsedata中的Content-Type header,调用json解析器处理responsedata http.get(path: "${reportURL}widgets/summary.json") { resp, json -> println resp.status passed = Integer.parseInt((String) json.statistic.passed) failed = Integer.parseInt((String) json.statistic.failed) skipped = Integer.parseInt((String) json.statistic.skipped) broken = Integer.parseInt((String) json.statistic.broken) unknown = Integer.parseInt((String) json.statistic.unknown) total = Integer.parseInt((String) json.statistic.total) } http.get(path: "${reportURL}data/behaviors.json") { resp, json -> List featureJson = json.children for (int i = 0; i < featureJson.size(); i++) { String featureName = featureJson.get(i).name Map<String, Integer> results = new HashMap<>() results['passed'] = 0 results['failed'] = 0 results['skipped'] = 0 results['broken'] = 0 results['unknown'] = 0 List storyJson = featureJson.get(i).children for (int j = 0; j < storyJson.size(); j++) { List caseJson = storyJson.get(j).children for (int k = 0; k < caseJson.size(); k++) { def caseInfo = caseJson.get(k) String status = caseInfo.status int num = results.get(status) + 1 results[status] = num } } int total = 0 results.each { key, value -> total = total + value } results['total'] = total map.put(featureName, results) } } } def int getLineCov(){ def htmlurl = "${jenkinsURL}/view/API/job/${JOB_NAME}/${BUILD_NUMBER}/_e4bba3_e7a081_e8a686_e79b96_e78e87_e68aa5_e5918a/index.html" String doc = Jsoup.connect(htmlurl).get().getElementsByClass("pc_cov").text(); int cov = Integer.parseInt(doc.replace("%", "")) println("当前行覆盖率为 ${cov}") return cov } def int getBranchCov(){ def htmlurl = "${jenkinsURL}/view/API/job/${JOB_NAME}/${BUILD_NUMBER}/_e4bba3_e7a081_e8a686_e79b96_e78e87_e68aa5_e5918a/index.html" String branchAll = Jsoup.connect(htmlurl).get().select(".total > :nth-child(5)").text(); String branchPartial = Jsoup.connect(htmlurl).get().select(".total > :nth-child(6)").text(); println("all branch number: ${branchAll}") println("cover branch number: ${branchPartial}") def cov = Integer.parseInt(branchPartial)/Integer.parseInt(branchAll) println("the branch cov is ${cov}") return cov } def call() { def version = "release/3.8.2" getResultFromAllure() getDatabaseConnection(type: 'GLOBAL') { map.each { feature, valueMap -> def sqlString = "INSERT INTO func_test (name, build_id, feature, version, total, passed, unknown, skipped, failed, broken, create_time) VALUES ('${JOB_NAME}', '${BUILD_ID}', '${feature}', '${version}', " + "${valueMap['total']}, ${valueMap['passed']}, ${valueMap['unknown']}, ${valueMap['skipped']}, ${valueMap['failed']}, ${valueMap['broken']}, NOW())" println(sqlString) sql sql: sqlString } def lineCov = getLineCov() def branchCov = getBranchCov() def sqlString = "INSERT INTO func_test_summary (name, build_id, version, total, passed, unknown, skipped, failed, broken, line_cov, branch_cov, create_time) VALUES ('${JOB_NAME}', '${BUILD_ID}', '${version}', " + "${total}, ${passed}, ${unknown}, ${skipped}, ${failed}, ${broken}, ${lineCov}, ${branchCov}, NOW())" sql sql: sqlString } }
上面的注意点:
使用grab 来下载http builder 和 jsoup的依赖, 分别用来请求allure的http接口, 以及使用jsoup来解析覆盖率这种纯html页面。
allure的分为allure1和allure2, 不同版本对外暴露的接口不一样, 这一点要注意,我这里统一使用allure2. 具体的接口细节可以在chrome上去抓
allure的接口用到的两个:summary用来获取整体的测试用例情况, 而behaviors 则能获取每一个模块的测试用例细节。
最后入库的sql指令使用的是database和mysql-database的插件,可以在jenkins上的插件管理中下载。然后在全局配置中,按如下进行配置。
PS:我曾经想要使用groovy的jdbc 包来与mysql通信, 但是在jenkins中的groovy毕竟跟常规的不一样,导致依赖包无法加载, 一直没有办法解决这个问题。所以只能用jenkins的插件来曲线救国了。
至于Metabase这个BI软件的教程就不写了, 非常简单,大家随便去搜一下吧, 我当时都没看文档, 直接启动起来以后玩两下子就行了。上面统计bug的信息是做了个定时任务,定时的去抓jira的接口然后入库搞的, 也就不详细讲了。
这个系列写到这,关于jenkins pipeline以及相关工具的介绍就结束了, 下一期开始写docker&k8s。
↙↙↙阅读原文可查看相关链接,并与作者交流