跳转至

第二节 声明式流水线语法

本节介绍

声明式 Pipeline 是最近添加到 Jenkins 流水线的,它在流水线子系统之上提供了一种更简单,更有主见的语法。 所有的声明式Pipeline都必须包含一个 pipeline块中,比如:

pipeline {
    //run
}

在声明式 Pipeline 中的基本语句和表达式遵循 Groovy的语法。但是有以下例外:

  • 流水线顶层必须是一个块,特别是pipelin{}
  • 不需要分号作为分割符,是按照行分割的。
  • 语句块只能由阶段、指令、步骤、赋值语句组成。例如: input被视为input()

1-1 agent(代理)

agent 指定了流水线的执行节点。

参数:

  • any 在任何可用的节点上执行pipeline
  • none 没有指定agent的时候默认。
  • label 在指定标签上的节点上运行Pipeline

node 允许额外的选项。

这两种是一样的

这两种是一样的
agent { node { label 'labelname' }}
agent { label ' labelname '}

1-1-1 agent部分详解

agent部分描述的是整个pipeline或在特定阶段执行任务时所在的agent。换句话说,Jenkins master根据此agent部分决定将任务分配到哪个agent上执行。agent部分必须在pipeline块内的顶层定义,而stage块内的定义是可选的。

agent any告诉Jenkins master:任何可用的agent都可以执行。

agent部分的定义可以放在阶段中,用于指定该stage执行时的agent

pipeline { 
    agent any 
    stages { 
        stage('Build') { 
            agent any 
            steps { 
                echo "build" 
            } 
        } 
    } 
}

通过标签指定agent

pipeline需要在JDK8环境下进行构建时,就需要通过标签来指定agent。代码如下:

pipeline 
{ 
    agent { 
        label 'jdk8' 
    } 
    stages { 
        stage('Build') { 
            steps { 
                echo "build" 
            } 
        } 
    } 
}

事实上,agent{label 'jdk8'}是以下定义方式的简略写法。

agent{
    node {
        label 'jdk8'
    }
}

label环境下同时执行,有windowsjdk8标签的agent

agent{
    label 'jdk8 && windows' 
}

上文中,在增加agent时,已配置好了该agent的默认工作目录路径,但是agent部分允许我们对工作目录进行自定义。 node除了label选项,还提供了另外一个选项customWorkspace,自定义工作目录,写法如下:

agent { 
    node { 
        label 'jdk8' 
        customWorkspace '/var/lib/custom' 
    } 
} 

customWorkspace选项除了写绝对一路径, 还可以写相对于默认工作目录路径的相对路径。

when指令的beforeAgent选项

在默认情况下,阶段内所有的代码都将在指定的Jenkins agent上执行:when指令提供了一个beforeAgent选项,当它的值为true时, 只有符合when条件时才会进入该Jenkins agent,这样就可以避免没有必要的工作空间的分配, 也就不需要等待可用的Jenkins agent了。

在某些场景下,beforeAgent选项通常用于加速pipeline的执行。示例如下:

pipeline { 
    agent none 
    stages {
         stage('Example Build') { 
            steps { 
            echo 'Hello World' 
        }
    } 
    stage('Example Deploy') { 
        agent { 
            label "some-label" 
        } 
        when { 
            beforeAgent true 
            branch 'production' 
            } 
            steps { 
                echo 'Deploying' 
            } 
        } 
    }
} 

只有分支为production时,才会进人“Example Deploy”阶段。这样就可以避免在some-labelagent中拉取代码,从而达到加速pipeline执行的目的。

1-1-2 将构建任务交给Docker

目前我们所有的构建都执行在机器上,接下来介绍如何让构建执行在Docker 容器中。

Jenkins master要将构建任务分配给Docker, 就必须在Jenkins agent上安装Docker。建议给 这些agent打上docker的标签。

pipeline {
    agent {
        docker { image 'node:7-alpine' }
    }
    stages {
        stage('Test') {
            steps {
                sh 'node --version'
            }
        }
    }
}
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Test)
[Pipeline] sh
+ node --version
v7.10.1
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
$ docker stop --time=1 1a245bfd1f49c695d450bd776ff6353b39be57db4c37af9a457e3a27baf9f5d7

与之前不同的,在agent部分我们将node换成了docker。下面分别解释docker的常用选项。

  • label(可选):字符串类型,与nodelabel的作用一样。
  • image:字符串类型,指定构建时使用的Docker镜像。
  • args(可选):字符串类型,Jenkins执行docker run命令时所带的参数,如args '-v /tmp:/tmp'
  • aiwaysPull(可选):布尔类型,强制每次执行docker pull命令时都重新拉取镜像。

1-1-3 配置Docker私有仓库

Docker 拉取镜像时,默认是从Docker官方中心仓库拉取的。那么如何实现从私有仓库拉取呢?

Docker插件为我们提供了界面操作,具体步骤如下:

进人Manage Jenkins -> Configure System页面,找到“Declarative Pipeline(Docker)”部分

Alt Image Text

  • Docker Label:当pipeline中的agent 部分没有指定label选项时,就会使用此配置
  • Docker registry URL : Docker私有仓库地址。
  • Registry credentials:登录Docker私有仓库的凭证。

1-2 post

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

  • always 无论流水线或者阶段的完成状态。
  • changed 只有当流水线或者阶段完成状态与之前不同时。
  • failure 只有当流水线或者阶段状态为”failure”运行。
  • success 只有当流水线或者阶段状态为”success”运行。
  • unstable 只有当流水线或者阶段状态为”unstable”运行。例如:测试失败。
  • aborted 只有当流水线或者阶段状态为”aborted “运行。例如:手动取消。
  • cleanup 清理条件块。不论当前完成状态是什么,在其他所有条件块执行完成后都执行。

post部分可以同时包含多种条件块。以下是post部分的完整示例。

pipeline {
agent any
stages {
    stage('Example') {
        steps {
            echo 'Hello World'
        }
        post {
            always {
                echo "stage post always"
            }
        }
    }
}
post { 
    always { 
        echo 'I will always say Hello again!'o
    }
}
}

1-3 stages(阶段)

包含一系列一个或多个stage 指令, 建议 stages 至少包含一个stage 指令用于连续交付过程的每个离散部分,比如构建, 测试, 和部署。

