首页 > 其他分享 >Jenkins Pipeline

Jenkins Pipeline

时间:2024-09-18 16:03:56浏览次数:1  
标签:Pipeline NAME echo JOB BASE 构建 Jenkins stage

Jenkins有一下几种模式:

  • 一般初次接触Jenkins建议使用自由风格(freestyle),可视化的UI页面配合一些Jenkins插件再写一些简单的shell/bat命令即可实现从源码构建到项目部署的整个步骤。
  • 当你对Jenkins熟悉度达到一定级别后可以尝试将自由风格的转变成Jenkins的pipeline语法编写 Jenkinsfile 形成构建模版共同类型的项目使用。

至今我们已经完成Jenkins的基础部分介绍,这里我们就开始新的篇章学习Jenkins的Pipeline。

Pipeline向Jenkins中添加了一组强大的工具, Pipeline在代码中实现的,通常会检查到源代码控制, 使团队有编辑, 审查和迭代他们的交付流水线的能力。

Jenkins Pipeline是一组插件,支持在Jenkins上实现和集成持续交付的管道。Pipeline这个单词是水管的意思。

Jenkins为了更好支持CI和CD,通过Groovy语言的DSL(动态描述语言)来开发Pipeline组件。在Jenkins中有一句话,Pipeline as code,Pipeline是Jenkins中最优雅的存在。之前Jenkins上UI操作动作,都可以在Pipeline中代码实现,主要你对Jenkins和Groovy语言有足够多掌握。

pipline介绍

使用要求

为了使用 Jenkins Pipeline,你需要:

  • Jenkins 2.x 或以上版本(旧版本到 1.642.3 可能可以,但不建议)
  • 安装Pipeline 插件

概念

  • 流水线pipeline: 是用户定义的一个CD流水线模型 。流水线的代码定义了整个的构建过程, 他通常包括构建, 测试和交付应用程序的阶段 。
  • 节点node: 是一个机器 ,它是Jenkins环境的一部分,它主要负责执行流水线,也就是我们常说的masterslave 的node节点
  • 阶段stage: 我们可以将构建的整个过程拆分很多阶段stage, 每个阶段执行不同的任务比如 "执行代码检出", "执行mvn构建", "执行部署" 和 " 执行制品收集" 等阶段。
  • 步骤step: 一个单一的任务, 每个任务都有自己的工作内容,比如我们在"执行mvn构建" 需要实行shell脚本 mvn clean install ,步骤必须包含在一个阶段内。

声明式和脚本化

在开始说声明式和脚本化的区别前我们先聊一个话题声明式编程模型命令式编程模型,这两者都是为了编程而存在的,只不过声明时编程模型更倾向于 I want to do , 而命令式编程 表现于 you must to do , 两个模型的特点是:

  • 声明式 就是告诉系统我想做什么,声明式编程限制用户使用更严格和预定义的结构,编程维度更高,实现功能更方便
  • 命令式 则是一板一眼,你让程序干什么,程序就按照你的指令执行,编程维度更低,能够实现更复杂的需求

jenkins的pipeline也根据这两个模型创建出两种语法格式:声明式语法和脚本化语法

Pipeline中的两种语法格式:声明式和脚本化,这两种语法格式在书写时是不同的。

  • 声明式相比脚本化的流水线语法,它提供更丰富的语法特性, 不需要Groovy语言功底支撑
  • 脚本化是为了使编写和读取流水线代码更容易而设计的。

两种的根本差距是:

  • 声明式是通过pipeline和jenkins的一系列插件提供的方法和函数进行编写pipeline code,Jenkins 会将声明式 Pipeline 转换为 Groovy 脚本,然后执行。
  • 脚本式 Pipeline 直接使用 Groovy 语法,因此更灵活和强大。用户可以直接编写 Groovy 代码来实现复杂的逻辑。

然而,在实际的应用场景中,我们通常都是声明式和脚本化相结合的流水线。

比较两种语法的不同:

// 声明时语法
pipeline {
    agent any 
    stages {
        stage('Build') { 
            steps {
                /* balabala小魔仙 */
            }
        }
    }
}
// 脚本时语法
node {  
    stage('Build') { 
        /* balabala小魔仙 */
    }
}

基础架构和语法

接下来我们看下一个最基本的pipeline语法, jenkins这边也提供了一个简单的pipeline示例

pipeline {
    agent any
    stages {
        stage('Hello') {
            steps {
                echo 'Hello World'
            }
        }
    }
}
  • pipeline 是声明式流水线的一种特定语法,他定义了包含执行整个流水线的所有内容和指令的 "block" 代码块。
  • agent是声明式流水线的一种特定语法,它指示 Jenkins 为整个流水线分配一个执行器 (在节点上)和工作区。
    • node 是脚本化流水线的一种特定语法,它指示 Jenkins 在任何可用的代理/节点上执行流水线 (和包含在其中的任何阶段)这实际上等效于 声明式流水线特定语法的agent
    • 如果期望在标签为 java的 机器上运行构建,两种表示方式为:
      • agent { label 'java' }
      • node('java') { }
  • stage 是一个描述 stages 的语法块。一个stages** 可以包含多个stage , 但是每个stage 后面的名字必须唯一**
  • steps 是声明式流水线的一种特定语法,它描述了在这个 stage 中要运行的步骤。注意看哦steps是个复数,所以每个stage 只能包含一个steps
  • echo 是一个执行打印命令的流水线 。是声明时语法内提供的函数

语法结构

agent: 节点
stage: 阶段
steps: 动作

  • 声明式流水线所有的代码包裹在pipline{}内,声明当前格式为pipline格式的job类型
  • stages{}层用来包含该pipeline所有的stage子层,每一个stage都是一个模块,相互不受影响
  • stage{}层用来包含具体我们需要编写任务的steps{}子层
  • steps{}层用来添加我们具体需要调用的模块语句

特殊的语法区域

agent 区域

agent 部分指定了整个流水线或特定部分将在 Jenkins 环境中执行的位置,这取决于 agent 区域的位置。在 pipeline 块的顶层必须定义 agent,而在 stage 级别的使用是可选的。
agent 定义了 Pipeline 的执行位置,可以使用 anynone、具体的 Jenkins 节点标签名、甚至 Docker 镜像。例如,我们需要指定在名为 “java” 的服务器上执行,可以写成:

agent { node { label 'java' } }
agent { label 'java' }
agent { docker 'maven:3-alpine' }
  • agent none 表示整个 Pipeline 没有默认的执行节点,因此 Pipeline 内的每个 stage 可以根据需要在其内部指定不同的执行节点,否则 stage 内的代码不会被执行。

environment 区域

environment 指令制定一个 键-值对序列,该序列将被定义为所有步骤的环境变量,或者是特定于阶段的步骤, 这取决于 environment 指令在流水线内的位置。

  • 可以出现在pipeline代码块和stage代码块
    • pipeline块出现表示全局变量
    • stage代码块出现则该环境变量只能生效在各个步骤内
  • 可以和凭据结合
    • 对于账号密码类型的凭据,指定的环境变量指定为 username:password
    • 对于Secret Text的凭证, 将确保指定的环境变量包含秘密文本内容。
    • 更多关于凭据的信息,请查看官网
environment {
    JAVA_HOME = "/usr/local/java8"
}
  • 全局变量:与 agent 平级,这时环境变量应用于所有的 stages 任务。
  • 局部变量:在 stage 内部定义 environment,此时仅在该 stage 内部任务中应用。

parameters 区域

parameters 区域主要为流水线构建添加运行时的参数选项。通过 params 来获取 parameters 设置的属性,params 获取的是 parameters 所有结果返回的 Map

script 区域

script 区域主要在 steps 内定义,允许编写 Groovy 脚本语言,并支持 Groovy 逻辑运算。

  • 条件判断
stage('Conditional Stage') {
    steps {
        script {
            if (env.BRANCH_NAME == 'main') {
                echo 'Deploying to production'
            } else {
                echo 'Deploying to staging'
            }
        }
    }
}

解释:这个示例展示了如何根据当前的分支名决定部署到哪个环境。如果分支名是 main,将打印 Deploying to production,否则打印 Deploying to staging

  • **循环 **
stage('Loop Example') {
    steps {
        script {
            for (int i = 0; i < 5; i++) {
                echo "Iteration ${i}"
            }
        }
    }
}

解释:在这个示例中,使用 for 循环输出从 0 到 4 的数字,每次迭代都会打印 Iteration <数字>

  • **调用外部命令并处理结果 **
stage('Execute Command') {
    steps {
        script {
            def result = sh(script: 'ls -l', returnStdout: true).trim()
            echo "Command output: ${result}"
        }
    }
}

解释:这里我们执行了 ls -l 命令,并将输出保存到 result 变量中。然后通过 echo 打印出命令的结果。

  • **自定义函数 **
stage('Custom Function') {
    steps {
        script {
            def greet = { name -> return "Hello, ${name}" }
            def message = greet('Jenkins')
            echo message
        }
    }
}

解释:这个示例定义了一个名为 greet 的自定义函数,该函数接受一个参数并返回一个问候语。然后调用该函数并输出问候语。

