在上一篇文章中,我们介绍了 Jenkins 2.x 实现流水线的两种语法,以及在实际工作中该如何选择脚本式语法或声明式语法。原文可查阅:「持续集成实践系列」Jenkins 2.x 搭建 CI 需要掌握的硬核要点(一)
在使用传统的 Jenkins Web 界面和项目时,比如自由风格类型的任务,我们对处理流程的控制能力是有限的。所采用的典型形式是任务链:任务完成后触发其他的任务。或者我们可能会包括构建后处理,不管任务成功完成与否,总是去做一些类似发送通知的事情。
除了这些基本的功能外,还可以添加条件性构建步骤插件,通过基于单个或者多个条件的构建步骤来定义更加复杂的流程。但即便如此,相比于我们编写程序时可以直接控制执行流程的方法,条件性构建步骤插件对流程的控制能力依然有限。
在本篇中,我们将聊一下,关于 Jenkins 流水线 DSL 语言所提供的用于控制流水线执行流程基本结构和一些常见技巧。
正如在系列第一篇文章中介绍到的,Jenkins DSL 采用的是 Groovy 脚本语言。这也意味着如果当你掌握了 Groovy 语言,可以按照需求在流水线中使用 Groovy 语言的结构和习惯用法,针对这一类使用者,通常会更倾向于用脚本式语法来实现流水线。但不管采用的是哪种语法,从流水线组成的角度来讲,都是由一些不同指令 + 步骤构建结构化代码块。
对于脚本式流水线,基本结构如下:
node('worker'){
stage('阶段'){
// DSL
}
}
构建脚本式流水线常用的结构或者说代码块节点主要由node
和stage
两个组成。
而,声明式流水线基本结构构成环节相对要多一些,整理了一张图如下:
需要划一个重点:可以简单理解node
是用于脚本式流水线,而agent
则是用于声明式流水线。
Jenkins Pipeline 支持的指令 (常见):
指令名 | 说明 | 作用域 |
---|---|---|
agent | 指定流水线或特定阶段在哪里运行。 | stage 或 pipeline |
environment | 设置环境变量 | stage 或 pipeline |
tools | 自动下载并安装指定的工具,并将其加入到 PATH 变量中 | stage 或 pipeline |
input | 暂停 pipeline,提示输入内容 | stage |
options | 用来指定一些预定义选项 | stage 或 pipeline |
parallel | 并行执行多个 step | stage |
parameters | 允许执行 pipeline 前传入一些参数 | pipeline |
triggers | 定义执行 pipeline 的触发器 | pipeline |
when | 定义阶段执行的条件 | stage |
build | 触发其他的 job | steps |
options Jenkins Pipeline 常见配置参数:
参数名 | 说明 | 例子 |
---|---|---|
buildDiscarder | 保留最近历史构建记录的数量 | buildDiscarder(logRotator(numToKeepStr: '10') |
timestamps | 添加时间戳到控制台输出 | timestamps() |
disableConcurrentBuilds | 阻止 Jenkins 并发执行同一个流水线 | disableConcurrentBuilds() |
retry | pipeline 发生失败后重试次数 | retry(4) |
timeout | pipeline 运行超时时间 | timeout(time:1, unit: 'HOURS') |
示例:
pipeline{
agent any
options{
buildDiscarder(logRotator(numToKeepStr: '10')
timestamps()
retry(3)
timeout(time:1, unit: 'HOURS')
}
stages{
stage('demo'){
steps{
sh 'echo hello'
}
}
}
}
更多 pipeline 指令,可参见官方介绍:
https://www.jenkins.io/doc/book/pipeline/syntax/#
下述仅挑几个常用的,用于流水线流程控制选项的指令项,介绍一些常用技巧。
这个 timeout 步骤允许限制等待某个行为发生时脚本所花费的时间。其语法相当简单。示例如下:
timeout(time:60,unit:'SECONDS'){
//该代码块中的过程被设置为超时
}
默认的时间单位是 min。如果发生超时,该步骤就会抛出一个异常。如果异常没有被处理,将导致整个流水线过程被中止。
通常推荐的做法是,在使用timeout
对任何造成流水线暂停的步骤(如一个 input 步骤)进行封装,这样做的结果是,即使出现差错导致在限定的时间内没有得到期望的输入,流水线也会继续执行。
示例如下:
node{
def response
stage('input'){
timeout(time:10,unit:'SECONDS'){
response = input message :'Please Input User'
parameters:[string(defaultValue:'mikezhou',description:'Enter UserId:',name:'userid')]
}
echo "Username = " + response
}
}
在这种情况下,Jenkins 将会给用户 10s 做出反应,如果时间到了,Jenkins 会抛出一个异常来中止流水线。
如果实际在设计流水线时,当超时发生时,并不想中止流水线向下执行,可以引入try...catch
代码块来封装 timeout。
如下代码块所示:
node{
def response
stage('input'){
try {
timeout(time:10,unit:'SECONDS'){
response = input message :'Please Input User'
parameters:[string(defaultValue:'mikezhou',description:'Enter UserId:',name:'userid')]
}
}
catch(err){
response = 'user1'
}
}
}
需要注意的是,在处理异常的时候,可以在捕获异常处设置为期望的默认值。
这个 retry 闭包将代码封底装为一个步骤,当代码中有异常发生时,该步骤可以重试 n 次。其语法如下:
retry(n){
//代码过程
}
如果达到重试的限制并且发生了一个异常,那么整个过程将会被中止(除非异常被处理,如使用try...catch
代码块)
retry(2){
try {
def result=build job: "test_job"
echo result
}
catch(err){
if(!err.getMessage().contains("UNSTABLE"))
throw err
}
}
引入waitUntil
步骤,会导致整个过程一直等待某件事发生,通常这里的 “某件事” 指的是可以返回 true 的闭包。
如果代码过程永不返回 true 的话,这个步骤将会无期限地等待下去而不会结束。所以一般常见的做法,会结合 timeout 步骤来封装 waitUntil 步骤。
例如,使用 waitUntil 代码块来等待一个标记文件出现:
timeout(time:15,unit:'SECONDS'){
waitUntil{
def ret = sh returnStatus:true,script:'test -e /home/jenkins2/marker.txt'
return (ret==0)
}
}
再举一个例子,假如我们要等待一个 Docker 容器运行起来,以便我们可以在流水线中通过 REST API 调用获取一些数据。在这种情况下,如果这个 URL 还不可用,就会得到一个异常。为了保证异常被抛出的时候进程不会立即退出,我们可以使用try...catch
代码块来捕获异常并且返回 false。
timeout(time:150,unit:'SECONDS'){
waitUntil{
try{
sh "docker exec ${containerid} curl --silent http://127.0.0.1:8080/api/v1/registry >/test/output/url.txt"
return true
}
catch(err)
return false
}
}
在 Jenkins 的 DSL 中,stash
和unstash
函数允许在流水线的节点间和阶段间保存或获取文件。
基本用法格式:
stash name:"<name>" [includes:"<pattern>" excludes:"<pattern>"]
unstash "<name>"
我们通过名称或模式来指定一个被包括或被排除的文件的集合。给这些文件的暂存处命名,以便后面通过这个名称使用这些文件。
提到 stash,很多读者可能会把Jenkins stash和Git stash功能弄混,需要说明一下,Jenkins stash
和Git stash
功能是不同的。Git stash
函数是为了暂存一个工作目录的内容,缓存那些还没有提交到本地代码仓库的代码。而Jenkins stash
函数是为了暂存文件,以便在节点间共享。
例如,master 节点和 node 节点,实现跨主机共享文件:
pipeline{
agent none
stages{
stage('stash'){
agent { label "master" }
steps{
writeFile file: "test.txt", text: "$BUILD_NUMBER"
stash name: "test", includes: "test.txt"
}
}
stage('unstash'){
agent { label "node" }
steps{
script{
unstash("test")
def content = readFile("test.txt")
echo "${content}"
}
}
}
}
}
如果你觉得文章还不错,请大家点赞分享下。你的肯定是我最大的鼓励和支持。