pipeline {
    agent any
    stages { 
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

1-4 steps(步骤)

step是每个阶段中要执行的每个步骤。

pipeline {
    agent any
    stages {
        stage('Example') {
            steps { 
                echo 'Hello World'
            }
        }
    }
}

1-5 指令

1-5-1 environment

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

该指令支持一个特殊的方法 credentials(),该方法可用于在Jenkins环境中通过标识符访问预定义的凭证。对于类型为 “Secret Text”的凭证,credentials() 将确保指定的环境变量包含秘密文本内容。对于类型为 “SStandard username and password”的凭证, 指定的环境变量指定为 username:password,并且两个额外的环境变量将被自动定义 :分别为 MYVARNAME_USRMYVARNAME_PSW

pipeline {
    agent any
    environment { 
        CC = 'jxi'
    }
    stages {
        stage('Example') {
            environment { 
                AN_ACCESS_KEY = credentials('my-prefined-secret-text') 
            }
            steps {
                sh 'printenv'
            }
        }
    }
}

环境变量可以被有作是pipelineJenkins交互的媒介。比如,刚以任pipeline中通过BUILD NUMBER变量知道构建任务的当前构建次数。环境变量可以分为Jenkins内置变量和自定义变量。

Jenkins内置变量

pipeline执行时,Jenkins通过一个名为env的全局变量,将Jenkins内置环境变量暴露出来。其使用方法有多种,示例如下:

pipeline { 
    agent any 
    stages { 
        stage('Example') { 
            steps {
                 echo "Running ${env.BUILD_NUMBER1} on ${env.JENKINS_URL}"  //方法 一
                 echo "Running $env.BUILD_NUMBER on $env.JENKINS_URL" //方法 二
                 echo "Running ${BUILD_NUMBER} on ${JENKINS_URL}" //方法 三
            }
        }
    }
}

默认env的属性可以直接在pipeline中引用。所以以上方法都是合法的。但是不推荐方法三因为出现变量冲突时非常难查在问题

那么。env变从那有哪此可用属性呢?通过访问的<Jenkins master的地址>/pipeline-syntax/globalenv来获取完整列表在列表中。当一个变量声明为”For a multibranch project"时, 代表只有多分支项目才会有此变量

下面我们简单介绍几个在实陈工作中经常用到的变量

  • BUILD_NUMBER:构建号,累加的数字。在打包时它可作为制品名称的一部分比如server-2.jar
  • BRANCH_NAME:多分支pipeline项目支持。当需要根据不同的分支做不同的事情时就会用到, 比如通过代码将代码将release分支发布到生产环境中、master分支发布到测试环境中
  • BUILD_URL:当前构建页面的URL。如果构建失败则需要将失败的构止链接放到邮件通知中,这个链接就可以是BUILD_URL
  • GIT_BRANCH:通过git拉取的源码构建的项目才会有此变最

在使用env变量时, 需要注意不同类型的项目,env变量从所包含的变量及其值是不一样的。 比如普通pipeline务中的GIT_BRANCH变量的值为origin/master,而在多分支pipeline任务中GIT_BRANCH变量的值为master

小技巧:在调试pipeline时可以在开始阶段加一句:sh 'printenv'时变量的属性值打印出来。这样可以帮助我们避免不少问题

自定义piepline环境变量

piepline变得复杂时, 我们就会有定义自己的环境变量的需求。声明式piepline提供了environment指令方便自定义变是。比如

pipeline { 
    agent any 
    environment {
        CC = 'clang'
    } 
 stages { 
        stage('Example') { 
            environment {
                DEBUG_FLAGS = '-g'
            }
            steps { 
                echo "${CC} ${DEBUG_FLAGS}" 
                sh 'printenv' 
            } 
        } 
    } 
} 
printenv
JENKINS_NODE_COOKIE=591de548-b216-498b-8221-26ae09ca063d
XDG_SESSION_ID=4
BUILD_URL=http://192.168.33.11:8080/job/Chapter2-6env/2/
HOSTNAME=jabox
TERM=xterm-256color
SHELL=/bin/bash
HUDSON_SERVER_COOKIE=ec6f20ac1eab6177
HISTSIZE=1000
STAGE_NAME=Example
SSH_CLIENT=172.16.72.2 51333 22
BUILD_TAG=jenkins-Chapter2-6env-2
SSH_TTY=/dev/pts/1
...
LANG=en_US.UTF-8
JOB_NAME=Chapter2-6env
BUILD_DISPLAY_NAME=#2
BUILD_ID=2
JENKINS_URL=http://192.168.33.11:8080/
HISTCONTROL=ignoredups
JOB_BASE_NAME=Chapter2-6env
HOME=/home/vagrant
SHLVL=3
EXECUTOR_NUMBER=3

另外,environment指令可以在pipeline中定义,代表变量作用域为整个pipeline;也可以在Stage中定义,代表变量只在该阶段有效。

但是这些变量都不是跨pipeline的,比如pipeline a访问不到pipeline b的变量。在pipeline之间共享变量可以通过参数化pipeline来实现。

在实际工作中,还会遇到一个环境变量引用另一个环境变量的情况。在 environment中可以这样定义:

environment ( 
    _server_name = 'mail—server' 
    _version = "${BUILD_NUMBER}" 
    _artifact_name = "${_server_name}—S{_VERSION}.jar" 

值得注意的是如果在environment中定义的变量与env中的变量重名那么被重名的变量的值会被攫盖掉。比如在environment中定义PATH变量(PATH也是env中的一个变量)。

environment ( 
    PATH="invalid path"
}

执行sh时,就会报错

自定义全局环境变量

env中的变欣都是Jenkins内置的或者是与具体plpeline相关的。有时候我们需要定义一些全局的跨plpeline的自定义变量

进入Manage Jenkins -> Configure System -> Global properties页,勾选"Environment variables"

Alt Image Text

自定义全局环境变童会被加人env属性列表中,所以使用自定义全局环境变量与使用Jenkins内置变量的方法无异:${env. whomai}

echo "whomai: ${env.whoami}"
whomai: admin_jacob

1-5-2 options

options 指令允许从流水线内部配置特定于流水线的选项。 流水线提供了许多这样的选项, 比如buildDiscarder,但也可以由插件提供, 比如 timestamps

  • buildDiscarder: 为最近的流水线运行的特定数量保存组件和控制台输出。
  • disableConcurrentBuilds: 不允许同时执行流水线。 可被用来防止同时访问共享资源等。
  • overrideIndexTriggers: 允许覆盖分支索引触发器的默认处理。
  • skipDefaultCheckout: 在agent 指令中,跳过从源代码控制中检出代码的默认情况。
  • skipStagesAfterUnstable: 一旦构建状态变得UNSTABLE,跳过该阶段。
  • checkoutToSubdirectory: 在工作空间的子目录中自动地执行源代码控制检出。
  • timeout: 设置流水线运行的超时时间, 在此之后,Jenkins将中止流水线。
  • retry: 在失败时, 重新尝试整个流水线的指定次数。

  • timestamps 预测所有由流水线生成的控制台输出,与该流水线发出的时间一致。

//指定一个小时的全局执行超时, 在此之后,Jenkins将中止流水线运行。
pipeline {
agent any
options {
    timeout(time: 1, unit: 'HOURS') 
}
stages {
    stage('Example') {
        steps {
            echo 'Hello World'
        }
    }
}
}

buildDiscarder

保存最近历史构建记录的数量。当piepline执行完成后,会在硬盘上保存制品和构建执行日志,如果长时间不清理会占用大量空间,设置此选项后会自动清理。此选项只能在piepline下的Options中使用。示例如下:

options {
    buildDiscarder(logRatator(numToKeepStr: '10'))
}

checkoutToSubdirectory:

Jenkins从版本控制库拉取源码时,默认检出到工作空间的根目录中,此选项可以指定检出到工作空间的子目录中。示例如下:

options {
    checkoutToSubdirectory('subdir')
}

disableConcurrentBuilds

同一个pipeline, Jenkins默认是可以同时执行多次的,此选项是为了禁止pipeline同时执行。 在某些pipeline存在抢占资源或调用冲突的场景下,此选项非常有用。

options {
    disableConcurrentBuilds()
}

newContainerPerStage

agentdockerdockerfile时,指定在同一个Jenkins节点上,每个stage都分别运行在一个新的容器中,而不是所有stage都运行在同一个容器中。

options {
    newContainerPerStage()
}

retry

当发生失败时进行重试,可以指定整个pipeline的重试次数。需要注意的是,这个次数是指总次数,包括第1次失败。以下例子总共会执行4次。当使用retry选项时, options可以被放在stage块中。

pipeline {
    agent any
    options {
        retry(4)    
    }

    stages{
    stage('build'){
        steps{
            echo 'ok'
            error("emmm...")
            }
        }   
    }
}
  • timeout:

如果piepline执行时间过长了,超出了我们设置的timeout时间, Jenkins 将终止pipeline. 以下例子中以小时为单位,还可以以SECONDS(秒)MINUTES(分钟)为单位。当使用timeout选项时,options可以被放在stage块中

options {
    timeout(time: 10, unit": 'HOURS'
}

设置此选项后, 强迫团队去处理执行时间过长的pipeline,从而优化pipeline的反馈周期。通常将timeout设置为10分钟就可以了

1-5-3 参数化pipeline

参数化pipeline是指可以通过传参来决定 pipeline的行为。参数化让写pipeline就像写函数, 而函数意为着可重用,更抽象。所以,通常使用参数化pipeline来实现一些通用的pipeline

为流水线运行时设置项目相关的参数

  • string 字符串类型的参数, 例如:
parameters { string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '') }
  • booleanParam 方法用来定义一个布尔类型的参数
    • defaultValue 默认值
    • description: 参数描述信息
    • name: 参数名
parameters { booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '') }
  • text 多行文本类型,换行使用\n
parameters {
    text(name, 'DEPLOY_TEXT, defaultValue: 'One\nTwo\nThree\n', description: '') 
}
  • choice 选择参数类型,使用\n来分隔多个选项。
parameters {
    choice(name: 'CHOICES', choices: 'dev\ntest\nstaging', description: 'Please chose your env') 
}
  • file,文件类型,用户可上传文件。

  • password,密码类型。

parameters { 
    password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'A secret password'
}
  • 多参数

参数化的pipeline不可能只支持接收一个参数,以下就是为pipeline同时定义多个参数的例子

parameters { 
    booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '') 
    password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'A secret password') 
    string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '') 
}
  • 示例
