Jenkins系列之pipeline語法介紹與案例

網名餘先生發表於2022-02-20

Jenkins Pipeline 的核心概念:

  • Pipeline 是一套執行於Jenkins上的工作流框架,將原本獨立執行於單個或者多個節點的任務連線起來,實現單個任務難以完成的複雜流程編排與視覺化

  • PipelineJenkins2.X最核心的特性,幫助Jenkins 實現從CI到CD與DevOps的轉變。

  • Pipeline是一組外掛,讓Jenkins可以實現持續交付管道的落地和實施。

持續交付管道(CD Pipeline)是將軟體從版本控制階段到交付給使用者或客戶的完整過程的自動化表現。軟體的每一次更改(提交到原始碼管理系統)都要經過一個複雜的過程才能被髮布。

Pipeline提供了一組可擴充套件的工具,通過Pipeline Domain Specific Language(DSL)syntax可以達到Pipeline as Code(Jenkinsfile儲存在專案的原始碼庫)的目的。

Pipeline入門:

先決條件
要使用Jenkins Pipeline,您將需要:

  • Jenkins 2.x或更高版本
  • Pipeline外掛(請自行在外掛管理中安裝。)

Pipeline 定義

指令碼Pipeline是用Groovy寫的 。Groovy相關語法請移步>

Pipeline支援兩種語法:

  • Declarative 宣告式
  • Scripted pipeline 指令碼式
    在這裡插入圖片描述

如何建立基本Pipeline

  • 直接在Jenkins網頁介面中輸入指令碼。
  • 通過建立一個Jenkinsfile可以檢入專案的原始碼管理庫。

用任一方法定義Pipeline的語法是一樣的,但是Jenkins支援直接進入Web UI的Pipeline,通常認為最佳實踐是在Jenkinsfile Jenkins中直接從原始碼控制中載入Pipeline。

在Web UI中定義Pipeline

要在Jenkins Web UI中建立基本Pipeline

  1. 單擊Jenkins主頁上的New Item。
    在這裡插入圖片描述

  2. 輸入Pipeline的名稱,選擇Pipeline,然後單擊確定。
    在這裡插入圖片描述

  3. 在指令碼文字區域中,輸入Pipeline,然後單擊儲存。
    在這裡插入圖片描述

  4. 單擊立即生成以執行Pipeline,檢視控制檯輸出。
    在這裡插入圖片描述

Pipeline幾個核心概念:

  • Stages:階段組/Stage:階段

    1. 一個 Pipeline 有多個 Stage 組成,每個 Stage 包含一組 Step。
    2. 注意一個 Stage 可以跨多個 Node 執行,即 Stage 實際上是 Step 的邏輯分組。
    3. 一個Jenkinsfile 可以分為大的階段,如打包、構建、 部署。測試
    4. 構建的流程,可以分為這幾步,獲取原始碼,然後打包,構建,進行編譯,替換配置檔案,編譯完打包,進行部署 這個階段就是stage
  • Node:節點,一個Node就是一個Jenkins節點,或者是Master,或者是Agent,是執行Step的具體執行環境。

  • Steps:步驟,Step是最基本的操作單元,小到建立一個目錄,大到構建一個Docker映象,由各類Jenklins Plugin提供,例如:sh ‘make’

Pipeline幾個核心關鍵字:

  • 塊(blocks{}):
    由大括號括起來的語句,如pipeline{},Section{},parameters{},script{}

  • 章節(Sections):
    通常包含一個或多個指令或步驟。如 agent 、post、stages、steps

  • 指令(Directives):
    environment、options、parameters、triggers(觸發)、stage、tools、when

  • 步驟(Steps):
    執行指令碼式pipeline:在該語句塊內使用script{}

  • agent
    必須存在,agent必須在pipeline塊內的頂層定義,但stage內是否使用是可選的
    引數:any/none/label/node/docker/dockerfile
    常用選項 label/cuetomWorkspace/reuseNode

指令名 說明 作用域
agent 定義執行任務的代理 stage 或pipeline
input 暫停pipeline,提示輸入內容 stage
environment 設定環境變數 stage或pipeline
tools 自動下載並安裝指定的工具,並將其加入到PATH變數中 stage或pipeline
options 配置Jenkins pipeline本身,如options{retry(3}},指pipeline失敗時再重試2次 stage 或 pipeline
build 觸發其他的job steps
when 定義階段執行的條件 stage
triggers 定義執行pipeline的觸發器 pipeline
parameters 執行pipeline前傳入一些引數 pipeline
parallel 並行執行多個step stage

