1 Pipeline简介

1.1 概念

Pipeline,简单来说,就是一套运行在 Jenkins 上的工作流框架,将原来独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排和可视化的工作。

1.2 使用Pipeline好处

来自翻译自官方文档:

  • 代码:Pipeline以代码的形式实现,通常被检入源代码控制,使团队能够编辑,审查和迭代其传送流程。
  • 持久:无论是计划内的还是计划外的服务器重启,Pipeline都是可恢复的。
  • 可停止:Pipeline可接收交互式输入,以确定是否继续执行Pipeline。
  • 多功能:Pipeline支持现实世界中复杂的持续交付要求。它支持fork/join、循环执行,并行执行任务的功能。
  • 可扩展:Pipeline插件支持其DSL的自定义扩展 ,以及与其他插件集成的多个选项。

1.3 创建 Jenkins Pipeline

Pipeline 脚本是由Groovy 语言实现的,但是我们没必要单独去学习 Groovy

Pipeline 支持两种语法:Declarative(声明式)和Scripted Pipeline(脚本式)语法

Pipeline 也有两种创建方法:可以直接在 Jenkins 的 Web UI 界面中输入脚本;也可以通过创建一个 Jenkinsfile 脚本文件放入项目源码库中(一般我们都推荐在 Jenkins 中直接从源代码控制(SCM) 中直接载入 Jenkinsfile Pipeline 这种方法)。

2 Pipeline插件

Pipeline插件,安装后可以创建流水线job

Pipeline: Stage View插件,构建的时候显示每个阶段视图

3 Pipeline基本应用

3.1 Pipeline script

3.1.1 Declarative声明式

创建项目

Jenkins Pipeline流水线_上传

流水线->选择HelloWorld模板

Jenkins Pipeline流水线_Jenkins_02

编写一个简单声明式Pipeline

pipeline {
  agent any
  stages {
    stage('拉取代码') {
      steps {
        echo '拉取代码'
      }
    }
    stage('编译构建') {
      steps {
        echo '编译构建'
      }
    }
    stage('项目部署') {
      steps {
        echo '项目部署'
      }
    }
  }
}

stages:代表整个流水线的所有执行阶段。通常stages只有1个,里面包含多个stage  

stage:代表流水线中的某个阶段,可能出现n个。一般分为拉取代码,编译构建,部署等阶段。  

steps:代表一个阶段内需要执行的逻辑。steps里面是shell脚本,git拉取代码,ssh远程发布等任意内容。

Jenkins Pipeline流水线_Jenkins_03

3.1.2 Scripted Pipeline脚本式

这次选择"Scripted Pipeline"

Jenkins Pipeline流水线_Jenkins_04

node {
  def mvnHome
    stage('Preparation') { // for display purposes
  }
    stage('Build') {
  }
    stage('Results') {
  }
}

Node:节点,一个 Node 就是一个 Jenkins 节点,Master 或者 Agent,是执行 Step 的具体运行  环境,后续讲到Jenkins的Master-Slave架构的时候用到。  

Stage:阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作,比如: Build、Test、Deploy,Stage 是一个逻辑分组的概念。  

Step:步骤,Step 是最基本的操作单元,可以是打印一句话,也可以是构建一个 Docker 镜像,  由各类 Jenkins 插件提供,比如命令:sh ‘make’,就相当于我们平时 shell 终端中执行 make 命令  一样。

3.2 Pipeline Script from SCM

刚才我们都是直接在Jenkins的UI界面编写Pipeline代码,这样不方便脚本维护,建议把Pipeline脚本放在项目中(一起进行版本控制)

在项目根目录建立Jenkinsfile文件,把内容复制到该文件中

代码提交Jenkinsfile

Jenkins Pipeline流水线_取代码_05

jenkins配置

Jenkins Pipeline流水线_上传_06

Jenkins Pipeline流水线_上传_07

执行结果

Jenkins Pipeline流水线_取代码_08

4 Pipeline实现Maven项目