pipeline {
agent any
parameters {
    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
}
stages {
    stage('Example') {
        steps {
            echo "Hello ${params.PERSON}"
        }
    }
}
}

1-5-4 触发pipeline详解:

1.构建触发器

  • cron 计划任务定期执行构建。
triggers { cron('H */4 * * 1-5') }
  • pollSCMcron定义类似,但是由jenkins定期检测源码变化。
triggers { pollSCM('H */4 * * 1-5') }
  • upstream 接受逗号分隔的工作字符串和阈值。 当字符串中的任何作业以最小阈值结束时,流水线被重新触发。
triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS) }
  • 示例
pipeline {
agent any
triggers {
    cron('H */4 * * 1-5')
}
stages {
    stage('Example') {
        steps {
            echo 'Hello World'
        }
    }
}
}

2.时间触发

时间触发是指定义一个时间,时间到了就触发pipeline执行。 在Jenkins Pipeline中使用trigger指令来定义时间触发。

trigger指令只能被定义在pipeline块下, Jenkins内置支待cronpollSCMupstream三种方式几他方式可以通过插件来实现。

  • 定时执行: cron

定时执行就像cronjob,一到时间点就执行。它的使用场景通常是执行一些周期性的job,如每夜构建

pipeline { 
    agent any 
    triggers { 
        cron('0 0 * * *') 
    }
    stages { 
        stage('Nightly build') { 
            steps { 
                echo "Build every night"
            }
        }
    }
}

Jenkins trigger cron语法采用的是UNIX cron语法(有些细微的区别)。

一条cron包含5个字段,使用空格或Tab分隔,格式为:MINUTE, HOUR, DOM, MONTH, DOW

每个字段的含义为:

  • MINUTE:一小时内的分钟,取值范围为0~59
  • HOUR:一天内的小时,取值范围为0~23
  • DOM:一个月的某一天,取值范围为1-31
  • MONTH:月份,取值范围为1~12
  • DOW:星期几,取值范围为0~707代表星期天。

还可以使用以下特殊字符,一次生指定多个值

  • :匹配所有的值
  • M-N:匹配M到N之间的值。
  • M-N/X or */x:指定在MN范围内,以N值为步长。
  • A,B,....Z:使用逗号枚举多个值

在一些大型组织中,会同时存在大量的同一时刻执行的定时任务,比如N个半夜零点(0 0 * * *中)执行的任务。这样会产生负载不均衡。

Jenkins trigger cron语法中使用"H"字符来解决这一问题,H代表hash。对于没必要准确到零点0分执行的任务,cron可以这样写:H 0 * * * , 代表在零点0分至零点59分之间任何一个时间点执行。

需要注意的是,H应用在DOM(一个月的某一天)字段时会有不准确的清况,因为10月有31天,而2月却是28天。

Jenkins trigger cron还设计了一些人性化的别名:@yearly@annually@monthly@weekly@daily@ midnight@hourly。例如,@hourlyH * * * *相同,代表一小时内的任何时间 @midnight实际上代表在半夜12:00到凌晨2:59之间的某个时间。其他别名很少有应用场景。

  • 轮询代码仓库:pollSCM

轮询代码仓库是指定期到代码仓库询问代码是否有变化,如果有变化就执行。有读者会问: 那多久轮询一次?笔者的回答是:越频繁越好。因为构建的间隔时间越长,在一次构建内就可能 会包含多次代码提交。当构建失败时,你无法马上知道是哪一次代码提交导致了构建失败。

pipeline { 
    agent any 
    triggers { 
        // check every minute 
        pollSCM(`H/1 * * * *`) 
    } 
}

3.事件触发

事件触发就是发生了某个事件就触发pipeline执行。这个事件可以是你能想到的任何事件。 比如手动在界面上触发、其他job主动触发、HTTP API Webhook触发等

由上游任务触发:upstream

B任务的执行依赖A任务的执行结果时,A就被称为B的上游任务。在Jenkins 2.22及以 上版本中,trigger指令开始支持upstream类型的触发条件。upstream的作用就是能让B plpeline自行决定依赖哪些上游任务。示例如下

// jobl fil job2 #6kII45- 

triggers { 
    upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS)
}

upstreamProjects参数接收多个任务时,使用, 分隔。threshold参数是指上游任务的执行结果是什么值时触发。hudson.model.Result是一个枚举包括以下值:

  • ABORTED:任务被手动中止。
  • FAILURE:构建失败。
  • SUCCESS:构建成功。
  • UNSTABLE:存在一些错误,但不至于构建失败。
  • NOT BUILT:在多阶段构建时,前面阶段的问题导致后面阶段无法执行。

注意:需要手动触发一次任务,让Jenkins加载pipeline后,trigger指令才会生效。

由另一个pipeline传参并触发

既然存在参数化的pipeline, 那么就表示可以在一个pipeline中"调用"另一个pipelineJenkins pipeline中可以使用build步掇实现此功能。build步骤是pipeline插件的一个组件所以不需要另外安装插件可以直接使用

build步骤其实也是一种触发pipeline执行的方式它与triggers指令中的upstream方式有两个区别:

  • (1) build步骤是由上游pipeline使用的,而upstream方式是由下游pipeline使用的。
  • (2) build步骤是可以带参数的,而upstream方式只是被动触发,并且没有带参数。
steps { 
    build( 
        job:"parameters-example", 
        parameters: [ 
            booleanParam(name: 'userFlag', value: true) 
        ]
    )
}

我们来看看build步骤的基本的两个参数。

  • job(必填):目标Jenkins任务的名称。
  • parameters(可选)数组类型,传入目标pipeline的参数列表。传参方法与定参方法类似。