示例:

  • agent:

    agent { label 'my-label' }
    
    agent {
        node {
            label 'my-label'
            customWorkspace '/some/other/path'
        }
    }
    
    agent {
        docker {
            image 'application_name:verison'
            label 'my-label'
            args '-v /tmp:/tmp'
        }
    }
    
  • stage間通過stash進行檔案共享,即使stage不在同一個執行主機上:

    pipeline{
        agent none
        stages{
            stage('stash'){
                agent { label "master" }
                steps{
                    writeFile file: "a.txt", text: "$BUILD_NUMBER"
                    stash name: "abc", includes: "a.txt"
                }
            }
            stage('unstash'){
                agent { label "node" }
                steps{
                    script{
                        unstash("abc")
                        def content = readFile("a.txt")
                        echo "${content}"
                    }
                }
            }
        }
    }
    
  • steps中的一些操作:

    命令名 說明
    error 丟擲異常,中斷整個pipeline
    timeout timeout閉包內執行的步驟超時時間
    waitUntil 一直迴圈執行閉包內容,直到return true,經常與timeout同時使用
    retry 閉包內指令碼重複執行次數
    sleep 暫停pipeline一段時間,單位為秒
    pipeline{
        agent any
        stages{
            stage('stash'){
                steps{
                    timeout(50){  
                        waitUntil{  
                            script{
                                def r = sh script: 'curl http://xxx', returnStatus: true 
                                return (r == 0)  
                            }
                        }
                    }
                    retry(10){ 
                        script{
                            sh script: 'curl http://xxx', returnStatus: true
                        }
                    }
                    sleep(20) 
                }
            }
        }
    }
    
  • triggers:定時構建

    pipeline {
      agent any
      triggers {
          cron('H 9 * * *')
      	}
    }
    
  • paramparameters:引數化構建

    1. pipeline 指令碼
      pipeline {
          agent any
          parameters {
            choice(name: 'ENV', choices: 'dev\nsit\nuat', description: '環境')
            // 或者 choice(name: 'ENV', choices: ['dev','sit',uat'], description: '環境')
            string(name: 'PROJECT', defaultValue: 'example-demo', description: '專案')
            booleanParam(defaultValue: true, description: '', name: 'BOOLEAN')
            text(defaultValue: '''this is a multi-line 
                                  string parameter example
                                  ''', name: 'MULTI-LINE-STRING')
          }
          stages {
              stage('Hello') {
                  steps {
                      echo 'Hello World'
                  }
              }
          }
      }
      
      
    2. web_ui配置:
      在這裡插入圖片描述
      也可用於選擇git分支/tag 進行釋出
      1. 新增引數型別為:
        在這裡插入圖片描述
      2. 編寫groovy指令碼:
        替換掉自己的git專案地址!
    def gettags = ("git ls-remote --heads  --tags ssh://git@{ your ip }/xx/project_name.git").execute()
    if (ENV.equals("pre")){
      gettags = ("git ls-remote --heads  --tags ssh://git@{ your ip }/xx/project_name.git").execute()
    } 
    def repoNameList = gettags.text.readLines().collect {
        it.split()[1].replaceAll('refs/heads/', '').replaceAll('refs/tags/', '').replaceAll("\\^\\{\\}", '')
    }
    repoNameList.eachWithIndex { it, i ->
      if (it.equals("master")){
        repoNameList[i]= "master:selected"
     }
    }
    return repoNameList
    

    在這裡插入圖片描述
    在這裡插入圖片描述

    效果:
    在這裡插入圖片描述

  • post:後置操作

    Jenkinsfile (Declarative Pipeline)
    pipeline {
        agent any
        stages {
            stage('Hello') {
                steps {
                    sh 'ls'
                }
                post {
    		        always {
    		          echo '步驟Hello體裡的post操作'
    		        }
      			}
            }
        }
        // post部分可以同時包含多種條件塊。
        post {
            always { 
                echo '永遠都會執行'
            }
            success { 
                echo '本次構建成功時執行'
            }
            unstable { 
                echo '構建狀態為不穩定時執行。'
            }
            failure { 
                echo '本次構建失敗時執行'
            }
            changed {  
                echo '只要本次構建狀態與上一次構建狀態不同就執行。'
            }
            fixed {
            	echo '上一次構建狀態為失敗或不穩定,當前完成狀態為成功時執行。'
    		}
    		regression {
    			echo '上一次構建狀態為成功,當前構建狀態為失敗、不穩定或中止時執行。'
    		}
    		aborted {
    			echo '當前執行結果是中止狀態時(一般為人為中止)執行。'
    		}
    		cleanup {
    			echo '清理條件塊。不論當前完成狀態是什麼,在其他所有條件塊執行完成後都執行'}
        }
    }
    

    效果:

    在這裡插入圖片描述