pipeline {
  agent any
  stages {
    stage('拉取代码') {
      steps {
        checkout([$class: 'GitSCM', branches: [[name: '*/master']],
	             doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [],
				 userRemoteConfigs: [[credentialsId: '8c8a0774-7d4c-48a9-8f52-dd1a04f71320', 
				 url:'https://192.168.137.129/ops/web_demo.git']]])
      }
    }
    stage('编译构建') {
      steps {
        sh label: '', script: 'mvn clean package'
      }
    }
	stage('项目部署') {
      steps {
        deploy adapters: [tomcat8(credentialsId: 'ee47b279-bc0f-47b7-b9e7-05f2a2a88ecf', path: '', url: 'http://192.168.137.130:8081')], 
		contextPath: null,
        war: 'target/*.war'
      }
    }
  }
}

语法中配置的凭据ID(credentialsId)都可以在凭据那里获取到

Jenkins Pipeline流水线_上传_09

5 Pipeline实现CI/CD

代码拉取->代码质检->Maven构建->代码上传Nexus->消息通知(通知质量检测结果)

整段代码
pipeline {
  agent any //在哪个节点运行
  //流水线
  stages {
    stage('下载代码') {
      steps {
        checkout([$class: 'GitSCM', branches: [[name: '*/master']],
	               doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [],
				         userRemoteConfigs: [[credentialsId: '8c8a0774-7d4c-48a9-8f52-dd1a04f71320', 
				         url:'https://192.168.137.129/ops/web_demo.git']]])
      }
    }
    stage('检查代码') {
      steps {
        script {
          scannerHome = tool 'sonar-scanner'
        }
        withSonarQubeEnv('SonarQube') {
          sh "${scannerHome}/bin/sonar-scanner"
        }
      }
    }
    stage('健康状态') {
      steps {
	    sh 'sleep 20'
        script {
          timeout(5) { //sonar webhook通知pipeline代码检测结果,未通过pipeline将会fail
            def qg = waitForQualityGate()
            echo "gg"
            if (qg.status != 'OK') {
              error "Sonarqube代码检查失败,failure: ${qg.status}"
            }
          }
        }
      }
    }
    stage('编译代码') {
      steps {
        sh 'mvn clean package -Dmaven.test.skip=true'
      }
    }
    stage('上传到制品库') {
      steps {
        sh 'sh -vx /app/module/script/push_war.sh'
      }
    }
    stage('获取质检脚本'){
	    steps {
	      sh 'sh -x /app/dingding.sh ${JOB_BASE_NAME}'
	      script {
	        env.BUG = sh(returnStdout: true, script: "grep 'BUG' build | awk -F '=' '{print \$2}'").trim()
	        env.VULNERABILITY = sh(returnStdout: true, script: "grep 'VULNERABILITY' build | awk -F '=' '{print \$2}'").trim()
	        env.CODE_SMELL = sh(returnStdout: true, script: "grep 'CODE_SMELL' build | awk -F '=' '{print \$2}'").trim()
	        env.CRITICAL = sh(returnStdout: true, script: "grep 'CRITICAL' build | awk -F '=' '{print \$2}'").trim()
	      }
	    }
    }
    stage('钉钉通知') {
      steps {
        echo '消息...'
      }
      post {
        success {
          dingtalk (
            robot: 'ad4dea91-8d38-467b-95c2-e1b2605dcac8',
            type: 'MARKDOWN',
            title: '${JOB_BASE_NAME} 构建成功',
            text: [
              '# ${JOB_BASE_NAME} 构建成功',
              '- 任务:[${BUILD_DISPLAY_NAME}](${BUILD_URL})',
              '- ${BUG}',
              '- ${VULNERABILITY}',
              '- ${CODE_SMELL}',
              '- ${CRITICAL}'
            ],
            at: [
              'all'
            ]
          )
        }
        failure{
          dingtalk(
            robot: 'ad4dea91-8d38-467b-95c2-e1b2605dcac8',
            type:'MARKDOWN',
            title: "failure: ${JOB_BASE_NAME}",
            text: [
              '# ${JOB_BASE_NAME} 构建失败',
              '构建分支:${BRANCH_NAME}',
              '项目地址 ${JOB_URL}',
              '- ${BUG}',
              '- ${VULNERABILITY}',
              '- ${CODE_SMELL}',
              '- ${CRITICAL}'
			      ],
            at: [
              'all'
            ]
          )
        }
      }
    }
  }
}