parameters: [ 
    booleanParam(name: 'DEBUG_BUILD', value: true), 
    password(name: 'PASSWORD', value: 'prodSECRET'), 
    string(name: 'DEPLOY_ENV', value: 'prod'), 
    text(name: 'DEPLOY_TEXT', value: 'a\nb\nc\n'), 
    string(name: 'CHOICES', value: 'dev') 
]

我们注意到choice类型的参数没有对应的传参方法,而是使用string传参方法代替的。

除此之外,build步骤还支持其他三个参数

  • propagate(可选):布尔类型,如果值为true,则只有当下游pipeline的最终构建状态为 SUCCESS时,上游pipeline才算成功;如果值为false,则不论下游pipeline的最终构建状态是什么,上游pipeline都忽略。默认值为true
  • quietPeriod(可选):整型,触发下游pipeline后,下游pipeline等待多久执行。如果不设置此参数,则等待时长由下游pipeline确定。单位为秒。
  • wait(可选):布尔类型,是否等待下游pipeline执行完成。默认值为true

如果你使用了folder插件(https://plugins.jenkins.io/clondbees-folder),那么就需要注意build 步骤的job参数的写法了。

使用Folder插件,可以让我们像管理文件夹下的文件一样来管理Jenkins项目。 我们的Jenkins 项目可以创建在这些文件夹下。如果日标pipeline与源pipeline在同一个目录下则叮以直接使 用名称;如果不在同一个目录下,则需要指定相对路径,如sister-folder/downstream,或者指定绝对路径,如top-level-folder/nested-folder/dowsstream

获取上游pipeline的信息

遗憾的是,上游pipeline触发下游pipeline时,并没有自动带上自身的信息。所以,当下游 pipeline需要使用上游pipeline的信息时,上游pipeline信息就要以参数的方式传给下游pipeline。 比如在上游pipeline中调用下游pipeline时,可以采用以下做法。

build job: 'all-in-one-deploy' , parameters: [ 
    string(name: 'DEPLOY_ENV', value: '${deploy_env}"), 
    string(name: 'triggerJobName', value: "${env.JOB_NAME}"),
    string(name: 'triggerJobBuildNumber', value: ${env.BUILD_NUMBER}") 
] 

4. 使用Conditonal BuildStep插件处理复杂的判断逻辑:

这样写起来很不优雅Conditional BuildStep插件,可以让我们像使用when指令样进行条件判断以下代码就是安装Conditional BuildStep插件的写法

pipeline {
    agent any 
    parameters {
        choice(name: 'CHOICES', choices: 'dev\ntest\nstaging', description: 'Please choose your env') 
    }

    stages { 
        stage("deploy test") { 
            when { 
                expression { return params.CHOICES == 'test' }
            }
            steps { 
                echo "deploy to test"
            }
        } 
        stage("deploy staging") { 
            when { 
                expression { return params.CHOICES == 'staging' }
            }
            steps { 
                echo "deploy to staging" 
            }
        }
    }
}
[Pipeline] {
[Pipeline] stage
[Pipeline] { (deploy test)
Stage "deploy test" skipped due to when conditional
[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (deploy staging)
[Pipeline] echo
deploy to staging

现实中,我们会面对更复杂的判断条件。而expression表达式本质上就是一个Groovy代 码块,大大提高了表达式的灵活胜。以下是比较常用的例子。

  • 或逻辑
when { 
    // A or B 
    expression {  return A || B } 
} 
  • 与逻辑
when { 
    // A or B 
    expression {  return A && B } 
} 
  • 从文件中取值
when { 
    expression { return readFile('pom.xml' ).contains('mycomponent') } 
}
  • 正则表达式
when { 
    expression { return token ==~ /(?i)(Y|YES|T|TRUE|ON|RUN)/}
}

5. 使用Generic Webhook Trigger插件实现触发

装Generic Webhook Trigger插件(下文使用GWT简称)后,Jenkins会暴露一个API: <JENKINS_URL>/generic-webhook-trigger/invoke,即由GWT插件来处理此API的请求。 如何处理呢?GWT插件接收到JSONXMLHTTP POST请求后,根据我们配置的规则决定触发哪个Jenkins项目。基本原理就这么简单。下面我们先感受一下,然后再详细介绍GWT各参数的含义。

现在,我们创建一个普通的pipeline项目。代码如下:

pipeline { 
    agent any 
    triggers { 
        GenericTrigger(
            genericVariables: [ 
                [key: 'ref', value: '$.ref']
            ],
            token: 'secret',
            causeString: 'Triggered on $ref',
            printContributedVariables: true,
            printPostContent: true
        )
    }
    stages { 
        stage('Some step')  { 
            steps { 
            sh "echo $ref"
            sh "printenv"
        }
    }
  }
} 

注意:在创建完成后,需要手动运行一次,这样pipeline的触发条件才会生效。 然后我们发起一次HTTP POST请求.

curl -X POST -H "Content-Type: application/json" -d '{"ref": "refs/heads/master"}' -vs http://192.168.33.11:8080/generic-webhook-trigger/invoke?token=secret
...
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 403 No valid crumb was included in the request</title>
</head>
<body><h2>HTTP ERROR 403 No valid crumb was included in the request</h2>
<table>
<tr><th>URI:</th><td>/jenkins/generic-webhook-trigger/invoke</td></tr>
<tr><th>STATUS:</th><td>403</td></tr>
<tr><th>MESSAGE:</th><td>No valid crumb was included in the request</td></tr>
<tr><th>SERVLET:</th><td>Stapler</td></tr>
</table>
<hr><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.4.27.v20200227</a><hr/>.
...

我们需要disbale CSRF, open "Manage Jenkins" / "Script Console"

import jenkins.model.Jenkins
def instance = Jenkins.instance
instance.setCrumbIssuer(null)

It should be enabled afterwards again by setting to the Default CrumbIssuer again

import hudson.security.csrf.DefaultCrumbIssuer
import jenkins.model.Jenkins

def instance = Jenkins.instance
instance.setCrumbIssuer(new DefaultCrumbIssuer(true))
instance.save()
curl -X POST -H "Content-Type: application/json" -d '{"ref": "refs/heads/master"}' -vs http://192.168.33.11:8080/generic-webhook-trigger/invoke?token=secret

日志如下

Triggered on refs/heads/master
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
GenericWebhookEnvironmentContributor
 Received:

{"ref": "refs/heads/master"}


Contributing variables:

    ref = refs/heads/master


[Pipeline] node
Running on Jenkins in /var/jenkins_home/workspace/Chapter2-8webhook
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Some step)
[Pipeline] sh
+ echo refs/heads/master
refs/heads/master
[Pipeline] sh
+ printenv
JAVA_URL_VERSION=8u242b08
...

Generi Trigger触发条件由GWT插件提供。此触发条件可以说是GWT的所有内容。

笔者将GenericTrigger触发条件分为5部分,这样更易于理解各参数的作用。

  • HTTP POST请求中提取参数值。
  • token,GWT插件用于标识Jenkins项目的唯一性。
  • 根据请求参数值判断是否触发Jenkins项目的执行。
  • 日志打印控制。
  • Webhook响应控制。

  • Webhook请求中捉取参数值

一个HTTP POST请求可以从三个维度提取参数即POST bodyURL参数和headerGWT 插件提供了三个参数分别对这下个维度的数据进行提取

1.genericVariables:提取POST body中的参数。

genericVariables: [ 
    [key: 'ref', value: '$.ref', 
    [key: 'before', 
    value: '$before', 
    expressionType: 'JSONPath', 
    regexprFiter: ' ',
    defaultValue: ' '
    ]
]
  • value: JSONPath表达式或者XPath表达式取决于expressionType参数值,用于从POST body 中提取值
  • key:从POST body中提取出的值的新变量,可用于pipeline其他步骤。
  • expressionType: 可选 value的表达式类型,默认为JSONPath。当请求为XML内容时,必须指定XPath值。
  • defaultValue:可选,当提取不到值,且defaultValue不为空时,则使用defaultValue作为返回值
  • regexpFilter:可选,过滤表达式,对提取出来的仇进行过滤。regexpFilter做的事情其实就是 string.replaceAll(regexpFilter," ");string是从HTTP请求中提取出来的值。

2.genericRequestVariables:从URL参数中提取值。

genericRequestVariables: [ 
    [key: 'requestWithNumber', regexpFilter: '[^0-9]'], 
    [key: 'requestWithString', regexpFilter: ''] 
] 

3.genericHeaderVariables:从HTTP header中提取值。

genericHeaderVariables: [ 
    [key: headerWithNumber', regexpFilter: '[^0-9]'], 
    [key: 'rheaderWithString', regexpFilter: ' '] 
] 

genericHeaderVariables的用法与genericRequestVariables一样,区另是它是从HTTP header中提取值的。

触发具体某个Jenkins项目

我们看到GenericTrigger方法有一个token参数。

triggers { 
    GenericTrigger( 
        token: 'secret',
    )
}

token参数的作用是标识一个pipeline,在Jenkins中的唯一性(当然,没有人阻止你让所有的 pipeline使用同一个token)。为什么需要这个参数呢?这要从GWT插件的原理说起

Jenkins接收到genenic-webhook-trigger/invoke接口的请求时,会将请求代理给GWT 插件处理。

GWT插件内部会从Jenkins实例对象中取出所有的参数化Jenkins项目,包括pipeline, 然后进行遍历。如果在参数化项目中GenericTrigger配置的token的值与Webhook请求时的token 的值一致,则触发此参数化项日。

如果多个参数化项目的token值一样,则他们都会被触发。

小技巧:pipelinetoken可以被设置为Jenkins的项目名。比如:

triggers { 
    GenericTrigger( 
    // ... 
    token: env.JOB_NAME, 
    // ...
    )
} 

根据请求参数值判断是否触发Jenkins项目执行

GWT并不只是根据token值来判断是否触发,还可以根据我们提取出的值进行判断。如下:

GenericTrigger( 
    genericVariables: [ 
        [key: 'refValue', value: '$.ref']
    ], 
    token: env.JOB_NAME, 

    regexpFilterText: '$refValue', 
    regegpFilterExpresston: 'refs/heads/(master|dev)' 
  • regexpFi!terText:需要进行匹配的key例子中,我们使用从POST body中提取出的refValue 变量值。
  • regexpFilterExpression:正则表达式。

如果regexpFilterText参数的值符合regexpFilterExpressnon参数的正则表达式,则触发执行。

控制打印内容

打印日志有助于调试。GWT插件提供了三个参数。

  • printPostContent:布尔值, 将Webhook请求信息打印到日志上。
  • printContributedVariables: 布尔值,将提取后的变量名及变量值打印出来。,
  • causeString:字符串类型,触发原因,可以直接引用提取后的变量, causeStringTriggered on $msg

控制响应

  • silentResponse:布尔类型,在正常情况下,当Webhook请求成功后,GWT插件会返回HTTP 200状态码和触发结果给调用方。但是当silentResponse设置为true时,就只返回HTTP 200 状态码,不返回触发结果。

1-5-5 tool

获取通过自动安装或手动放置工具的环境变量。支持maven/jdk/gradle。工具的名称必须在系统设置->全局工具配置中定义

示例:

pipeline {
    agent any
    tools {
        maven 'apache-maven-3.0.1' 
    }
    stages {
        stage('Example') {
            steps {
                sh 'mvn --version'
            }
        }
    }
}

Or

Global Tool Configuration -> Maven

Alt Image Text

Agent上面安装最新的maven

https://linuxize.com/post/how-to-install-apache-maven-on-centos-7/
wget https://www-us.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz -P /tmp
sudo tar xf /tmp/apache-maven-3.6.3-bin.tar.gz -C /opt
sudo ln -s /opt/apache-maven-3.6.3 /opt/maven
sudo vim /etc/profile.d/maven.sh

export JAVA_HOME=/usr/lib/jvm/jre-openjdk
export M2_HOME=/opt/maven
export MAVEN_HOME=/opt/maven
export PATH=${M2_HOME}/bin:${PATH}

sudo chmod +x /etc/profile.d/maven.sh

source /etc/profile.d/maven.sh
$ mvn -version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /opt/maven
Java version: 1.8.0_252, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "3.10.0-957.12.2.el7.x86_64", arch: "amd64", family: "unix"

$ whereis mvn
mvn: /opt/apache-maven-3.6.3/bin/mvn.cmd /opt/apache-maven-3.6.3/bin/mvn
stage('tools') {
        steps {

            mvnHome = tool "m2"
            println(mvnHome)

            sh "${mvnHome}/bin/mvn --version"
        }
    }

Alt Image Text

1-5-6 input

执行input步骤会暂停pipeline,直到用户输入参数。这是一种特殊的参数化pipeline的方法。我们可以利用Input步骤实现以下两种场景:

  • (1)实现简易的审批流程。例如,pipeline暂停在部署前的阶段,由负责人点击确认后,才能部署
  • (2)实现手动测试阶段。在pipeline中增加一个手动测试阶段,该阶段中只有一个input步骤,当手动测试通过后,测试人员才可以通过这个Input步骤。
pipeline { 
    agent any 
    stages { 
        stage('deploy') { 
            steps { 
                input message: "Deploy or terminate" 
            } 
        }
    }
}

Alt Image Text

[Pipeline] input
Deploy or terminate
Proceed or Abort
[Pipeline] }
[Pipeline] // stage

Input 步骤的复杂用法

def approvalMap 

pipeline { 
    agent any 
    stages { 
        stage('pre deploy') { 
            steps { 
                script { 
                    approvalMap = input( 
                        message: 'Ready to deploy to which env?' ,
                        ok: 'ok', 
                        parameters: [ 
                            choice(choices: 'dev\ntest\nprod', description: 'Deploy to which Env? ', name: 'ENV'),
                            string(defaultValue: '', description: '', name: 'myparam') 
                        ], 
                        submitter: 'admin,admin2,releaseGroup', 
                        submitterParameter: 'APPROVER' 
                    )
                }
            }
        }
        stage('deploy') { 
            steps { 
                echo "APPROVER is ${approvalMap['APPROVER']}" 
                echo "Deploy to the Env: ${approvalMap['ENV']}"
                echo "Self defined param: ${approvalMap['myparam']}" 
                }
            }
        }
}

approvaiMap 还可以防在environment中,更加优雅

environment {
    approvalMap = ' '
}

我们在pipeline外定义了一个变量approvalMap:这是因为定义在阶段内的变最的作用域只在这个阶段中,而input步骤的返回值需要跨阶段使用,听以需要将其定义在pipeline外:这样变量approvaiMap的作用域就是整个pipeline了。

同时,由于在pipeline中直接使用了Groovy语言赋值表达式.所以需要将approvalMap=input(...)放到script块中。

input步骤的返回值类型取决于要返回的值的个数。如果只有一个值,返回值类型就是这个值的类型;如果有多个值,则返回值类型是Map类型。本例返回的approvalMap就是一个map, Mapkey就是每个参数的name属性,比如ENVmyparam都是key

除了可以在返回的map中放手动输入的值,还可以放其他数据,比如submitterParameter: 'APPROVER,代表将key APPROVER放到返回的map中。 下面我们分别介绍input步骤的参数。

  • message 呈现给用户的提示信息。
  • id 可选,默认为stage名称。
  • ok 默认表单上的ok文本。
  • submitter可选的,以逗号","分隔的用户列表或允许提交的外部组名。默认允许任何用户。
  • submitterParameter 环境变量的可选名称。如果存在,用submitter名称设置。
  • parameters 提示提交者提供的一个可选的参数列表。

示例:

pipeline {
    agent any
    stages {
        stage('Example') {
            input {
                message "Should we continue?"
                ok "Yes, we should."
                submitter "alice,bob"
                parameters {
                    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
                }
            }
            steps {
                echo "Hello, ${PERSON}, nice to meet you."
            }
        }
    }
}

设置手动输入步骤超时后,pipeline自动中止

input步骤可以与timeout步骤实现超时自动中止pipeline,防止无限等待。以下pipeline一小时后不处理就自动中止。

pipeline { 
    agent any 
    stages { 
        stage('deploy') { 
            steps { 
                timeout(time: 1, unit:'HOURS') { 
                    input message: "Deploy or terimate" 
                } 
            }
        }
    }
}

1-5-7 when和多分支构建

根据分支部署到不同的环境

git分支可以用于对代码进行物理隔离。对分支的管理有很多方法,比如主干开发,发布分支以及Gitflow法等。我们不讨论它们的好坏。

但不论使用哪种分支管理方法,都可能会涉及一个问题:如何根据不同的分支做不同的事情,比如根据不同的分支部署到不同的环境。类似这样的事情可以使用if-else来实现。

stage("deploy to test"){ 
    steps{ 
        script { if (env.GIT_BRANCH == 'master') { 
                echo "deploy to test env" 
            } 
        } 
    } 
} 
stage("deploy to prod"){ 
    steps{ 
        script { 
            if (env.GIT_BRANCH == 'release') { 
                echo "deploy to prod" 
                } 
            } 
        } 
} 

但是这样的代码不够优雅,而且不是声明式的。使用when指令可以让pipeline看起来更优雅。

stage("deploy to test"){ 
    when { 
        branch 'master' 
    } 
        steps{ 
        echo "deploy to test" 
    } 
} 

stage("deploy to prod"){ 
    when { 
        branch 'release' 
    } 
    steps{ 
        echo "deploy to prod" 
    } 
} 

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

内置条件

  • changelog:如果版本控制库的changelog符合正则表达式, 则执行
when {
    changelog '.*^\\[DEPENDENCY\\].+$'
}
  • changeset:如果版本控制库的变更集合中包含一个或多个文件符合给定的Ant风格路径表达式,则执行
when {
    changeset "**/*.js"
}
  • equals:如果期望值与给定的值相同则执行
when { 
    equals expected: 2, actual: currentBuild.number 
}
  • branch: 当正在构建的分支与模式给定的分支匹配时,执行这个阶段,这只适用于多分支流水线例如:
when { branch 'master' }
  • environment 当指定的环境变量是给定的值时,执行这个步骤,例如:
when { environment name: 'DEPLOY_TO', value: 'production' }
  • expression 当指定的Groovy表达式评估为true时,执行这个阶段, 例如:
when { expression { return params.DEBUG_BUILD } }
when { 
    expression  { 
        return env.BRANCH_NAME != 'master'; 
    }
}

当表达式返回的是字符串时,它必须转换成布尔类型或null;否则,所有的字符串都被当作true处理。

  • buildingTag:如果pipeline所执行的代码被打了tag则执行
when { 
    buildingTag() 
} 
  • not 当嵌套条件是错误时,执行这个阶段,必须包含一个条件,例如:
when { not { branch 'master' } }
  • allOf 当所有的嵌套条件都正确时,执行这个阶段,必须包含至少一个条件,例如:
when { 
    allOf {
         branch 'master'; 
         environment name: 'DEPLOY_TO', value: 'production' 
    } 
}
  • anyOf 当至少有一个嵌套条件为真时,执行这个阶段,必须包含至少一个条件,例如:
when { anyOf { branch 'master'; branch 'staging' } }
  • tag:如果pipeline所执行的代码被打了tag,且tag名称符合规则,则执行
when { 
    tag "release-*" 
} 

如果tag的参数为空,即tag(),则表示不论tag名称是什么都执行,与buildingTag的效果相同。

tag条件支持comparator参数,支持的值如下。

EQUALS:简单的文本比较。

when { 
    tag pattern: "release-3.1", comparator: "EQUALS"
} 

GLOB(默认值):Ant风格路径表达式。由于是默认值,所以使用时一般省略。完整写法如下:

when { 
    tag pattern: "release—*", comparator: "GLOB"
}

PEGEXP:正则表达式。使用方法如下

when { 
    tag pattern: "release —\\d+", comparator: "REGEXP" 
}

示例:

pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                branch 'production'
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}
pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                branch 'production'
                environment name: 'DEPLOY_TO', value: 'production'
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}
pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                allOf {
                    branch 'production'
                    environment name: 'DEPLOY_TO', value: 'production'
                }
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}
pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                branch 'production'
                anyOf {
                    environment name: 'DEPLOY_TO', value: 'production'
                    environment name: 'DEPLOY_TO', value: 'staging'
                }
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}
pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                expression { BRANCH_NAME ==~ /(production|staging)/ }
                anyOf {
                    environment name: 'DEPLOY_TO', value: 'production'
                    environment name: 'DEPLOY_TO', value: 'staging'
                }
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}
pipeline {
    agent none
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            agent {
                label "some-label"
            }
            when {
                beforeAgent true
                branch 'production'
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

Generic Webhook Trigger插件在多分支pipeline

在多分支pipeline场景下,我们希望触发某个分支的构建执行,GenericTrigger可以这么传参:

triggers { 
    GenericTrigger( 
        genericVariables: [ 
            [key: 'ref', value: '$.ref'] 
        ], 

        token: env.JOB_NAME, 
        regexpFilterText: '$ref', 
        regexpFilterExpression: 'refs/heads/' + env.BRANCH_NAME
        } 
}

指令 1-5-8 Examples

pipeline {
    agent { node { label "hostmachine"   //指定运行节点的标签或者名称
            }
    }
parameters {
    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
}
stages {
    stage('parameter') {
        when { environment name: 'PERSON', value: 'jacob' }
        steps {
            echo "Hello ${params.PERSON}"
        }
    }

    stage('tools') {
        steps {
            script{
                mvnHome = tool "m2"
                println(mvnHome)

                sh "${mvnHome}/bin/mvn --version"
            }
        }
    }

    stage('Input') {
            input {
                message "Should we continue?"
                ok "Yes, we should."
                submitter "Jacob"
                parameters {
                    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
                }
            }
            steps {
                echo "Hello, ${PERSON}, nice to meet you."
            }
        }
}
}

Alt Image Text which is not jacob

when { environment name: 'PERSON', value: 'jacob' } 跳过此Stage

Alt Image Text

1-5-9 并行构建

声明式流水线的阶段可以在他们内部声明多隔嵌套阶段, 它们将并行执行。 注意,一个阶段必须只有一个 stepsparallel的阶段。 嵌套阶段本身不能包含 进一步的 parallel 阶段, 但是其他的阶段的行为与任何其他 stageparallel 的阶段不能包含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"
                    }
                }
            }
        }
    }
}