案例:

  • 執行自動化測試指令碼並通過飛書卡片訊息傳送執行結果到飛書群:
    先上效果圖:
    在這裡插入圖片描述
    圖中用例執行結果需在程式碼中獲取pytest執行結果統計
    需在最外層conftest中加以下程式碼:
    from _pytest import terminal
    
    def pytest_terminal_summary(terminalreporter):
    	"""收集測試結果,注:跟xdist外掛不相容"""
    	# print(terminalreporter.stats)
    	total = terminalreporter._numcollected - len(terminalreporter.stats.get('deselected', []))  # 收集總數-未選中用例數
    	passed = len([i for i in terminalreporter.stats.get('passed', []) if i.when == 'call'])
    	failed = len([i for i in terminalreporter.stats.get('failed', []) if i.when == 'call'])
    	error = len([i for i in terminalreporter.stats.get('error', []) if i.when != 'teardown'])
    	skipped = len([i for i in terminalreporter.stats.get('skipped', []) if i.when != 'teardown'])
    	pass_rate = round(passed / (total - skipped) * 100, 2)
    
    	# terminalreporter._sessionstarttime 會話開始時間
    	duration = time.time() - terminalreporter._sessionstarttime
    	# 將結果寫入到一個檔案中,後續再讀取出來
    	result_path = os.path.join(PROJECT_DIR, "reports/result.json")
    	with open(result_path, "w")as fp:
    		fp.write(
    			str({"total": total, "passed": passed, "failed": failed, "error": error, "skipped": skipped,
    				 "pass_rate": pass_rate, "duration": round(duration, 3)}))
    
    