关键字介绍

pipeline  声明pipline语法
node      是脚本化流水线的一种特定语法,它指示 Jenkins 在任何可用的代理/节点上执行流水线 (和包含在其中的任何阶段)这实际上等效于 声明式流水线特定语法的`agent`。
agent   指示 Jenkins 为整个流水线分配一个执行器(在 Jenkins 环境中的任何可用代理/节点上)和工作区。
stages  步骤组,包含一个或多个stages 
stage  定义了在整个流水线的执行任务的概念性地不同的的子集 
step   步骤,任务。
parameters   为流水线定义构建参数
params   将为流水线定义的所有参数作为 Map,例如:params.MY_PARAM_NAME。
env     可以从脚本式流水线中访问的环境变量(jenkins的环境变量),例如: env.PATH 或 env.BUILD_ID 
currentBuild  可用于发现当前正在执行的流水线的信息, 比如 currentBuild.result,currentBuild.displayName 等属性。参考内置的全局变量参考页面 ${YOUR_JENKINS_URL}/pipeline-syntax/globals 以获取完整的,最新的,currentBuild 的属性列表。

when

when 指令允许流水线根据给定的条件决定是否应该执行阶段。 when 指令必须包含至少一个条件。 如果 when 指令包含多个条件, 所有的子条件必须返回True,阶段才能执行。 这与子条件在 allOf 条件下嵌套的情况相同

  • 只能出现在stage 代码块
  • when有以下几个参数:
    • branch: 当正在构建的分支与模式给定的分支匹配时,执行这个阶段, 例如: when { branch 'master' }。注意,这只适用于多分支流水线。
    • environment: 当指定的环境变量是给定的值时,执行这个步骤, 例如: when { environment name: 'DEPLOY_TO', value: 'production' }
    • expression: 当指定的Groovy表达式评估为true时,执行这个阶段, 例如: when { expression { return params.DEBUG_BUILD } }
    • not: 当嵌套条件是错误时,执行这个阶段,必须包含一个条件,例如: when { not { branch 'master' } }
    • allOf: 当所有的嵌套条件都正确时,执行这个阶段,必须包含至少一个条件,例如: when { allOf { branch 'master'; environment name: 'DEPLOY_TO', value: 'production' } }
    • anyOf: 当至少有一个嵌套条件为真时,执行这个阶段,必须包含至少一个条件,例如: when { anyOf { branch 'master'; branch 'staging' } }
  • 在进入 stage 前评估 when,当满足when的条件后才会进入该 stage
  • stage 内指定agent 时,如果设置了 when 则会先进入agent环境后在评估条件,除非你设置 whenbeforeAgent 属性为 true