5.1 下载代码

stage('下载代码') {
      steps {
        checkout([$class: 'GitSCM', branches: [[name: '*/master']],
	               doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [],
				         userRemoteConfigs: [[credentialsId: '8c8a0774-7d4c-48a9-8f52-dd1a04f71320', 
				         url:'https://192.168.137.129/ops/web_demo.git']]])
      }
    }

credentialsId 配置jenkins的凭据ID(gitlab-auth-passwd))

5.2 检查代码

stage('检查代码') {
      steps {
        script {
          scannerHome = tool 'sonar-scanner'
        }
        withSonarQubeEnv('SonarQube') {
          sh "${scannerHome}/bin/sonar-scanner"
        }
      }
    }
sonar-scanner是在系统管理-全局工具配置-SonarQube Scanner配置的Name
SonarQube是在系统管理-系统配置-SonarQube servers配置的Name

为啥不写sonar推送的参数
在项目根目录下面写了个sonar-project.properties文件
# must be unique in a given SonarQube instance
sonar.projectKey=web_demo_pipline
# this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
sonar.projectName=web_demo_pipline
sonar.projectVersion=1.0
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.# This property is optional if sonar.modules is set.
sonar.sources=.
sonar.exclusions=**/test/**,**/target/**
sonar.java.source=1.8
sonar.java.target=1.8
# Encoding of the source code. Default is default system encoding
sonar.sourceEncoding=UTF-8

5.3 健康状态

stage('健康状态') {
      steps {
	    sh 'sleep 20'
        script {
          timeout(5) { //sonar webhook通知pipeline代码检测结果,未通过pipeline将会fail
            def qg = waitForQualityGate()
            echo "gg"
            if (qg.status != 'OK') {
              error "Sonarqube代码检查失败,failure: ${qg.status}"
            }
          }
        }
      }
    }

一定要sh 'sleep 20',他扫描完需要时间汇总,如果不sleep,会一直卡着

Jenkins Pipeline流水线_Jenkins_10

配置sonarqube web回调接口

配置sonarqube web回调接口,将扫描的结果通知jenkins服务器,实际测试即使不配置回调接口,健康状态也能获取到

Name: jenkins

URL: http://admin:123456@192.168.137.130:8080/sonarqube-webhook

Jenkins Pipeline流水线_上传_11

5.4 编译代码

stage('编译代码') {
      steps {
        sh 'mvn clean package -Dmaven.test.skip=true'
      }
    }

5.5 上传到制品库

stage('上传到制品库') {
      steps {
        sh 'sh -vx /app/module/script/push_war.sh'
      }
    }

上传脚本
cat /app/module/script/push_war.sh
#!/bin/bash
#通过pom获取信息
groupId=$(mvn help:evaluate -Dexpression=project.groupId | egrep -v "[INFO]")
artifactId=$(mvn help:evaluate -Dexpression=project.artifactId | egrep -v "[INFO]")
version=$(mvn help:evaluate -Dexpression=project.version | egrep -v "[INFO]")
packaging=$(mvn help:evaluate -Dexpression=project.packaging | egrep -v "[INFO]")
#上传至nexus
mvn deploy:deploy-file \
-DgroupId=${groupId} \
-DartifactId=${artifactId} \
-Dversion=${version} \
-Dpackaging=${packaging} \
-Dfile=target/${artifactId}-${version}.${packaging} \
-Durl=http://192.168.137.130/repository/maven-releases/ \
-DrepositoryId=maven-releases \
-DgeneratePom=true

5.6 获取质检脚本

stage('获取质检脚本'){
	    steps {
	      sh 'sh -x /app/dingding.sh ${JOB_BASE_NAME}'
	      script {
	        env.BUG = sh(returnStdout: true, script: "grep 'BUG' build | awk -F '=' '{print \$2}'").trim()
	        env.VULNERABILITY = sh(returnStdout: true, script: "grep 'VULNERABILITY' build | awk -F '=' '{print \$2}'").trim()
	        env.CODE_SMELL = sh(returnStdout: true, script: "grep 'CODE_SMELL' build | awk -F '=' '{print \$2}'").trim()
	        env.CRITICAL = sh(returnStdout: true, script: "grep 'CRITICAL' build | awk -F '=' '{print \$2}'").trim()
	      }
	    }
    }
 
 #脚本在集成钉钉里面有

5.7 钉钉通知

stage('钉钉通知') {
      steps {
        echo '消息...'
      }
      post {
        success {
          dingtalk (
            robot: 'ad4dea91-8d38-467b-95c2-e1b2605dcac8',
            type: 'MARKDOWN',
            title: '${JOB_BASE_NAME} 构建成功',
            text: [
              '# ${JOB_BASE_NAME} 构建成功',
              '- 任务:[${BUILD_DISPLAY_NAME}](${BUILD_URL})',
              '- ${BUG}',
              '- ${VULNERABILITY}',
              '- ${CODE_SMELL}',
              '- ${CRITICAL}'
            ],
            at: [
              'all'
            ]
          )
        }
        failure{
          dingtalk(
            robot: 'ad4dea91-8d38-467b-95c2-e1b2605dcac8',
            type:'MARKDOWN',
            title: "failure: ${JOB_BASE_NAME}",
            text: [
              '# ${JOB_BASE_NAME} 构建失败',
              '构建分支:${BRANCH_NAME}',
              '项目地址 ${JOB_URL}',
              '- ${BUG}',
              '- ${VULNERABILITY}',
              '- ${CODE_SMELL}',
              '- ${CRITICAL}'
			      ],
            at: [
              'all'
            ]
          )
        }
      }
    }

robot配置的是系统管理里面钉钉配置以后自动生成的

Jenkins Pipeline流水线_Jenkins_12

5.8 Pipiline执行结果

Jenkins Pipeline流水线_Jenkins_13

5.9 钉钉消息

Jenkins Pipeline流水线_上传_14

6 附录邮箱配置

pipeline {
  post { 
    always { 
      emailext( 
        subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!', 
        body: '${FILE,path="email.html"}',  
      ) 
    } 
  } 
} 

#他会找项目根目录下的email.html
email.html内容
<!DOCTYPE html>    
<html>    
<head>    
<meta charset="UTF-8">    
<title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>    
</head>    
    
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4"    
    offset="0">    
    <table width="95%" cellpadding="0" cellspacing="0"  style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">    
        <tr>    
            本邮件由系统自动发出,无需回复!<br/>            
            各位同事,大家好,以下为${PROJECT_NAME }项目构建信息</br> 
            <td><font color="#CC0000">构建结果 - ${BUILD_STATUS}</font></td>   
        </tr>    
        <tr>    
            <td><br />    
            <b><font color="#0B610B">构建信息</font></b>    
            <hr size="2" width="100%" align="center" /></td>    
        </tr>    
        <tr>    
            <td>    
                <ul>    
                    <li>项目名称 : ${PROJECT_NAME}</li>    
                    <li>构建编号 : 第${BUILD_NUMBER}次构建</li>    
                    <li>触发原因: ${CAUSE}</li>    
                    <li>构建状态: ${BUILD_STATUS}</li>    
                    <li>构建日志: <a href="${BUILD_URL}console">${BUILD_URL}console</a></li>    
                    <li>构建  Url : <a href="${BUILD_URL}">${BUILD_URL}</a></li>    
                    <li>工作目录 : <a href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li>    
                    <li>项目  Url : <a href="${PROJECT_URL}">${PROJECT_URL}</a></li>    
                </ul>    

<h4><font color="#0B610B">失败用例</font></h4>
<hr size="2" width="100%" />
$FAILED_TESTS<br/>

<h4><font color="#0B610B">最近提交(#$SVN_REVISION)</font></h4>
<hr size="2" width="100%" />
<ul>
${CHANGES_SINCE_LAST_SUCCESS, reverse=true, format="%c", changesFormat="<li>%d [%a] %m</li>"}
</ul>
详细提交: <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a><br/>

            </td>    
        </tr>    
    </table>    
</body>    
</html>