如何傳送卡片訊息到飛書群(企微/釘釘同理):

  1. 建立群機器人
    在這裡插入圖片描述
    在這裡插入圖片描述
    儲存好這個webhook接收地址!
    在這裡插入圖片描述

  2. 官網查詢開發者文件,定製訊息樣式。
    本文使用訊息卡片,自己按需使用
    在這裡插入圖片描述

  3. 編寫傳送訊息指令碼

    #!/bin/env python3
    # -*- coding: utf-8 -*-
    # desc:封裝Jenkins構建結果傳送飛書通知
    
    import requests
    import sys
    
    
    class LarkBotReq(object):
    	# 機器人webhook接收地址
       API_ENDPOINT = "https://open.feishu.cn/open-apis/bot/v2/hook/{}"
    
       def __init__(self, bot_id=None, current_result=None, result_space=None):
       	# 獲取用例執行結果
           with open(result_space, 'r') as fp:
               result = fp.read()
           result = eval(result)
           self.BUILD_MSG_TMPL = '''*構建結果:*
           部署環境: **{env}**
           當前版本: **{target}**
           構建結果: **{current_result}**
           構建發起: **{build_user}**
           持續時間: **{duration}**
           構建日誌: [點選檢視詳情]({build_url}console)
           提交資訊: [點選檢視詳情]({git_url}/commit/{env_commit})
           '''
           self.RUN_MSG_TMPL = f'''\n --------------\n*本次執行結果:*
           總用例數: **{result["total"]}**
           通過用例: **{result['passed']}**
           失敗用例: **{result["failed"]}**
           跳過用例: **{result['skipped']}**
           異常用例:**{result['error']}**
           通過率(%): **{result['pass_rate']}**
           執行耗時:**{result['duration']}s**
           '''
           self.COMMIT_MSG_TMPL = '''\n --------------\n*歷史更新記錄:*
           {git_commit_msg}
           '''
    
           self.url = self.API_ENDPOINT.format(bot_id)
           self.body = None
    
           self.msg_tmpl = self.BUILD_MSG_TMPL + self.COMMIT_MSG_TMPL + self.RUN_MSG_TMPL
    
       def post_json(self):
           # Send http request with json body
           headers = {
               "Content-Type": "application/json; charset=utf-8"
           }
           res = requests.post(self.url, json=self.body, headers=headers)
           return res.text
    
       def formatter(self, **kwargs):
           self.body = {
               "msg_type": "interactive",
               "card": {
                   "config": {
                       "wide_screen_mode": True
                   },
                   "header": {
                       "title": {
                           "tag": "plain_text",
                           "content": "{project} 專案構建資訊".format(**kwargs)
                       },
                       # 控制卡片顏色
                       "template": "green" if result.get('pass_rate') >= 100 else "red"
                   },
                   "elements": [
                       {
                           "tag": "markdown", # 使用markdown格式
                           "content": self.msg_tmpl.format(**kwargs),
                       }
                   ]
               }
           }
    
    
    if __name__ == '__main__':
       bot = LarkBotReq(bot_id=sys.argv[1], current_result=sys.argv[2], result_space=sys.argv[12])
       bot.formatter(
           project=sys.argv[3],
           env=sys.argv[4],
           target=sys.argv[5],
           current_result=sys.argv[2],
           build_user=sys.argv[6],
           duration=sys.argv[7],
           build_url=sys.argv[8],
           git_url=sys.argv[9],
           env_commit=sys.argv[10],
           git_commit_msg=sys.argv[11]
       )
       print(bot.post_json())
    
    """
    呼叫方:
    /usr/local/python3/bin/python3 send_msg_2_lark.py '你的機器人id' ' success' 'project_name' 'env' 'master' '{buildUser}' '{持續時間}' '{build_url}' '{git_url}' '{commit_id}' '{commit_msg}' 'reports/result.json'
    
    """
    
  4. 把上面這個傳送訊息指令碼放到jenkins伺服器的某個目錄裡,如:/data/deploy/notify

  5. 配置jenkins

    • 配置上文提到的獲取git分支/tag groovy指令碼

    • 編寫pipeline指令碼
      下文中引用的credentialsId配置credentialsId參閱往期>
      下文中的測試報告外掛使用的是pytest-html,請自行去外掛管理下載HTML Publisher;如果用的allure替換掉下文的pipeline中publishHTML配置。

      在這裡插入圖片描述

      下文中所引用的env.xx變數是Jenkins提供的全域性變數,請自行查閱;你的job地址後拼接/pipeline-syntax/globals即可查閱。
      或:

      在這裡插入圖片描述 在這裡插入圖片描述

      def project_url = 'ssh://git@{your host}/xx/project_name.git' // 用於拉程式碼
      def GIT_URL = 'https://{your host}/xx/project_name' // 用於傳送的訊息卡片跳轉
      def credentialsId = 'your credentialsId' // git登陸憑證id
      pipeline {
        agent any
          parameters {
            choice(name: 'ENV', choices: ['dev','sit','prod'], description: '環境')
            string(name: 'PROJECT', defaultValue: 'xx_project', description: '專案')
        }
        triggers {
            cron('H 9 * * *')
        }
        stages {
          stage('Checkout') {
              steps {
                script {
                  // 使用echo 輸出變數需用雙引號,單引號會當成字串
                  echo "${target}"
                  checkout([$class: 'GitSCM', branches: [[name: '$target']], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CleanBeforeCheckout']], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '${credentialsId}', url: '$project_url']]])
                  echo 'Checkout'
                  // 此處路徑地址需要和前文中conftest中配置的一致,傳送訊息的指令碼需讀取;workspace相當於你的專案根路徑
                  result_json_path="${env.WORKSPACE}"+"/"+"reports/result.json"
                  echo "${result_json_path}"
               }
            }
          }
          stage('Set Build Name And Description'){
              steps{
                  script{
                      currentBuild.displayName = "#${BUILD_NUMBER} *** ${PROJECT} ** ${ENV} * ${target}"
                      currentBuild.description = "本次構建資訊:${BUILD_NUMBER} 專案名:${PROJECT} 環境:${ENV} 分支名:${target}"
                  }
              }
          }
          stage('get_GIT_COMMIT_MSG') {
              steps {
                  script {
                      env.GIT_COMMIT_MSG = sh (script: 'git log -1 --pretty=%B ${GIT_COMMIT}', returnStdout: true).trim()
                      env.COMMT= sh(  returnStdout: true, script: 'git log --oneline -1 | awk \'{print \$1}\'')
                      echo "${env.GIT_COMMIT_MSG}"
                  }
              }
          }    
      
          stage('get_BUILD_USER') {
              steps {
                  script{
                      wrap([$class: 'BuildUser']) {
                          BUILD_USER = "${env.BUILD_USER}"
                     
                  }
                  if ("${BUILD_USER}"!="null"){
                      echo "${BUILD_USER}" 
                  }else{
                      BUILD_USER = "定時器"
                  }
                  }
              }
          }
          stage('執行全部專案檢查指令碼 ') {      
            steps {
            // 執行測試指令碼
              sh '''
              /usr/bin/python3 -m pytest tests --html=reports/report.html --self-contained-html
              '''
            }
          }
        }
        post {
          always {
            //jenkins外掛中的pytest-html報告外掛,如果用allure的自行替換即可
              publishHTML (target:[
                  allowMissing: false,
                  alwaysLinkToLastBuild: true,
                  keepAll: true,
                  reportDir: './reports/',
                  reportFiles: 'report.html',
                  reportName: 'HTML Report',
                  reportTitles: ''
                ]
              )
              script{
              // 執行前面的傳送訊息指令碼,機器人id請自行替換。
              sh """
              cd /data/deploy/notify
              /usr/local/python3/bin/python3 send_msg_2_lark_for_test.py '機器人id' '${currentBuild.currentResult}' \
              '${PROJECT}' '${ENV}' '${target}' '${BUILD_USER}' '${currentBuild.durationString}' '${env.BUILD_URL}' '${GIT_URL}' '${env.COMMT}' '${env.GIT_COMMIT_MSG}' '${result_json_path}'
              """
              }
          }
        }
      }
      
  6. 執行job
    在這裡插入圖片描述
    在這裡插入圖片描述

相關文章