Jenkins pipeline插件支持这种并行构建,并月一使用起来也非常简单。

pipeline{ 
    agent none 
    stages{ 
        stage('Run Tests'){ 
            failFast true 
            parallel{ 
                stage('Test On Chrome'){ 
                    agent{ label "chrome" } 
                    steps{ 
                        echo "Chrome U工测试"
                    } 
                } 
                stage('Test On Firefox'){ 
                    agent{ label "firefox" } 
                    steps{ 
                        echo "Firefox UI测试" 
                        } 
                } 
                stage('Test On IE'){ 
                    agent{ label "ie" } 
                    steps{ 
                        echo "IE UI 测试“ 
                    } 
                } 
            } //end of parallel 
        } //end of run tests 
    } //end of stages 
} //end of pipeline 

stage部分包含一个Run Tests阶段,在这个阶段下包含一个parallel块,在parallel块下又包含了多个阶段。位于parallel块下的阶段都将并行执行,而且并行阶段还可以被分到不同的Jenkins agent上执行。

因为parallel本身不包含任何步骤,所以在parallel块下本身不允许包含agenttools.

在默认情况下,Jenkins pipeline要等待parallel块下所有的阶段都执行完成,才能确定结果。如果希望所有并行阶段中的某个阶段失败后,就让其他正在执行的阶段都中止,那么只需要在与parallel块同级的位置加人failFast true就可以了。

