前言
隨着pipeline交付流水線在團隊中的推廣,使用pipeline腳本的job也迅速增加。雖然我們已經基於公司的技術棧特點做了一個盡可能通用的pipeline腳本樣例,讓搭建者只需要修改幾個賦值參數就可以在自己的項目中應用,初衷是希望所有人能理解pipeline中的過程,但也發現一些比較麻煩的問題,比如有些人不熟悉具體的腳本拿來隨意刪改導致各種錯誤,還有就是我們在pipeline腳本中增加一些新功能時又需要通知所有的pipeline維護人員去修改,過程非常糾結。
這時候就意味着我們需要用到pipline的共享庫功能(Shared Libraries)了,在各種項目之間共享pipeline核心實現,以減少冗余並保證所有job在構建的時候會調用最新的共享庫代碼 。
這篇我們就介紹下pipeline的這個黑科技:Shared Libraries
目錄結構
Shared Library通過庫名稱、代碼檢索方法(如SCM)、代碼版本三個要素進行定義,庫名稱盡量簡潔,因為它會在腳本中被調用,在編寫 Shared Library的時候,我們需要遵循固定的代碼目錄結構。
Shared Library代碼目錄結構如下:
src目錄就是標准的Java源目錄結構。執行Pipeline時,該目錄將添加到類路徑中。
vars目錄托管定義可從Pipeline訪問的全局腳本(一般我們可以在這里編寫標准化腳本)。通常,每個.groovy文件的基本名稱應使用駝峰(camelCased)模式,.txt(如果存在)可以包含格式化處理的文檔。
resources目錄允許libraryResource從外部庫中使用步驟來加載相關聯的非Groovy文件。目前內部庫不支持此功能。
定義全局庫
這里只介紹全局 Shared Library的方式,通過Manage Jenkins » Configure System » Global Pipeline Libraries 的方式可以添加一個或多個共享庫。
這些庫將全局可用,系統中的任何Pipeline都可以利用這些庫中實現的功能。並且通過配置SCM的方式,可以保證在每次構建時獲取到指定Shared Library的最新代碼。
動態加載庫
從2.7版本起,Pipeline: Shared Groovy Libraries plugin插件提供了一個新的參數“library”,用於在腳本中加載(non-implicit)庫
如果只需要加載全局變量/函數感興趣(從vars/目錄中),語法非常簡單:
此后腳本中可以訪問該庫中的任何全局變量。
library 'my-shared-library'
采用此方式從src/目錄中引用類也是可以的,不過只能動態地使用庫類(無類型檢查),從library步驟的返回值通過指定名稱訪問它們。比如static可以使用類似Java的語法來調用方法:
library('my-shared-library').com.mycorp.pipeline.Utils.someStaticMethod()
使用該library步驟時,您還可以指定一個版本,該指定版本將會覆蓋默認版本。
library 'my-shared-library@master'
Shared Libraries實戰
我們在https://testerhome.com/topics/10010已經介紹了一個項目樣例,可以看到過程已經非常復雜,讓普通業務測試人員管理確實起來確實有點困難。
通過參數化處理后,除了一些各項目的業務變量,整個過程在所有項目都是通用的,完全適合采用共享庫的方式進行改造,屏蔽腳本的復雜度。
在改造之前我們勾畫了兩種思路:pipeline模塊庫和模版庫(姑且這么叫吧)。
1.模塊庫方式
模塊庫的方式,其實就是考慮把各個stage的實現通過函數化的方式抽象出來,比如獲取代碼的stage實現我們就抽象出codeFetch(),單元測試的 stage我們就抽象出unitTest().
特點:業務測試人員負責維護pipeline的初始賦值和整體結構,靈活度高,可自主裁剪stage場景
不足:整體結構還是比較復雜,需要維護的共享腳本比較多,無法對交付流水線過程進行統一管理,Declarative Pipeline只支持script部分腳本的共享庫。
pipeline代碼樣例:
#!groovy library 'weiyi-pipeline-library' pipeline { agent any parameters { //repoBranch參數 string(name:'repoBranch', defaultValue: 'master', description: 'git分支名稱') //服務器選擇 choice(name: 'server',choices:'192.168.1.107,9090\n192.168.1.60,9090', description: '測試服務器列表選擇(IP,JettyPort,Name,Passwd)') string(name:'dubboPort', defaultValue: '31100', description: '測試服務器的dubbo服務端口') //單元測試代碼覆蓋率要求,各項目視要求調整參數 string(name:'lineCoverage', defaultValue: '20', description: '單元測試代碼覆蓋率要求(%),小於此值pipeline將會失敗!') //若勾選在pipelie完成后會郵件通知測試人員進行驗收 booleanParam(name: 'isCommitQA',description: '是否在pipeline完成后,郵件通知測試人員進行人工驗收',defaultValue: false ) } //環境變量,初始確定后一般不需更改 tools { maven 'maven3' jdk 'jdk8' } ....... //pipeline的各個階段場景 stages { stage('代碼獲取') { steps { codeFetch() } } stage('單元測試') { steps { unitTest() } } } }
共享庫代碼:
// vars/codeFetch.groovy
def call() { echo "starting fetch code......" }
2.模版庫方式
Declarative 1.2(released in late September, 2017),開始支持整條Declarative Pipeline作為共享庫,使用條件如下:
Only entire pipeline`s can be defined in shared libraries as of this time. This can only be done in `vars/*.groovy, and only in a callmethod. Only one Declarative Pipeline can be executed in a single build, and if you attempt to execute a second one, your build will fail as a result.
特點:可以將整條declarative pipeline作為共享庫讓各個項目調用,業務測試人員只需要維護初始化賦值參數即可。
不足:公司技術棧不統一的話,pipeline模版庫的適配能力需要比較強(比如可能會出現虛擬機/docker共存,gradle/maven共存等多種情況),可能需要定義多個模版庫,不過這些問題通過groovy代碼邏輯上應該都可以控制。
pipeline代碼樣例(敏感信息隱藏):
#!groovy library 'weiyi-pipeline-library' def map = [:] /*參數化變量,運行時可選擇*/ //git分支名稱 map.put('repoBranch','master') //測試服務器列表選擇(IP,JettyPort,Name,Passwd) map.put('server','192.168.1.107,9090\n192.168.1.60,9090') //測試服務器的dubbo服務端口 map.put('dubboPort','31100') //單元測試代碼覆蓋率要求,各項目視要求調整參數 map.put('lineCoverage','20') /*環境變量,初始確定后一般不需更改*/ map.put('maven','maven3') map.put('jdk','jdk8') /*常量參數,初始確定后一般不需更改*/ map.put("isDocker",false) //項目gitlab代碼地址 map.put('REPO_URL','****') //git服務全系統只讀賬號,無需修改 map.put('CRED_ID','****') //pom.xml的相對路徑 map.put('POM_PATH','pom.xml') //生成war包的相對路徑 map.put('WAR_PATH','rpc/war/target/*.war') //測試人員郵箱地址 map.put('QA_EMAIL','***') //接口測試job名稱 map.put('ITEST_JOBNAME','Guahao_InterfaceTest_ExpertPatient') pipelineCall("maven",map)
共享庫代碼:
#!groovy def call(String type,Map map) { if (type == "maven") { pipeline { agent any //參數化變量,目前只支持[booleanParam, choice, credentials, file, text, password, run, string]這幾種參數類型,其他高級參數化類型還需等待社區支持 parameters { //固定設置三類pipeline場景 choice(name:'scene',choices:"scene1:完整流水線\nscene2:代碼檢查\nscene3:測試部署", description: '場景選擇,默認運行完整流水線,如果只做開發自測可選擇代碼檢查,如果只做環境部署可選擇測試部署') //repoBranch參數后續替換成git parameter不再依賴手工輸入,JENKINS-46451 string(name:'repoBranch', defaultValue: "${map.repoBranch}", description: 'git分支名稱') //服務器相關參數采用了組合方式,避免多次選擇 choice(name: 'server',choices:"${map.server}", description: '測試服務器列表選擇') string(name:'dubboPort', defaultValue: "${map.dubboPort}", description: '測試服務器的dubbo服務端口') //單元測試代碼覆蓋率要求,各項目視要求調整參數 string(name:'lineCoverage', defaultValue: "${map.lineCoverage}", description: '單元測試代碼覆蓋率要求(%),小於此值pipeline將會失敗!') //若勾選在pipelie完成后會郵件通知測試人員進行驗收 booleanParam(name: 'isCommitQA', defaultValue: false, description: '是否在pipeline完成后,郵件通知測試人員進行人工驗收') } //環境變量,初始確定后一般不需更改 tools { maven "${map.maven}" jdk "${map.jdk}" } //常量參數,初始確定后一般不需更改 environment{ REPO_URL="${map.REPO_URL}" //git服務全系統只讀賬號,無需修改 CRED_ID="${map.CRED_ID}" //pom.xml的相對路徑 POM_PATH="${map.POM_PATH}" //生成war包的相對路徑 WAR_PATH="${map.WAR_PATH}" //測試人員郵箱地址 QA_EMAIL="${map.QA_EMAIL}" //接口測試job名稱 ITEST_JOBNAME="${map.ITEST_JOBNAME}" } options { disableConcurrentBuilds() timeout(time: 1, unit: 'HOURS') //保持構建的最大個數 buildDiscarder(logRotator(numToKeepStr: '10')) } //pipeline的各個階段場景 stages { stage('代碼獲取') { steps { //一些初始化操作 script { //根據param.server分割獲取參數 def split=params.server.split(",") serverIP=split[0] jettyPort=split[1] serverName=split[2] serverPasswd=split[3] //場景選擇 println params.scene //單元測試運行場景 isUT=params.scene.contains('scene1:完整流水線') || params.scene.contains('scene2:代碼檢查') println "isUT="+isUT //靜態代碼檢查運行場景 isCA=params.scene.contains('scene1:完整流水線') || params.scene.contains('scene2:代碼檢查') println "isCA="+isCA //部署測試環境運行場景 isDP=params.scene.contains('scene1:完整流水線') || params.scene.contains('scene3:測試部署') println "isDP="+isDP //第三方庫安全性檢查 isDC=params.scene.contains('scene1:完整流水線') println "isDC="+isDC //接口測試運行場景 isIT=params.scene.contains('scene1:完整流水線') println "isIT="+isIT try{ wrap([$class: 'BuildUser']){ userEmail="${BUILD_USER_EMAIL},${QA_EMAIL}" user="${BUILD_USER_ID}" } }catch(exc){ userEmail="${QA_EMAIL}" user="system" } echo "starting fetchCode from ${