pipeline {
    agent none
    stages {
        stage('Example Deploy') {
            agent {
                label "java"
            }
            when {
                beforeAgent true
                branch 'master'
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

parallel

声明式流水线的parallel 块内可以包含多个stage ,这表示它们将并行执行。 但需要注意哦:一个阶段必须只有一个 stepsparallel 的阶段。 嵌套阶段本身不能包含进一步的 parallel 阶段。任何包含 parallel 的阶段(下面案例的stage('Parallel Stage')阶段)不能包含 agenttools 阶段, 因为他们没有相关 steps

通过添加 failFast true 到包含 parallelstage 中, 当其中一个进程失败时,你可以强制所有的 parallel 阶段都被终止。

pipeline {
    agent any
    stages {
        stage('Non-Parallel Stage') {
            steps {
                echo 'This stage will be executed first.'
            }
        }
        stage('Parallel Stage') {
            when {
                branch 'master'
            }
            failFast true
            parallel {
                stage('Branch A') {
                    agent {
                        label "for-branch-a"
                    }
                    steps {
                        echo "On Branch A"
                    }
                }
                stage('Branch B') {
                    agent {
                        label "for-branch-b"
                    }
                    steps {
                        echo "On Branch B"
                    }
                }
            }
        }
    }
}

流程控制

if语句

脚本化流水线Jenkinsfile 的顶部开始向下串行执行, 就像 Groovy 或其他语言中的大多数传统脚本一样。 因此,提供流控制取决于 Groovy 表达式, 比如 if/else 条件, 例如:

node {
    stage('Example') {
        if (env.BRANCH_NAME == 'master') {
            echo 'I only execute on the master branch'
        } else {
            echo 'I execute elsewhere'
        }
    }
}

如果是声明时语法则需要使用script 进行包裹强调这是一段脚本语法:

pipeline {
    agent any
    stages {
        stage('Example') {
            script {
                if (env.BRANCH_NAME == 'master') {
                    echo 'I only execute on the master branch'
                } else {
                    echo 'I execute elsewhere'
                }
            }

        }
    }
}

捕获异常

脚本化流水线语法使用Groovy的异常处理支持来管理脚本化流水线流控制。当步骤失败 ,无论什么原因,它们都会抛出一个异常。处理错误的行为必须使用Groovy中的 try/catch/finally 块 , 例如:

node {
    stage('Example') {
        try {
            sh 'exit 1'
        }
        catch (exc) {
            echo 'Something failed, I should sound the klaxons!'
            throw
        }
    }
}

常用函数

sh

在 Unix/Linux 环境中执行 Shell 命令

sh 'echo Hello from Unix/Linux'

echo

输出消息到 Jenkins 控制台。

echo 'This is a message'

parallel

并行执行多个步骤。

parallel(
    stage('xxx') {
        steps {
            echo 'Running step xxx'
        }
    }
    stage('yyy') {
        steps {
            echo 'Running step yyy'
        }
    }
)

input

等待用户输入

input 'Do you want to proceed?'

timeout

设置步骤的超时时间

timeout(time: 10, unit: 'MINUTES') {
    sh 'long-running-command'
}

retry

重试指定次数的步骤

retry(3) {
    sh 'unstable-command'
}

archiveArtifacts

保存构建产生的工件

archiveArtifacts artifacts: '**/target/*.jar', allowEmptyArchive: true

currentBuild

currentBuild 是 Jenkins Pipeline 中用于访问和操作当前构建(build)状态的变量。通过 currentBuild,你可以获取构建的各种信息,设置构建结果,添加描述等。

常见用法

  • 获取当前构建的基本信息
  • 设置当前构建的结果
  • 添加构建描述
  • 访问构建的环境变量
属性和方法
  • currentBuild.number: 获取当前构建的编号。
  • currentBuild.url: 获取当前构建的 URL。
  • currentBuild.durationString: 获取当前构建的持续时间字符串。
  • currentBuild.result : 获取或设置当前构建的结果。
  • currentBuild.displayName: 获取或设置当前构建的显示名称。
  • currentBuild.fullDisplayName: 获取当前构建的完整显示名称。
  • currentBuild.id: 获取当前构建的 ID。
  • currentBuild.startTimeInMillis: 获取当前构建的开始时间(以毫秒为单位)。
  • currentBuild.duration: 获取当前构建的持续时间(以毫秒为单位)。
  • currentBuild.buildCauses: 获取触发当前构建的原因。

Pipeline 常用函数一览

代码检出

  • checkout : 检出代码。
  • git : 使用 Git 检出代码。
  • svn : 使用 Subversion 检出代码。

构建和执行

  • sh : 在 Unix/Linux 系统上执行 Shell 命令。
  • bat : 在 Windows 系统上执行批处理命令。
  • powershell : 在 Windows 系统上执行 PowerShell 脚本。
  • docker : 使用 Docker 进行构建和执行。
  • maven : 使用 Maven 进行构建。

测试

  • junit : 解析 JUnit 测试结果。
  • archiveArtifacts : 归档构建产物。

通知

  • mail : 发送电子邮件。
  • emailext : 发送电子邮件。需要依赖 Email Extension 插件
  • dingtalk: 发送 钉钉 消息。需要依赖 dingtalk 插件
  • wxwork: 发送 微信消息。需要依赖 WXWork Notification 插件

流程控制

  • stage : 定义流水线的一个阶段。
  • node : 指定在哪个节点上执行步骤。
  • parallel : 并行执行多个步骤。
  • timeout : 设置步骤的超时时间。
  • retry : 重试执行步骤。
  • try {...} catch(exc) { } : 捕获并处理错误。
  • error : 触发错误并终止流水线。
  • input : 等待用户输入。
  • sleep : 暂停一段时间。
sleep time: 10, unit: 'SECONDS'

文件操作

  • readFile: 读取文件内容。
    def content = readFile 'example.txt'
    echo "File content: ${content}"
  • writeFile: 写入文件内容。
writeFile file: 'example.txt', text: 'Hello, Jenkins!'
  • stash: 存储文件以供后续步骤使用。
stash name: 'sourceFile', includes: '**'
  • unstash: 恢复之前存储的文件。
unstash 'source'

环境变量

  • withEnv: 在指定的环境变量下执行步骤。
withEnv(['VAR_NAME=value']) {
    // 任务
}
  • env: 访问和设置环境变量。

其他

  • echo: 打印消息。
  • pwd: 获取当前工作目录。
  • dir: 在指定目录下执行步骤。类似Linux的cd 命令
dir('path/to/dir') {
    // 任务
}
  • fileExists: 检查文件是否存在。
  • deleteDir: 删除当前目录及其内容。
  • archive: 归档文件。
  • fingerprint: 生成文件的指纹。
  • timestamps: 为日志添加时间戳。
timestamps {
    // 任务
}
  • wrap: 包装步骤以添加额外的功能(如超时、重试等)。

Pipeline使用方法

什么是 JenkinsFile?

JenkinsFile 是来定义 Jenkins pipeline的文本文件。 使用 JenkinsFile 将流水线实现为代码,并且可以使用域特定语言(DSL)来定义。 JenkinsFile一般有三种方式创建:

  • 使用经典 UI 创建的 Jenkinsfile 由 Jenkins 自己保存(一般保存在/var/lib/jenkins/jobs/${JOB_NAME}/config.xml

复杂的流水线很难在流水线配置页面经典 UI的脚本文本区域进行编写和维护。为简化操作,流水线的 Jenkinsfile 可以在文本编辑器或集成开发环境(IDE)中进行编写并提交到源码管理系统 (可选择性地与需要 Jenkins 构建的应用程序代码放在一起)。然后 Jenkins 从源代码管理系统中检出 Jenkinsfile 文件作为流水线项目构建过程的一部分并接着执行你的流水线。

Pipeline的工作机制

Jenkins Pipeline 底层是基于 Groovy 语言的。Groovy 是一种动态脚本语言,可以与 Java 无缝集成。Jenkins 使用 Groovy 作为其脚本语言,因为它强大且灵活。

jenkins 依赖 Pipeline 插件,Pipeline 插件 将我们书写的Pipeline 代码(无论是声明式还是脚本式) 转化为 groovy 脚本后进行执行

pipeline构建ruoyi项目

书写框架

我们来看看我们在自由风格构建的步骤

1.拉取代码

2.漏洞扫描

2.编译打包

4.推送nexus

5.部署服务

那我们可以按照这个思路进行书写pipeline框架

pipeline {
    agent any
    stages {
        stage('拉取代码') {
            steps {
                echo "拉取代码"
            }
        }
        stage('漏洞扫描') {
            steps {
                echo "漏洞扫描"
            }
        }
        stage('编译打包') {
            steps {
                echo "编译打包"
            }
        }
        stage('推送nexus') {
            steps {
                echo "推送nexus"
            }
        }
        stage('部署服务') {
            steps {
                echo "部署服务"
            }
        }
    }
    post {
        success {
            echo 'Pipeline 执行成功!'
        }
        failure {
            echo 'Pipeline 执行失败,请检查问题。'
        }
    }
}

创建pipeline

这里我们还是以ruoyi-auth为例,为了不和之前的环境ruoyi-auth重名,首先需要创建一个文件夹,命名为测试

在测试文件夹内 新增item -->命令为ruoyi-auth -->创建类型 pipeline -->流水线 --> pipeline Script,这里先编写我们需要做得步骤和具体内容

可以看到执行成功,那么我们再进行一些改造

设置job属性

pipeline可以为每个job设置特定的参数属性,比如是否允许并发构建、保留多少个构建历史、job构建超时时间等等,你是不是突然想起什么,这不就是UI界面内的内容嘛

pipeline通过 **options**

  • buildDiscarder:设置项目构建历史保留多少个。例如: options { buildDiscarder(logRotator(numToKeepStr: '1')) }
  • disableConcurrentBuilds: 是否允许并发构建。例如: options { disableConcurrentBuilds() },禁止单个项目并发构建
  • timeout:设置流水线运行的超时时间, 在此之后,Jenkins将中止流水线。例如: options { timeout(time: 1, unit: 'HOURS') }
  • retry:在失败时, 重新尝试整个流水线的指定次数。 例如: options { retry(3) }
  • timestamps: 显示构建时间。 例如: options { timestamps() }需要依赖 timestamps 插件


设置构建参数

虽然我们在pipeline流程图内没有标记设置构建参数,前面我们在设计ruoyi-auth的时候是有构建参数的,那么我们这边就从构建参数开始设置。

前面已经说过,parameters是设置构建参数的关键字,所有的构建参数必须要写在parameters块内,针对于参数jenkins pipeline和freestyle一样,都有一下构建选项:

  • 字符参数: 在pipeline内使用 string 表示
  • 文本参数:在pipeline内使用 text 表示
  • 选项参数: 在pipeline内使用 choice表示
  • 布尔参数: 在pipeline内使用 booleanParam表示
  • List Git branches : 在pipeline内使用 listGitBranches表示 (需要安装List Git Branches插件)

现在我们需要实现ruoyi-auth freestyle 时的样式

pipeline {
    agent any
    parameters {
        listGitBranches(
            branchFilter: '.*', 
            credentialsId: 'git-root', 
            defaultValue: '', 
            description: '请选择分支或标签', 
            listSize: '5', 
            name: 'BRANCH', 
            quickFilterEnabled: false, 
            remoteURL: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git', 
            selectedValue: 'NONE', 
            sortMode: 'NONE', 
            tagFilter: '*', 
            type: 'PT_BRANCH_TAG'
        )
        choice(
            choices: ['dev', 'qa'], 
            description: '请选择要部署的环境', 
            name: 'PALTFORM'
        )
    }
    stages {
        stage('拉取代码') {
            steps {
                echo "拉取代码"
            }
        }
        stage('编译打包') {
            steps {
                echo "编译打包"
            }
        }
        stage('代码扫描') {
            steps {
                echo "代码扫描"
            }
        }
        stage('部署服务') {
            steps {
                echo "部署服务"
            }
        }
    }

    post {
        success {
            echo 'Pipeline 执行成功!'
        }
        failure {
            echo 'Pipeline 执行失败,请检查问题。'
        }
    }
}

保存后,可以看见,构建按钮还是 立即构建 并不是参数化构建按钮 Build with Parameteres,说明这个时候我们设置的参数还没生效。这是因为在 Jenkins 中,Pipeline 作业的配置通常是从 Jenkinsfile 中读取的。对于参数化构建,Jenkins 需要读取和解析 Jenkinsfile 中的参数配置,并将其应用到作业中。所以需要我们手动触发一下构建。


构建完成就能发现,构建的按钮已经变了对吧,如果还是没有变化,可能是你的pipeline代码没有写对哦

代码检出过程

清理工作区间

deleteDir() 是pipeline内部内置的函数,作用就删除当前工作目录的所有内容,类似shell的rm -rf * 因此请确保你在适当的上下文中使用它,以避免意外删除重要文件。

执行代码检出

在 Jenkins Pipeline 中,gitcheckout 是两种用于检出代码库的不同方式。它们都可以用于从版本控制系统(如 Git)中检出代码,但它们的使用方式和灵活性有所不同。

我们在代码检出阶段,使用的是echo 进行模拟代码检出操作。这里进行真实的代码检出。如果你的代码仓库使用的是公开仓库(不需要账号密码即可拉取代码),那么可以直接使用 git 将代码进行检出。

git branch: BRANCH, url: 'https://gitee.com/kubesre/ruoyi-auth.git'
// 使用公开仓库不需要输入凭据即可拉取代码,如果是私有仓库,可以写成下面的方式'git-root'表示凭据ID
git branch: BRANCH, url: 'https://gitee.com/kubesre/ruoyi-auth.git', credentialsId: 'git-root'
  • git 是pipeline内部内置的函数
  • BRANCH 是获取 listGitBranches 参数的值,当别人写成 env.BRANCH 或者 params.BRANCH 的时候你不要惊讶,jenkins的pipeline插件会将构建参数的name 映射成:
    • groovy内部变量BRANCH
    • params MAP中的元素
    • 以及系统环境变量BRANCH

大多数情况下,我们代码仓库为了保密性都是私有仓库,所以我们需要和freestyle一样需要使用凭据将代码检出。pipeline不仅支持 git 函数检出,还支持checkout 函数检出,checkout 函数可以定制更高级的检出细节,具体内容请参考本篇内的【Pipeline内部函数】-【插件提供的步骤】-【checkout 步骤】

checkout scmGit(branches: [[name: '${BRANCH}']], extensions: [], userRemoteConfigs: [[credentialsId: 'git-root', url: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git']])

目前的全部代码如下:

pipeline {
    agent any
    parameters {
        listGitBranches(
            branchFilter: '.*', 
            credentialsId: 'git-root', 
            defaultValue: '', 
            description: '请选择分支或标签', 
            listSize: '5', 
            name: 'BRANCH', 
            quickFilterEnabled: false, 
            remoteURL: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git', 
            selectedValue: 'NONE', 
            sortMode: 'NONE', 
            tagFilter: '*', 
            type: 'PT_BRANCH_TAG'
        )
        choice(
            choices: ['dev', 'qa'], 
            description: '请选择要部署的环境', 
            name: 'PALTFORM'
        )
    }
    stages {
        stage('拉取代码') {
            steps {
                echo "拉取代码"
                checkout scmGit(
                    branches: [[name: '${BRANCH}']], 
                    extensions: [], 
                    userRemoteConfigs: [[credentialsId: 'git-root', 
                    url: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git']])
            }
        }
        stage('编译打包') {
            steps {
                echo "编译打包"
            }
        }
        stage('代码扫描') {
            steps {
                echo "代码扫描"
            }
        }
        stage('部署服务') {
            steps {
                echo "部署服务"
            }
        }
    }

    post {
        success {
            echo 'Pipeline 执行成功!'
        }
        failure {
            echo 'Pipeline 执行失败,请检查问题。'
        }
    }
}

构建过程

执行shell命令

如果我们在不期望得到命令的返回值于状态码的情况下我们执行shell命令:

sh 'ls -l'

但是这种情况需要注意,当shell命令的退出状态码为非0时,pipeline会触发结束构建流程,所以如果我们需要获取shell的执行结果状态码,可以使用下面返回命令状态码的方式。

我们目前使用的是声明时语法,以下代码使用的是脚本时语法格式,所以以下代码内容需要放到 script {} 块中,表示强调这些代码是脚本时语法格式,让pipeline按照脚本时语法进行转换

返回命令状态码

pipeline {
    ......
    stages {
        stage('代码检出') {
            steps {
                script {
                    def statusCode = sh returnStatus: true, script: 'command-not-found'
                    println statusCode
                    if (statusCode!=0) {
                        env.REASON="命令执行出错!!!"
                    }	
                    println "${env.REASON}"
                }
            }
        }
     ......
}

def 在groovy中表示定义一个变量
println 和echo 意思一样,表示打印一个内容
if () {} 在groovy中表示if语句判断,后面groovy语法会讲

执行结果,可以看到捕获到错误状态码了

当然,我们也可以获取命令执行的返回结果

返回命令输出

def stdoutMsg = sh returnStdout: true, script: 'ls'

小知识:在控制台中,隐藏正在执行的命令

问题描述
在控制台中,当使用 sh 步骤时会打印正在执行的 Shell 命令,如下所示:

...
[Pipeline] script
[Pipeline] {
[Pipeline] sh
+ ls -l -h
total 16K
drwxr-xr-x   2 jenkins jenkins   25 Mar 21 21:00 deploy
drwxr-xr-x   4 jenkins jenkins  192 Mar 21 21:00 docs
drwxr-xr-x 139 jenkins jenkins 8.0K Mar 21 21:00 notebook
-rw-r--r--   1 jenkins jenkins   47 Mar 21 21:00 README.md
drwxr-xr-x   7 jenkins jenkins  211 Mar 21 21:00 scripts
drwxr-xr-x   2 jenkins jenkins   23 Mar 21 21:00 template
[Pipeline] }
[Pipeline] // script}
# 我们希望能够隐藏命令输出,即隐藏 + ls -l -h 输出行。

问题原因: 在 Jenkins 执行 Shell 命令时,默认启用 -x 与 -e 选项,而 -x 选项导致命令打印。

解决办法

# 关闭 -x 选项,即仅使用 -e 选项(以下两种写法皆可):

sh """#!/bin/sh -e
// do some stuff
"""

sh """\
#!/bin/sh -e
// do some stuff
"""
# 还可以使用 set +x 也可以,但是会输出 set +x 命令自身,不够“干净”:

sh """
set +x
// do some stuff
"""

下面我们就是用shell命令在pipeline中执行构建命令吧,如果不想要返回值(状态码和输出结果),可以直接使用 内置函数sh

stage('构建') {
    steps {
        echo '构建'
        sh "
            source  /etc/profile
            mvn clean install 
        "
        echo '检查构建产物是否存在'
        echo '检查产物名称是否符合规范'
        echo '将制品纳入归档'
    }
}

如果你想获取返回值,则必须使用脚本式声明 ,声明时语法不支持 groovy 式赋值:

        stage('编译打包') {
            steps {
                echo "编译打包"
                script {
                    def statusCode = sh returnStatus: true, script: '#!/bin/sh -e\n source  /etc/profile ; mvn clean install '
                    if (statusCode!=0) {
                         error "构建出现错误!!!请开发在本地打包测试后再提交构建"
                    }
                }
                echo '检查构建产物是否存在'
                echo '检查产物名称是否符合规范'
                echo '将制品纳入归档'
            }
        }	

执行结果

前面我们使用 /etc/profile.d/maven.sh 定义 mvn 的环境变量,所以一定要 source /etc/profile,不然会提示找不到mvn 命令

如果你不想通过source /etc/profile 引用环境变量,那么可以在pipeline内使用jenkins已定义的全局工具

使用在jenkins内部已定义的工具

我们在构建过程中可以使用shell命令直接调用一些构建命令,有时候我们也可以调用在jenkins内已经定义的工具,这些工具在jenkins--> 管理jenkins --> tools内定义的我们都可以直接使用。比如我们在jenkins的工具内定义了maven

只需要在pipeline内定义tools即可,如下

pipeline {
    agent any
    tools {
        maven "mvn3.3.3" // mvn3.3.3 就是我们在全局定义的maven工具名称,你可以定义多个,只需要指定名称即可
    }

整体代码如下,发现编译打包部分不需要再去source 环境变量了

        stage('编译打包') {
            steps {
                echo "编译打包"
                script {
                    def statusCode = sh returnStatus: true, script: '#!/bin/sh -e\n mvn clean install '
                    if (statusCode!=0) {
                         error "构建出现错误!!!请开发在本地打包测试后再提交构建"
                    }
                }
                echo '检查构建产物是否存在'
                echo '检查产物名称是否符合规范'
                echo '将制品纳入归档'
            }
        }
pipeline {
    agent any
    tools {
        maven "maven3.3.3"
    }
    parameters {
        listGitBranches(
            branchFilter: '.*', 
            credentialsId: 'git-root', 
            defaultValue: '', 
            description: '请选择分支或标签', 
            listSize: '5', 
            name: 'BRANCH', 
            quickFilterEnabled: false, 
            remoteURL: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git', 
            selectedValue: 'NONE', 
            sortMode: 'NONE', 
            tagFilter: '*', 
            type: 'PT_BRANCH_TAG'
        )
        choice(
            choices: ['dev', 'qa'], 
            description: '请选择要部署的环境', 
            name: 'PALTFORM'
        )
    }
    stages {
        stage('拉取代码') {
            steps {
                echo "拉取代码"
                checkout scmGit(
                    branches: [[name: '${BRANCH}']], 
                    extensions: [], 
                    userRemoteConfigs: [[credentialsId: 'git-root', 
                    url: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git']])
                sh 'pwd && ls -l'
                script {
                    def statusCode = sh returnStatus: true, script: 'command-not-found'
                    println statusCode
                    if (statusCode!=0) {
                        env.REASON="命令执行出错!!!"
                    }	
                    println "${env.REASON}"
                }
            }
        }
        stage('编译打包') {
            steps {
                echo "编译打包"
                script {
                    def statusCode = sh returnStatus: true, script: '#!/bin/sh -e\n mvn clean install '
                    if (statusCode!=0) {
                         error "构建出现错误!!!请开发在本地打包测试后再提交构建"
                    }
                }
                echo '检查构建产物是否存在'
                echo '检查产物名称是否符合规范'
                echo '将制品纳入归档'
            }
        }
        stage('代码扫描') {
            steps {
                echo "代码扫描"
            }
        }
        stage('部署服务') {
            steps {
                echo "部署服务"
            }
        }
    }

    post {
        success {
            echo 'Pipeline 执行成功!'
        }
        failure {
            echo 'Pipeline 执行失败,请检查问题。'
        }
    }
}

检查制品是否存在、纳入制品库

当我们构建完成后,需要检查我们的打包结果,这里可以使用 pipeline 内置函数 fileExists ,它的用法:

我们使用if 判断,当文件存在时将文件纳入制品库,如果不存在或者名称不规范将包构建直接报错

  • 将制品纳入制品库可以使用 archiveArtifacts 函数

注意事项1:使用文件夹内构建的流水线,必须将JOB_NAME 需要变成 JOB_BASE_NAME,生产环境建议大家也常用JOB_BASE_NAME 排除文件夹名的干扰

注意事项2:使用变量时必须用双引号,流水线语法生成的都不带双引号导致识别不到变量

if (fileExists("target/${JOB_BASE_NAME}.jar")){
    // 归档构建产物
    archiveArtifacts artifacts: "target/${JOB_BASE_NAME}.jar", fingerprint: true, followSymlinks: false, onlyIfSuccessful: true
} else {
    error "未找到构建成品target/${JOB_NAME}.jar,请研发检查项目pom.xml的输出路径以及包名是否规范"
}

解释如下

fingerprint: true #记录所有归档成品的指纹
followSymlinks: false #归档时不跟随符号链接
onlyIfSuccessful: true #只有构建成功时归档

整体代码如下

pipeline {
    agent any
    tools {
        maven "maven3.3.3"
    }
    parameters {
        listGitBranches(
            branchFilter: '.*', 
            credentialsId: 'git-root', 
            defaultValue: '', 
            description: '请选择分支或标签', 
            listSize: '5', 
            name: 'BRANCH', 
            quickFilterEnabled: false, 
            remoteURL: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git', 
            selectedValue: 'NONE', 
            sortMode: 'NONE', 
            tagFilter: '*', 
            type: 'PT_BRANCH_TAG'
        )
        choice(
            choices: ['dev', 'qa'], 
            description: '请选择要部署的环境', 
            name: 'PALTFORM'
        )
    }
    stages {
        stage('拉取代码') {
            steps {
                echo "拉取代码"
                checkout scmGit(
                    branches: [[name: '${BRANCH}']], 
                    extensions: [], 
                    userRemoteConfigs: [[credentialsId: 'git-root', 
                    url: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git']])
                script {
                    def statusCode = sh returnStatus: true, script: 'pwd'
                    println statusCode
                    if (statusCode!=0) {
                        env.REASON="命令执行出错!!!"
                    }	
                    println "${env.REASON}"
                }
            }
        }
        stage('编译打包') {
            steps {
                echo "编译打包"
                script {
                    def statusCode = sh returnStatus: true, script: '#!/bin/sh -e\n mvn clean install '
                    if (statusCode!=0) {
                         error "构建出现错误!!!请开发在本地打包测试后再提交构建"
                    }
                }
                echo '检查构建产物是否存在'
                echo '检查产物名称是否符合规范'
                script {
                    if (fileExists("target/${JOB_BASE_NAME}.jar")){
                         // 归档构建产物
                        archiveArtifacts artifacts: "target/${JOB_BASE_NAME}.jar", fingerprint: true, followSymlinks: false, onlyIfSuccessful: true
                    } else {
                        error "未找到构建成品target/${JOB_BASE_NAME}.jar,请研发检查项目pom.xml的输出路径以及包名是否规范"
                    }
                }
                echo '将制品纳入归档'
            }
        }
        stage('代码扫描') {
            steps {
                echo "代码扫描"
            }
        }
        stage('部署服务') {
            steps {
                echo "部署服务"
            }
        }
    }

    post {
        success {
            echo 'Pipeline 执行成功!'
        }
        failure {
            echo 'Pipeline 执行失败,请检查问题。'
        }
    }
}

部署过程

生成启动脚本

使用 sh 函数,生成sh脚本

sh '''
tee > start_up.sh <<EOF
#!/bin/bash
declare -A server_port_set
server_port_set=( ["ruoyi-auth"]="8090" ["ruoyi-system"]="8091" ["ruoyi-gateway"]="8080" )
server_port=\\${server_port_set["${JOB_BASE_NAME}"]}

[ -d /data/prog/project/${JOB_BASE_NAME} ] || mkdir -p  /data/prog/project/${JOB_BASE_NAME} 
project_jar_path=/data/prog/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar
update_jar_path=/data/update/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar

[ -f \\${project_jar_path} ] && { 
      echo "检测到旧的jar包,开始替换"
      backup_path=/data/backup/\\$(date +%F-%H-%M-%S)
      mkdir -p \\${backup_path}
      /bin/cp  -f \\${project_jar_path} \\${backup_path}/
      /bin/cp  -f \\${update_jar_path} \\${project_jar_path}
} || {
      /bin/cp  -f \\${update_jar_path} \\${project_jar_path}
}

jps |  grep ${JOB_BASE_NAME}.jar && {
    kill -9  \\$(ps -ef | grep ${JOB_BASE_NAME}.jar | grep -v grep  |awk '{print \\$2}')
}

[ -d /data/logs/project/${JOB_BASE_NAME} ]  || mkdir -p /data/logs/project/${JOB_BASE_NAME}/
cd /data/prog/project/${JOB_BASE_NAME}/
nohup /data/prog/jdk1.8.0_271/bin/java \
-server -Xms500m -Xmx500m  \
-jar /data/prog/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar \
--server.port=\\${server_port} --spring.profiles.active=dev \
--spring.cloud.nacos.config.server-addr=192.168.1.91:8848 \
--spring.cloud.nacos.config.username=nacos \
--spring.cloud.nacos.config.password=nacos \
--spring.cloud.nacos.discovery.server-addr=192.168.1.91:8848 \
--spring.cloud.nacos.discovery.username=nacos \
--spring.cloud.nacos.discovery.password=nacos >> /data/logs/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}-console.log 2>&1 & 
EOF
'''

通过Over SSH部署

这里我们还是自由风格的使用over ssh配置

通过流水线语法生成配置之后去掉一些默认配置

sshPublisher

                sshPublisher(publishers: [
                    sshPublisherDesc(
                        configName: 'ruoyi-server',
                        transfers: [
                            sshTransfer(
                                remoteDirectory: '/data/update/${JOB_BASE_NAME}/',
                                removePrefix: 'target/',
                                sourceFiles: 'target/${JOB_BASE_NAME}.jar'
                            ),
                            sshTransfer(
                                execCommand: 'cd /data/prog/project/${JOB_BASE_NAME}/ ; sh bin/start_up.sh',
                                remoteDirectory: '/data/prog/project/${JOB_BASE_NAME}/bin',
                                sourceFiles: 'start_up.sh'
                            )
                        ]
                    )
                ])

完整代码如下

pipeline {
    agent any
    tools {
        maven "maven3.3.3"
    }
    parameters {
        listGitBranches(
            branchFilter: '.*', 
            credentialsId: 'git-root', 
            defaultValue: '', 
            description: '请选择分支或标签', 
            listSize: '5', 
            name: 'BRANCH', 
            quickFilterEnabled: false, 
            remoteURL: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git', 
            selectedValue: 'NONE', 
            sortMode: 'NONE', 
            tagFilter: '*', 
            type: 'PT_BRANCH_TAG'
        )
        choice(
            choices: ['dev', 'qa'], 
            description: '请选择要部署的环境', 
            name: 'PALTFORM'
        )
    }
    stages {
        stage('拉取代码') {
            steps {
                echo "拉取代码"
                checkout scmGit(
                    branches: [[name: '${BRANCH}']], 
                    extensions: [], 
                    userRemoteConfigs: [[credentialsId: 'git-root', 
                    url: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git']])
                script {
                    def statusCode = sh returnStatus: true, script: 'pwd'
                    println statusCode
                    if (statusCode != 0) {
                        env.REASON = "命令执行出错!!!"
                    }	
                    println "${env.REASON}"
                }
            }
        }
        stage('编译打包') {
            steps {
                echo "编译打包"
                script {
                    def statusCode = sh returnStatus: true, script: '#!/bin/sh -e\n mvn clean install'
                    if (statusCode != 0) {
                         error "构建出现错误!!!请开发在本地打包测试后再提交构建"
                    }
                }
                echo '检查构建产物是否存在'
                echo '检查产物名称是否符合规范'
                script {
                    if (fileExists("target/${JOB_BASE_NAME}.jar")) {
                        // 归档构建产物
                        archiveArtifacts artifacts: "target/${JOB_BASE_NAME}.jar", fingerprint: true, followSymlinks: false, onlyIfSuccessful: true
                    } else {
                        error "未找到构建成品target/${JOB_BASE_NAME}.jar,请研发检查项目pom.xml的输出路径以及包名是否规范"
                    }
                }
                echo '将制品纳入归档'
            }
        }
        stage('代码扫描') {
            steps {
                echo "代码扫描"
            }
        }
        stage('部署服务') {
            steps {
                echo "部署服务"
                sh '''
tee > start_up.sh <<EOF
#!/bin/bash
declare -A server_port_set
server_port_set=( ["ruoyi-auth"]="8090" ["ruoyi-system"]="8091" ["ruoyi-gateway"]="8080" )
server_port=\\${server_port_set["${JOB_BASE_NAME}"]}

[ -d /data/prog/project/${JOB_BASE_NAME} ] || mkdir -p /data/prog/project/${JOB_BASE_NAME} 
project_jar_path=/data/prog/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar
update_jar_path=/data/update/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar

[ -f \\${project_jar_path} ] && { 
      echo "检测到旧的jar包,开始替换"
      backup_path=/data/backup/\\$(date +%F-%H-%M-%S)
      mkdir -p \\${backup_path}
      /bin/cp -f \\${project_jar_path} \\${backup_path}/
      /bin/cp -f \\${update_jar_path} \\${project_jar_path}
} || {
      /bin/cp -f \\${update_jar_path} \\${project_jar_path}
}

jps | grep ${JOB_BASE_NAME}.jar && {
    kill -9 \\$(ps -ef | grep ${JOB_BASE_NAME}.jar | grep -v grep | awk '{print \\$2}')
}

[ -d /data/logs/project/${JOB_BASE_NAME} ] || mkdir -p /data/logs/project/${JOB_BASE_NAME}/
cd /data/prog/project/${JOB_BASE_NAME}/
nohup /data/prog/jdk1.8.0_271/bin/java \
-server -Xms500m -Xmx500m \
-jar /data/prog/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar \
--server.port=\\${server_port} --spring.profiles.active=dev \
--spring.cloud.nacos.config.server-addr=192.168.1.91:8848 \
--spring.cloud.nacos.config.username=nacos \
--spring.cloud.nacos.config.password=nacos \
--spring.cloud.nacos.discovery.server-addr=192.168.1.91:8848 \
--spring.cloud.nacos.discovery.username=nacos \
--spring.cloud.nacos.discovery.password=nacos >> /data/logs/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}-console.log 2>&1 & 
EOF
'''
                sshPublisher(publishers: [
                    sshPublisherDesc(
                        configName: 'ruoyi-server',
                        transfers: [
                            sshTransfer(
                                remoteDirectory: '/data/update/${JOB_BASE_NAME}/',
                                removePrefix: 'target/',
                                sourceFiles: 'target/${JOB_BASE_NAME}.jar'
                            ),
                            sshTransfer(
                                execCommand: 'cd /data/prog/project/${JOB_BASE_NAME}/ ; sh bin/start_up.sh',
                                remoteDirectory: '/data/prog/project/${JOB_BASE_NAME}/bin',
                                sourceFiles: 'start_up.sh'
                            )
                        ]
                    )
                ])
            }
        }
    }

    post {
        success {
            echo 'Pipeline 执行成功!'
        }
        failure {
            echo 'Pipeline 执行失败,请检查问题。'
        }
    }
}

通过ansible部署

ansible 配合 sh 模块实现传包、启动

                script {
                    // 运行 Ansible 命令,并捕获退出码
                    def exitCode = sh(script: """
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m shell -a "mkdir -p /data/update/${JOB_BASE_NAME}"
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m copy -a "src=target/${JOB_BASE_NAME}.jar dest=/data/update/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar"
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m script -a "start_up.sh"
                    """, returnStatus: true)

                    // 根据退出码进行处理
                    if (exitCode != 0) {
                        echo "Ansible 部署出现异常,请运维人员检查~!"
                        // 你可以在这里添加更多的错误处理逻辑
                        currentBuild.result = 'FAILURE'
                    } else {
                        echo "部署成功."
                    }       
                }

完整如下

pipeline {
    agent any
    tools {
        maven "maven3.3.3"
    }
    parameters {
        listGitBranches(
            branchFilter: '.*', 
            credentialsId: 'git-root', 
            defaultValue: '', 
            description: '请选择分支或标签', 
            listSize: '5', 
            name: 'BRANCH', 
            quickFilterEnabled: false, 
            remoteURL: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git', 
            selectedValue: 'NONE', 
            sortMode: 'NONE', 
            tagFilter: '*', 
            type: 'PT_BRANCH_TAG'
        )
        choice(
            choices: ['dev', 'qa'], 
            description: '请选择要部署的环境', 
            name: 'PALTFORM'
        )
    }
    stages {
        stage('拉取代码') {
            steps {
                echo "拉取代码"
                checkout scmGit(
                    branches: [[name: '${BRANCH}']], 
                    extensions: [], 
                    userRemoteConfigs: [[credentialsId: 'git-root', 
                    url: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git']])
                script {
                    def statusCode = sh returnStatus: true, script: 'pwd'
                    println statusCode
                    if (statusCode != 0) {
                        env.REASON = "命令执行出错!!!"
                    }	
                    println "${env.REASON}"
                }
            }
        }
        stage('编译打包') {
            steps {
                echo "编译打包"
                script {
                    def statusCode = sh returnStatus: true, script: '#!/bin/sh -e\n mvn clean install'
                    if (statusCode != 0) {
                         error "构建出现错误!!!请开发在本地打包测试后再提交构建"
                    }
                }
                echo '检查构建产物是否存在'
                echo '检查产物名称是否符合规范'
                script {
                    if (fileExists("target/${JOB_BASE_NAME}.jar")) {
                        // 归档构建产物
                        archiveArtifacts artifacts: "target/${JOB_BASE_NAME}.jar", fingerprint: true, followSymlinks: false, onlyIfSuccessful: true
                    } else {
                        error "未找到构建成品target/${JOB_BASE_NAME}.jar,请研发检查项目pom.xml的输出路径以及包名是否规范"
                    }
                }
                echo '将制品纳入归档'
            }
        }
        stage('代码扫描') {
            steps {
                echo "代码扫描"
            }
        }
        stage('部署服务') {
            steps {
                echo "部署服务"
                sh '''
tee > start_up.sh <<EOF
#!/bin/bash
declare -A server_port_set
server_port_set=( ["ruoyi-auth"]="8090" ["ruoyi-system"]="8091" ["ruoyi-gateway"]="8080" )
server_port=\\${server_port_set["${JOB_BASE_NAME}"]}

[ -d /data/prog/project/${JOB_BASE_NAME} ] || mkdir -p /data/prog/project/${JOB_BASE_NAME} 
project_jar_path=/data/prog/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar
update_jar_path=/data/update/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar

[ -f \\${project_jar_path} ] && { 
      echo "检测到旧的jar包,开始替换"
      backup_path=/data/backup/\\$(date +%F-%H-%M-%S)
      mkdir -p \\${backup_path}
      /bin/cp -f \\${project_jar_path} \\${backup_path}/
      /bin/cp -f \\${update_jar_path} \\${project_jar_path}
} || {
      /bin/cp -f \\${update_jar_path} \\${project_jar_path}
}

jps | grep ${JOB_BASE_NAME}.jar && {
    kill -9 \\$(ps -ef | grep ${JOB_BASE_NAME}.jar | grep -v grep | awk '{print \\$2}')
}

[ -d /data/logs/project/${JOB_BASE_NAME} ] || mkdir -p /data/logs/project/${JOB_BASE_NAME}/
cd /data/prog/project/${JOB_BASE_NAME}/
nohup /data/prog/jdk1.8.0_271/bin/java \
-server -Xms500m -Xmx500m \
-jar /data/prog/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar \
--server.port=\\${server_port} --spring.profiles.active=dev \
--spring.cloud.nacos.config.server-addr=192.168.1.91:8848 \
--spring.cloud.nacos.config.username=nacos \
--spring.cloud.nacos.config.password=nacos \
--spring.cloud.nacos.discovery.server-addr=192.168.1.91:8848 \
--spring.cloud.nacos.discovery.username=nacos \
--spring.cloud.nacos.discovery.password=nacos >> /data/logs/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}-console.log 2>&1 & 
EOF
'''
                script {
                    // 运行 Ansible 命令,并捕获退出码
                    def exitCode = sh(script: """
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m shell -a "mkdir -p /data/update/${JOB_BASE_NAME}"
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m copy -a "src=target/${JOB_BASE_NAME}.jar dest=/data/update/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar"
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m script -a "start_up.sh"
                    """, returnStatus: true)

                    // 根据退出码进行处理
                    if (exitCode != 0) {
                        echo "Ansible 部署出现异常,请运维人员检查~!"
                        // 你可以在这里添加更多的错误处理逻辑
                        currentBuild.result = 'FAILURE'
                    } else {
                        echo "部署成功."
                    }       
                }
            }
        }
    }

    post {
        success {
            echo 'Pipeline 执行成功!'
        }
        failure {
            echo 'Pipeline 执行失败,请检查问题。'
        }
    }
}

构建历史显示

现在我们就需要将他进行优化,修改构建历史名称和构建历史描述pipeline 给我提供了两种方式:

  • 使用Build Name and Description Setter插件提供的函数修改
buildDescription '这是一个构建描述'
buildName '这是我自主命名的job_name'
  • 使用currentBuild内置方法 修改
currentBuild.displayName = '这是我自主命名的job_name'
currentBuild.description = 'This is a sample build description'

增加stage

        stage('增加构建信息') {
            steps {
                script {
                    // 更新构建名称和描述
                    currentBuild.displayName = "${BUILD_NUMBER}-${JOB_NAME}"
                    currentBuild.description = "提交者: ${BUILD_USER} <br> 提交者ID:${BUILD_USER_ID} <br> 提交时间:${BUILD_TIMESTAMP} <br> 构建分支:${BRANCH}"
                }
                }
            }

完整代码

pipeline {
    agent any
    tools {
        maven "maven3.3.3"
    }
    parameters {
        listGitBranches(
            branchFilter: '.*', 
            credentialsId: 'git-root', 
            defaultValue: '', 
            description: '请选择分支或标签', 
            listSize: '5', 
            name: 'BRANCH', 
            quickFilterEnabled: false, 
            remoteURL: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git', 
            selectedValue: 'NONE', 
            sortMode: 'NONE', 
            tagFilter: '*', 
            type: 'PT_BRANCH_TAG'
        )
        choice(
            choices: ['dev', 'qa'], 
            description: '请选择要部署的环境', 
            name: 'PLATFORM'
        )
    }
    stages {
        stage('拉取代码') {
            steps {
                echo "拉取代码"
                checkout scmGit(
                    branches: [[name: '${BRANCH}']], 
                    extensions: [], 
                    userRemoteConfigs: [[credentialsId: 'git-root', 
                    url: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git']])
                script {
                    def statusCode = sh returnStatus: true, script: 'pwd'
                    println statusCode
                    if (statusCode != 0) {
                        env.REASON = "命令执行出错!!!"
                    }	
                    println "${env.REASON}"
                }
            }
        }
        stage('增加构建信息') {
            steps {
                script {
                    // 更新构建名称和描述
                    currentBuild.displayName = "${BUILD_NUMBER}-${JOB_NAME}"
                    currentBuild.description = "提交者: ${BUILD_USER} <br> 提交者ID:${BUILD_USER_ID} <br> 提交时间:${BUILD_TIMESTAMP} <br> 构建分支:${BRANCH}"
                }
                }
            }
        stage('编译打包') {
            steps {
                echo "编译打包"
                script {
                    def statusCode = sh returnStatus: true, script: '#!/bin/sh -e\n mvn clean install'
                    if (statusCode != 0) {
                         error "构建出现错误!!!请开发在本地打包测试后再提交构建"
                    }
                }
                echo '检查构建产物是否存在'
                echo '检查产物名称是否符合规范'
                script {
                    if (fileExists("target/${JOB_BASE_NAME}.jar")) {
                        // 归档构建产物
                        archiveArtifacts artifacts: "target/${JOB_BASE_NAME}.jar", fingerprint: true, followSymlinks: false, onlyIfSuccessful: true
                    } else {
                        error "未找到构建成品target/${JOB_BASE_NAME}.jar,请研发检查项目pom.xml的输出路径以及包名是否规范"
                    }
                }
                echo '将制品纳入归档'
            }
        }
        stage('代码扫描') {
            steps {
                echo "代码扫描"
            }
        }
        stage('部署服务') {
            steps {
                echo "部署服务"
                sh '''
tee > start_up.sh <<EOF
#!/bin/bash
declare -A server_port_set
server_port_set=( ["ruoyi-auth"]="8090" ["ruoyi-system"]="8091" ["ruoyi-gateway"]="8080" )
server_port=\\${server_port_set["${JOB_BASE_NAME}"]}

[ -d /data/prog/project/${JOB_BASE_NAME} ] || mkdir -p /data/prog/project/${JOB_BASE_NAME} 
project_jar_path=/data/prog/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar
update_jar_path=/data/update/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar

[ -f \\${project_jar_path} ] && { 
      echo "检测到旧的jar包,开始替换"
      backup_path=/data/backup/\\$(date +%F-%H-%M-%S)
      mkdir -p \\${backup_path}
      /bin/cp -f \\${project_jar_path} \\${backup_path}/
      /bin/cp -f \\${update_jar_path} \\${project_jar_path}
} || {
      /bin/cp -f \\${update_jar_path} \\${project_jar_path}
}

jps | grep ${JOB_BASE_NAME}.jar && {
    kill -9 \\$(ps -ef | grep ${JOB_BASE_NAME}.jar | grep -v grep | awk '{print \\$2}')
}

[ -d /data/logs/project/${JOB_BASE_NAME} ] || mkdir -p /data/logs/project/${JOB_BASE_NAME}/
cd /data/prog/project/${JOB_BASE_NAME}/
nohup /data/prog/jdk1.8.0_271/bin/java \
-server -Xms500m -Xmx500m \
-jar /data/prog/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar \
--server.port=\\${server_port} --spring.profiles.active=dev \
--spring.cloud.nacos.config.server-addr=192.168.1.91:8848 \
--spring.cloud.nacos.config.username=nacos \
--spring.cloud.nacos.config.password=nacos \
--spring.cloud.nacos.discovery.server-addr=192.168.1.91:8848 \
--spring.cloud.nacos.discovery.username=nacos \
--spring.cloud.nacos.discovery.password=nacos >> /data/logs/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}-console.log 2>&1 & 
EOF
'''
                script {
                    // 运行 Ansible 命令,并捕获退出码
                    def exitCode = sh(script: """
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m shell -a "mkdir -p /data/update/${JOB_BASE_NAME}"
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m copy -a "src=target/${JOB_BASE_NAME}.jar dest=/data/update/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar"
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m script -a "start_up.sh"
                    """, returnStatus: true)

                    // 根据退出码进行处理
                    if (exitCode != 0) {
                        echo "Ansible 部署出现异常,请运维人员检查~!"
                        // 你可以在这里添加更多的错误处理逻辑
                        currentBuild.result = 'FAILURE'
                    } else {
                        echo "部署成功."
                    }       
                }
            }
        }
    }

    post {
        success {
            echo 'Pipeline 执行成功!'
        }
        failure {
            echo 'Pipeline 执行失败,请检查问题。'
        }
    }
}

钉钉告警

我们之前在freestyle的时候在开始部分设置了钉钉的机器人,可以自定义通知时机

当然在pipeline中也有相关的关键字,它就是post关键字。

post 部分定义一个或多个steps,这些阶段根据流水线或阶段的完成情况而 运行(取决于流水线中 post 部分的位置). post 支持以下 触发时机: always, changed, failure, success, unstable, 和 aborted。这些条件块允许在 post 部分的步骤的执行取决于流水线或阶段的完成状态。

  • always: 无论流水线或阶段的完成状态如何,都允许在 post 部分运行该步骤。
  • changed: 只有当前流水线或阶段的完成状态与它之前的运行不同时,才允许在 post 部分运行该步骤。
  • failure: 只有当前流水线或阶段的完成状态为"failure",才允许在 post 部分运行该步骤, 通常web UI是红色。
  • success: 只有当前流水线或阶段的完成状态为"success",才允许在 post 部分运行该步骤, 通常web UI是蓝色或绿色。
  • unstable: 只有当前流水线或阶段的完成状态为"unstable",才允许在 post 部分运行该步骤, 通常由于测试失败,代码违规等造成。通常web UI是黄色。
  • aborted: 只有当前流水线或阶段的完成状态为"aborted",才允许在 post 部分运行该步骤, 通常由于流水线被手动的aborted。通常web UI是灰色。

post可以出现在pipeline{} 代码块内,也可以出现在stage代码块内。但是对于pipeline语法来说,post 并不是一个必须得代码块,你可以根据你的需要去使用它。

位于pipeline代码块内:

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
    post { 
        always { 
            echo '无论哪种构建情形(成功失败)我都会执行'
        }
    }
}

位于stage代码块内:

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
            post { 
                success { 
                    echo '只有Example步骤运行成功后,我才会执行'
                }
            }
        }
    }
    post { 
        always { 
            echo '无论哪种构建情形(成功失败)我都会执行'
        }
    }
}

添加post

post { 
    always { 
        dingtalk (
            //指定钉钉机器人名称
            robot: "dev",
            type: 'MARKDOWN',
            title: "${JOB_BASE_NAME} 构建${currentBuild.result}",
            text: [
                "## [${JOB_BASE_NAME} 构建${currentBuild.result}提醒](${BUILD_URL}console)",
                "---",
                "- 项目名称:${JOB_BASE_NAME} ",
                "- 构建编号:${BUILD_NUMBER} ",
                "- 构建分支:${BRANCH} ",
                "- 构建人:${BUILD_USER} ",
                "- 构建URL:${BUILD_URL} ",
                "- 构建结果:${currentBuild.result} ",
                "- 构建环境:${env.PLATFORM} ",
                "- 构建持续时间:${currentBuild.duration/1000} "
            ]
        )
    }
}

完整如下

pipeline {
    agent any
    tools {
        maven "maven3.3.3"
    }
    parameters {
        listGitBranches(
            branchFilter: '.*', 
            credentialsId: 'git-root', 
            defaultValue: '', 
            description: '请选择分支或标签', 
            listSize: '5', 
            name: 'BRANCH', 
            quickFilterEnabled: false, 
            remoteURL: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git', 
            selectedValue: 'NONE', 
            sortMode: 'NONE', 
            tagFilter: '*', 
            type: 'PT_BRANCH_TAG'
        )
        choice(
            choices: ['dev', 'qa'], 
            description: '请选择要部署的环境', 
            name: 'PLATFORM'
        )
    }
    stages {
        stage('拉取代码') {
            steps {
                echo "拉取代码"
                checkout scmGit(
                    branches: [[name: '${BRANCH}']], 
                    extensions: [], 
                    userRemoteConfigs: [[credentialsId: 'git-root', 
                    url: 'http://gitlab.jiajia.top/java-ruoyi/ruoyi-auth.git']])
                script {
                    def statusCode = sh returnStatus: true, script: 'pwd'
                    println statusCode
                    if (statusCode != 0) {
                        env.REASON = "命令执行出错!!!"
                    }	
                    println "${env.REASON}"
                }
            }
        }
        stage('增加构建信息') {
            steps {
                script {
                    // 更新构建名称和描述
                    currentBuild.displayName = "${BUILD_NUMBER}-${JOB_NAME}"
                    currentBuild.description = "提交者: ${BUILD_USER} <br> 提交者ID:${BUILD_USER_ID} <br> 提交时间:${BUILD_TIMESTAMP} <br> 构建分支:${BRANCH}"
                }
                }
            }
        stage('编译打包') {
            steps {
                echo "编译打包"
                script {
                    def statusCode = sh returnStatus: true, script: '#!/bin/sh -e\n mvn clean install'
                    if (statusCode != 0) {
                         error "构建出现错误!!!请开发在本地打包测试后再提交构建"
                    }
                }
                echo '检查构建产物是否存在'
                echo '检查产物名称是否符合规范'
                script {
                    if (fileExists("target/${JOB_BASE_NAME}.jar")) {
                        // 归档构建产物
                        archiveArtifacts artifacts: "target/${JOB_BASE_NAME}.jar", fingerprint: true, followSymlinks: false, onlyIfSuccessful: true
                    } else {
                        error "未找到构建成品target/${JOB_BASE_NAME}.jar,请研发检查项目pom.xml的输出路径以及包名是否规范"
                    }
                }
                echo '将制品纳入归档'
            }
        }
        stage('代码扫描') {
            steps {
                echo "代码扫描"
            }
        }
        stage('部署服务') {
            steps {
                echo "部署服务"
                sh '''
tee > start_up.sh <<EOF
#!/bin/bash
declare -A server_port_set
server_port_set=( ["ruoyi-auth"]="8090" ["ruoyi-system"]="8091" ["ruoyi-gateway"]="8080" )
server_port=\\${server_port_set["${JOB_BASE_NAME}"]}

[ -d /data/prog/project/${JOB_BASE_NAME} ] || mkdir -p /data/prog/project/${JOB_BASE_NAME} 
project_jar_path=/data/prog/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar
update_jar_path=/data/update/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar

[ -f \\${project_jar_path} ] && { 
      echo "检测到旧的jar包,开始替换"
      backup_path=/data/backup/\\$(date +%F-%H-%M-%S)
      mkdir -p \\${backup_path}
      /bin/cp -f \\${project_jar_path} \\${backup_path}/
      /bin/cp -f \\${update_jar_path} \\${project_jar_path}
} || {
      /bin/cp -f \\${update_jar_path} \\${project_jar_path}
}

jps | grep ${JOB_BASE_NAME}.jar && {
    kill -9 \\$(ps -ef | grep ${JOB_BASE_NAME}.jar | grep -v grep | awk '{print \\$2}')
}

[ -d /data/logs/project/${JOB_BASE_NAME} ] || mkdir -p /data/logs/project/${JOB_BASE_NAME}/
cd /data/prog/project/${JOB_BASE_NAME}/
nohup /data/prog/jdk1.8.0_271/bin/java \
-server -Xms500m -Xmx500m \
-jar /data/prog/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar \
--server.port=\\${server_port} --spring.profiles.active=dev \
--spring.cloud.nacos.config.server-addr=192.168.1.91:8848 \
--spring.cloud.nacos.config.username=nacos \
--spring.cloud.nacos.config.password=nacos \
--spring.cloud.nacos.discovery.server-addr=192.168.1.91:8848 \
--spring.cloud.nacos.discovery.username=nacos \
--spring.cloud.nacos.discovery.password=nacos >> /data/logs/project/${JOB_BASE_NAME}/${JOB_BASE_NAME}-console.log 2>&1 & 
EOF
'''
                script {
                    // 运行 Ansible 命令,并捕获退出码
                    def exitCode = sh(script: """
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m shell -a "mkdir -p /data/update/${JOB_BASE_NAME}"
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m copy -a "src=target/${JOB_BASE_NAME}.jar dest=/data/update/${JOB_BASE_NAME}/${JOB_BASE_NAME}.jar"
                    ansible ${PLATFORM}-${JOB_BASE_NAME} -m script -a "start_up.sh"
                    """, returnStatus: true)

                    // 根据退出码进行处理
                    if (exitCode != 0) {
                        echo "Ansible 部署出现异常,请运维人员检查~!"
                        // 你可以在这里添加更多的错误处理逻辑
                        currentBuild.result = 'FAILURE'
                    } else {
                        echo "部署成功."
                    }       
                }
            }
        }
    }
    post { 
    always { 
        dingtalk (
            //指定钉钉机器人名称
            robot: "dev",
            type: 'MARKDOWN',
            title: "${JOB_BASE_NAME} 构建${currentBuild.result}",
            text: [
                "## [${JOB_BASE_NAME} 构建${currentBuild.result}提醒](${BUILD_URL}console)",
                "---",
                "- 项目名称:${JOB_BASE_NAME} ",
                "- 构建编号:${BUILD_NUMBER} ",
                "- 构建分支:${BRANCH} ",
                "- 构建人:${BUILD_USER} ",
                "- 构建URL:${BUILD_URL} ",
                "- 构建结果:${currentBuild.result} ",
                "- 构建环境:${env.PLATFORM} ",
                "- 构建时间:${BUILD_TIMESTAMP} ",
                "- 构建持续时间:${currentBuild.duration/1000}秒"
            ]
        )
    }
}
}

标签:Pipeline,NAME,echo,JOB,BASE,构建,Jenkins,stage
From: https://www.cnblogs.com/Unstoppable9527/p/18418709

相关文章

  • linux下安装部署jenkins
    目录环境准备jdk安装安装依赖安装jenkins启动jenkins查看初始密码更换源汉化环境准备jdk安装1.8版本安装#!/bin/bash[-d/data/setup/]||mkdir-p/data/setup/[-d/data/prog/]||mkdir-p/data/prog/cd/data/setup/[-f/data/setup/jdk-8u271-linux-x64.......
  • 工具分享 | JenkinsExploit-GUI - 一款Jenkins综合漏洞利用工具,一键getshell。
    0x00工具介绍JenkinsExploit-GUI是一款Jenkins综合漏洞利用工具。0x01下载链接JenkinsExploit-GUI下载链接:夸克网盘分享0x02功能介绍CVE-2015-8103/CVE-2016-0788CVE-2016-0792CVE-2017-1000353CVE-2018-1000600CVE-2018-1000861CVE-2018-1999002CVE-20......
  • jenkins上的执行者数量的设置
    原创地址: https://www.cnblogs.com/zndxall/p/9166480.htmljenkins上的执行者数量的设置并不是随意设置的,位置如下:他是跟cpu核数密切相关的,原则上是不能超过cpu的核数的,如何查看cpu的核数呢,命令如下:查看物理CPU个数cat/proc/cpuinfo|grep“physicalid”|sort|uniq|wc......
  • Redis之pipeline与事务
    前言    Redis使用的是单reactor网络模型,也就是io多路复用+非阻塞io的异步处理流程(注册事件,在事件循环callback处理事件)。我们可以将每个连接抽象看成一个pipe,哪个pipe中的数据先满就先处理。注意,单reactor指的是acceptor只有一个,而工作线程在6.0版本之前只有一个,也就......
  • Kubernetes部署jenkins
    目录配置存储方式创建命名空间创建RBAC权限创建无头服务创建有状态服务创建ingress部署配置存储方式本次使用NFS作为存储,请确保提前部署好storageClass。创建命名空间kubectlcreatensops创建RBAC权限#serviceaccountapiVersion:v1kind:ServiceAccountmetadata:......
  • jenkins远程启动任务--启用远程触发构建
    一:前言在执行Jenkins的项目构建的时候,一般都是通过web管理界面中的”构建”来执行项目构建操作,但是除此之外我们还可以通过项目配置中的”构建触发器”来触发构建操作,其中”构建触发器”有一种方式是通过配置令牌远程触发项目构建。 二:设置用户token打开当前登录用户设置页面......
  • jenkins备份与还原
    jenkins备份备份Jenkins配置和数据:登录Jenkins管理界面,点击“系统管理”->“系统设置”。在“系统设置”页面中,找到“系统配置”部分,点击“导出”按钮。在弹出的对话框中选择“导出所有配置”,然后点击“确定”。Jenkins将会生成一个config.xml文件,包含了Jenkins的所有配置信息。将......
  • Pipelines.Sockets.Unofficial 一个纯托管实现对接 System.IO.Pipelines 的 Sockets
    本文将和大家介绍Pipelines.Sockets.Unofficial这个由纯托管代码实现的,对接了System.IO.Pipelines的Sockets库。这个库不仅代码性能高,且上层调用的API足够简洁本文介绍的Pipelines.Sockets.Unofficial库是在GitHub上使用最友好的MIT协议开源的项目,详细请参阅https......
  • Jenkins 编译 .NET 6 WPF
    最近公司需求要将产品编译自动化,干了那么多年客户端开发一直都是小作坊作业最近换了一个比较正规的互联网公司一切都需要标准化流程化了,自动化也必不可少!然后我就了解到了Jenkins这玩意,找了两天资料感觉还挺简单的写篇文章收录下。因为签名UKey只要windows驱动,所以我只能将环境......
  • 【TVM 教程】在 Relay 中使用 Pipeline Executor
    ApacheTVM是一个端到端的深度学习编译框架,适用于CPU、GPU和各种机器学习加速芯片。更多TVM中文文档可访问→ApacheTVM中文站​tvm.hyper.ai/作者:HuaJiang本教程介绍如何将「PipelineExecutor」与Relay配合使用。importtvmfromtvmimportteimportnumpyasn......