在不同的分支上应用并行构建

pipeline { 
    agent any 
    stages { 
        stage('Parallel Stage') { 
            failFast true 
            parallel { 
                stage('Branch master') { 
                    when { branch 'master' } 
                    agent any 
                    steps { 
                        echo "On Branch master" 
                    } 
                }
                stage('Branch dev') {
                    when { branch 'staging' }
                    agent any
                    stages {
                        stage('嵌套staging 1'){
                            steps{
                                echo "staging 1"
                            }
                        }
                        stage('嵌套staging 2'){
                            steps{
                                echo "staging 2"
                            }
                        }
                    }
                } // end of staging
            } // end of parallel
        }
    }
} //end of pipepline 

我们注意到在并行阶段Branch staging下又出现了一个stage部分。 是的,阶段是可以嵌套的。

并行步骤

前面我们介绍的是阶段级别的并行执行Jenkins pipeline还支持步骤级别的并行执行。这也是Jenkins Pipeline最早支持的并行方式

stage('parallel build') { 
    steps { 
        parallel {
            jdk8: {
                echo "jdk8 build" 
            },
            jdk9: {
                echo "jdk9 build" 
            }
        }
    }
}

1-6 step步骤

1-6-1 script

script 步骤需要 [scripted-pipeline]块并在声明式流水线中执行。对于大多数用例来说,应该声明式流水线中的“脚本”步骤是不必要的,但是它可以提供一个有用的”逃生出口”。非平凡的规模和/或复杂性的script块应该被转移到 共享库 。

示例:比如分别在不同的浏览器上跑测试

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'

                script {
                    def browsers = ['chrome', 'firefox']
                    for (int i = 0; i < browsers.size(); ++i) {
                        echo "Testing the ${browsers[i]} browser"
                    }
                }
            }
        }
    }
}

1-7 pipeline内置基础步骤

1-7-1 文件目录相关步骤

deleteDir:删除当前目录

deleteDir是一个无参步骤,删除的是当前工作目录。通常它与dir步骤一起使用,用于删 除指定目录下的内容。

dir:切换到目录

默认pipeline工作在工作空间目录下,dir步骤可以让我们切换到其他目录。使用方法如下:

dir("/var/logs"){
    deleteDir()
}

fileExists:判断文件是否存在

fileExists('/tmp/a.jar') 判断/tmp/a.jar文件是否存在。如果参数是相对路径,则判断在相对当前工作目录下,该文件是否存在。结果返回布尔类型。

isUnix:判断是否为类UNIX系统

如果当前pipeline运行在一个类UNIX系统上,则返回true

pwd:确认当前目录

pwdLinuxpwd命令一样,返回当前所在目录:它有一个布尔类型的可选参数:tmp,如 果参数值为true,则返回与当前工作空间关联的临时目录。

writeFile:将内容写入指定文件中

writeFile支持的参数有:

  • file:文件路径,可以是绝对路径,也可以是相对路径。
  • text: 要写人的文件内容。
  • encoding(可选):目标文件的编码。 如果留空,则使用操作系统默认的编码。如果写的是 Base64的数据,则可以使用Base64 编码。

readFile:读取文件内容

读取指定文件的内容,以文本返回。readFile支持的参数有:

  • file:路径,可以是绝对路径,也可以是相对路径:
  • encoding(可选):读取文件时使用的编码。

示例如下:

script{ 
    // "amVua2lucyBib29r" 是 "jenkins book" 进行Base64编码后的值
    writeFile(file:"base64File", text: "amVua2lucyBib29r", encoding: "Base64") 
    def content = readFile(file: 'base64File', encoding: 'UTF—8' ) 
    echo "${content}" 
    // 打印结果: jenkins book 
}

1-7-2 制品相关步骤

stash:保存临时文件

stash步骤可以将一些文件保存起来,以便被同一次构建的其他步骤或阶段使用。如果整个 pipeline的所有阶段在同一台机器上执行,则stash步骤是多余的。所以,通常需要stash的文件都是要跨Jenkins node使用的。

stash步骤会将文件存储在tar文件中,对于大文件的stash操作将会消耗Jenkins master的计算资源。Jenkins官方文档推荐,当文件大小为5~1OOMB,应该考虑使用其他替代方案。

stash步骤的参数列表如下:

  • name:字符串类型,保存文件的集合的唯一标识。
  • allowEmpty:布尔类型,允许stash内容为空。
  • excludes:字符串类型,将哪些文件排除。如果排除多个文件,则使用逗号分隔。留空代表 不排除任何文件。
  • includes:字符串类型,stash哪些文件,留空代表当前文件夹下的所有文件。
  • useDefaultExciudes:布尔类型,如果为true,则代表使用Ant风格路径默认排除文件列表。

除了name参数,其他参数都是可选的。excludesincludes使用的是Ant风格路径表达式。

unstash:取出之前stash的文件

unstash步骤只有一个name参数,即stash时的唯一标识。通常stashunstash步骤同时使用。以下是完整示例。

pipeline { 
    agent none 
    stages {
        stage('stash') { 
            agent { label "master" } 
            steps { 
                writeFile file: "a.txt",text: "$BUILO_NUMBER" 
                stash(name "abc", includes: "a.txt")
                }
            }   
        stage('unstash') { 
            agent { label "node2" }
            steps {
                script{ 
                    unstash("abc") 
                    def content = readFile("a.txt") 
                    echo "${content}" 
                }
            }
        }
    }
}

stash步骤在master节点上执行,而unstash步骤在 node2节点上执行

1-7-3 命令相关步骤

与命令相关的步骤其实是Pipeline: Nodes and processes插件提供的步骤。 由于它是Pipeline 插件的一个组件,所以基本不需要单独安装。

sh:执行shell命令

sh步骤支持的参数有:

  • script:将要执行的shell脚本,通常在类UNIX系统上可以是多行脚本。
  • encoding:脚本执行后输出日志的编码,默认值为脚本运行听在系统的编码。
  • return Status:布尔类型, 默认脚本返回的是状态码, 如果是一个非零的状态码,则会引发 pipeline执行失败。如果returnStatus参数为true,则不论状态码是什么,pipeline的执行都不会受影响。
  • return Stdout:布尔类型,如果为true,则任务的标准输出将作为步骤的返回值,而不是打印到构建日志中(如果有错误,则依然会打印到日志中)。

除了script参数,其他参数都是可选的。

returnStatusreturnStdout参数一般不会同时使用,因为返回值只能有一个。如果同时使用、 则只有returnStatus参数生效。

bat、powershell步骤

bat步骤执行的是Windows的批处理命令。powershell步骤执行的是PowerShell脚本,支持3+版本。这两个步骤支持的参数与sh步骤的一样,这里就不重复介绍了。

1-7-4 其他步骤

error:主动报错,中止当前pipeline

error步骤的执行类似于抛出一个异常。它只有一个必需参数:message。通常省略参数: error("there's an error")

tool:使用预定义的工具

如果在Global Tool Configuration(全局工具配置)中配置了工具,如图所示,比如 配置了Docker,那么可以通过tool步骤得到工具路径。

Alt Image Text

stage('Docker') {
            steps { 
                script{ 
                    def t = tool name: 'docker', type: 'org.jenkinsci.plugins.docker.commons.tools.DockerTool'
                    echo "${t}" // 打印 /var/lib/docker 
                } 
            }
        }
...
/var/lib/docker
[Pipeline] }
[Pipeline] // script
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline

tool步骤支持的参数有:

  • name:工具名称。
  • type(可选):工具类型,指该工具安装类的全路径类名。

每个插件的type值都不一样,而且绝大多数插件的文档根本不写type值。除了到该插件的源码中查找,还有一种方法可以让我们快速找到type值,就是前往Jenkins pipeline代码片段生成器中生成该tool步骤的代码即可,

Alt Image Text

timeout:代码块超时时间

timeout步骤闭包内运行的代码设置超时时间限制。一如果超时,将抛出一个org.jenkinsci.plugins.workflow. steps.FlowlnterruptedException异常。timeout步骤支持如下参数:

  • time:整型, 超时时间。
  • unit(可选):时间单位,支持的值有NANOSECONDSMICROSECONDSMILLISECONDSSECONDSMINUTES(默认)、HOURSDAYS
  • activity(可选):布尔类型,如果值为true,则只有当日志没有活动后,才真正算作超时

waitUntil:等待条件满足

不断重复waitUntil块内的代码,直到条件为true. waitUntil不负责处理块内代码的异常,直到异常时直接向外抛出。waitUntil步骤最好与timeout步骤共同使用,避免死循环。示例如下:

timeout(50) { 
    waitUntil { 
        script { 
            def r = sh script: 'curl http://exmaple', returnStatus: true 
            return (r == 0) 
        } 
    } 
} 

retry:重复执行块

执行N次闭包内的脚本。如果其中某次执行抛出异常,则只中止本次执行,并不会中止整个retry的执行。同时,在执行retry的过程中,用户是无法中止pipeline的。

steps{ 
    retry(20){ 
        script{ 
        sh script: 'curl http://exmaple', returnStatus: true
        } 
    } 
} 

sleep:让pipeline休眠一段时间

sleep步骤可用于简单地暂停pipeline,其支持的参数有:

  • time:整型,休眠时间。
  • unit(可选):时间单位,支持的值有NANOSECONDSMICROSECONDSMILLISECONDSSECONDS(默认)、MINUTESHOURSDAYS

示例如下:

sleep(120) // 休眠120s
sleep(time:'2', unit:"MINUTES") // 休眠2分钟 

1-7-5 使用pipeline代码片段生成器学习

对于初学Jenkins pipeline的新人来说,如何开始写pipeline是一个坎儿。好在Jenkins提供了

一个pipeline代码片段生成器,通过界面操作就可以生成代码。

进入pipeline项目后,单击左边的“Pipeline Syntax”菜单项(只有pipeline项目有)图3-6所示。

进入“Pipeline Syntax"页面后,在右边的“Sample Step”下拉框中选择需要生成代码的步骤,并根据提示填入参数,然后单击“Generate Pipeline Script”按钮,就可以生成代码了,如

Alt Image Text

1-7-6 使用VS Code扩展校验Jenkinsfile

不像Java语言有各种开发工具支持,Jenkinsfile 从诞生以来就没有很好的工具支持, 无奈只能使用VSCode文本编辑器+Groovy语法高亮进行开发。对语法的校验全凭自己对Jenkinsfile的了解

2018年11月初,Jenkins官方博客介绍了一个VS Code扩展:Jenkins Pipeline Linter Connector, 实现了对Jenkinsfile的语法校验。

VS Code应用市场搜索“Jenkins Pipeline Linter Connector”并安装,然后对一该扩展进行设置,

Alt Image Text

然后,进人JenkinsManage Jenkins->Manage Configure Global Security页,确认Jenkins 启用了“CSRF Protection"

Alt Image Text

接下来,打开一个Jenkinsfile文件,调用扩展命令,

vscode shift + command + p

Alt Image Text

最后在OUTPUT中可以看利校验结果

Alt Image Text

使用Workspace Cleanup插件理空间

通常, 当pipeline执行完成后,不会自动清理空间如果需要(通常需要)清理工作空间,则可以通过Workspace Cleanup插件实现

  • 安装Workspace Cleanup插件
  • pipelinepost部分加入插件步骤
post {
    always {
        cleanWs()
    }
}

1-7-7 Ant风格路径表达式简介

Ant是比Maven更老的Java构建工具。Ant发明了一种描述文件路径的表达式,大家都习惯称其为Ant风格路径表达式。 Jenkins pipeline的很多步骤的参数也会使用此表达式。

Ant路径表达式包括3种通配符。

  • :匹配任何单字符。
  • *:匹配0个或者任意数量的字符。
  • **:匹配0个或者更多的目录。

我们通过以下例子来学习。

  • **/cvs/*:匹配cvs文件夹下的所有文件,cvs文件夹可以在任何层级。

以下路径会被匹配到:

CVS/Repository 
org/apache/CVS/Entries 
org/apache/jakarta/tools/ant/CVS/Entries 

以下foo/bar部分不会被匹配到

org/apache/CVS/foo/bar/Entries 
  • org/apache/jakarta/**:匹配org/apache/jakarta路径下的所有文件。

以下路径会被匹配到:

org/apache/jakarta/tools/ant/docs/index.html 
org/apache/jakarta/test.xml 

以下路径不会被匹配到:

org/apache/xyz.java
  • org/apache/**/CVS/*:匹配org/apache路径下的cvs文件夹下的所有文件。cvs文件夹可以在任何层级。

以下路径会被匹配到:

org/apache/CVS/Entries 
org/apache/jakarta/tools/ant/CVS/Entries 

以下路径不会被匹配到:

org/apache/CVS/foo/bar/Entries 
  • **/test/**: 匹配所有路径中含有test的路径

How to check Jenkins DSL Viewer Page

Input the url of Viewer Page

htpp://jenkins_url/plugin/job-dsl/api-viewer/index.html

For example:

http://192.168.33.11:8080/plugin/job-dsl/api-viewer/index.html

Alt Image Text