Jenkins Pipeline
一、什么是 Pipeline?
很多项目中都有 pipeline 的概念,比如:redis,在 redis 中 pipeline 代表一组操作,我们通过将一组操作合并为 pipeline 使得多个操作在一次网络交互上完成,这在可以有效的提升性能,尤其是网络延迟较大的场景
话说回来,Jenkins 中 Pipeline 又是什么意思呢?
其实也差不太多,也是用来封装了一系列操作,通过 pipeline 我们可以完成从简单到复杂的交付流水线实现,引用《持续交付——发布可靠软件的系统方法》中的一句话
“部署流水线(Deployment pipeline)是指从 软件版本控制库 到 用户 手中这一过程的自动化表现形式。”
二、为什么使用 Pipeline?
回答这个问题前,我们思考下,我们为什么会选择 Jenkins?为什么不人肉运维,手工操作?
答案不难回答,一、效率 二、标准化
效率的重要性自不必多言,自动化一切重复性的工作,这是我们所应该追求的,标准化的意义,只有吃过苦头的人才能认识的深刻
现在,我们回过头来再看这个问题,为什么使用 pipeline?
最主要的原因是 pipeline 的特点完美贴合了我们需求,Jenkins 中 pipeline 是通过 Jenkinsfile 编写实现的,文件,这意味着什么?
规范、版本控制,Right?喏,标准化的问题解决了
除此外,流水线还具有 代码化、耐用性、可暂停的、可扩展性 等特性
- 代码化:流水线是在代码中实现的,可以通过 Git 等代码库进行版本控制,一次追踪变更,提高团队协作效率
- 耐用性:流水线可以从 Jenkins 的 master 节点重启后继续运行
- 可暂停的:流水线可以在执行过程中设置“暂停”,等待特定用户“批准”后继续运行
- 解决复杂发布:支持复杂的交付流程,例如循环、并行执行
- 可扩展性:支持扩展 DSL 和其他各类插件集成
- 可重用性:手动操作没法重用,但是代码可以重用
喏,效率的问题也得到了保障,并且还支持各种各样的丰富特性,所以为什么不选 Pipeline 呢?所以,放弃手动操作,全面依赖 Pipeline as Code 吧!
三、什么是 Jenkinsfile?
Jenkinsfile 中包含了 Pipeline 的所有执行逻辑,Jenkinsfile 之于 Jenkins Pipeline 如同 Dockerfile 之于 Docker Image,前者是用来描述 Pipeline,后者是用来描述 Image
Jenkins 默认是不支持 Jenkinsfile 的。我们需要安装 pipeline 相关插件再进行使用,pipeline 的书写语法分为两种:声明式、脚本式,两者从根本上是不同的
- 声明式:Jenkins 流水线更友好,书写更简单,上手更快,并且声明式流水线支持嵌套 脚本式 的代码
- 脚本式:支持更丰富的语法,灵活度更高,但相对来说书写麻烦
通常来说基本是以 “声明式为主,脚本式为辅”,所以,对于学习者而言,两种都要牢牢掌握
Jenkins 团队在一开始实现 pipeline 时选择 Groovy 语言作为基础,所以起初写 pipeline 其实就是在写 Groovy 脚本,不过对于不使用Groovy 的开发团队来说,脚本式(Scripted)Groovy 语法,无形中增加了不必要的学习成本。
所以,为了降低 Jenkins 使用者的学习成本,Jenkins 团队基于 Groovy 设计出一种更简单、结构化的语法,声明式(Declar-ative)语法,由于声明式语法更符合人类的阅读习惯,无论是社区还是业内实践都优先选择其为主要书写语法
3.1 声明式(Declarative Pipeline)
声明式流水线依赖插件 Declarative
3.1.1 流水线组成(Sections)
首先,所有声明式流水线必须包含在一个 pipeline
块中
pipeline {
/* insert Declarative Pipeline here */
}
pipeline 块内主要由 4 部分组成,示例代码如下:
String workspace = "/opt/jenkins/workspace"
pipeline {
// 一、流水线运行节点及工作目录
agent {
node {
// 声明流水线作业必须运行在 包含特定标签 或 名称 的节点
label "build-server-aliyun-ecs-01"
// 指定流水线运行工作目录
customWorkspace "${workspace}"
}
}
// 二、流水线运行时选项
options {
// 作业日志输出中包含日期时间
timestamps()
// 当使用声明式编写 Jenkinsfile 时,会默认检查项目是否配置代码库,若配置了,默认会自动拉取
// 不过,通常我们都会选择跳过默认 checkout scm 语句,自己在执行阶段中手动拉取代码
skipDefaultCheckout()
// 禁止流水线并行运行,根据流水线使用场景选择是否
disableConcurrentBuilds()
// 设置流水线运行超时
timeout(time: 1, unit: 'HOURS')
}
// 三、流水线执行阶段
stages {
// 拉取代码
stage("GetCode"){ // 阶段名称
// 第一个步骤
steps {
timeout(time: 5, unit: "MINUTES"){
// 嵌套脚本式代码
script {
println('拉取项目代码.')
}
}
}
}
// 构建应用
stage("Build"){
// 第一个步骤
steps {
timeout(time: 10, unit: "MINUTES"){
script {
println('构建打包应用.')
}
}
}
}
// 代码扫描
stage("CodeScan"){
// 第一个步骤
steps {
timeout(time: 20, unit: "MINUTES"){
script {
println('扫描项目代码.')
}
}
}
}
}
// 四、流水线构建后操作
post {
// 当流水线构建成功后执行
// 通常伴随着一个 http 请求,通知任务调度系统 流水线执行成功
success {
script {
// currentBuild 全局变量
// currentBuild.description 构建描述
currentBuild.description = "构建成功!"
}
}
// 当流水线构建失败后执行
// 通常伴随着一个 http 请求 或 邮件,通过 钉钉 或 邮件告知技术人员构建出现异常
failure {
script {
currentBuild.description = "构建失败!"
}
}
// 当流水线构建被取消时执行
aborted {
script {
currentBuild.description = "构建取消!"
}
}
// 不论流水线构建状态如何总会执行
always {
script {
println("流水线运行结束.")
}
}
}
}
agent 块
agent 块用于指定整个 流水线 或 特定 stage 的执行位置(节点),包含以下几个参数
参数 | 值 | 描述 |
---|---|---|
any | 随机选择可用的代理(节点)运行流水线 | |
none | 当在 pipeline 顶层块 agent 为 none 时,不分配全局的代理,各 stage 自己去定义 agent 代理 |
|
label | agent { label 'my-defined-label' } |
在包含 特定标签 或 名称的代理(节点)运行流水线 |
node | agent { node { label 'labelName' } } |
与 label 类似,但允许额外的选项,如:customWorkspace |
docker | 使用给定的容器执行流水线或阶段 | |
dockerfile | 基于源代码库包含的 Dockerfile 构建的容器执行 流水线 或 阶段 |
前面几个不难理解,主要是 none、docker、dockerfile 我们看几个例子
docker 示例
pipeline 代码
pipeline {
agent { docker 'nginx:latest' }
stages {
stage('Example') {
steps {
sh 'nginx -v'
}
}
}
}
执行效果
dockerfile 示例
dockerfile 依赖一个 Git 仓库,并配合 Jenkinsfile 一起使用,具体使用方式如下
仓库结构
Dockerfile
FROM node:lts-alpine
RUN apk add -U subversion
Jenkinsfile
pipeline {
agent { dockerfile true }
stages {
stage('Test') {
steps {
sh 'node --version'
sh 'svn --version'
}
}
}
}
执行构建
options 块
官方把它放到“指令”那类,我个人从书写的角度归为 pipeline 的组成块之一
options 用于配置流水线的选项,它的作用域可以是 pipeline 或 stages,主要包括以下配置项
配置项 | 描述 | 示例 |
---|---|---|
buildDiscarder | 保存多少构建历史记录及构建制品 | options { buildDiscarder(logRotator(numToKeepStr: '1')) } 保留 1 份构建历史options { buildDiscarder(logRotator(daysToKeepStr: '7')) } 保留一周内的构建历史buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '1')) 保留 10 份构建历史 及 一份构建制品buildDiscarder(logRotator(daysToKeepStr: '7', artifactDaysToKeepStr: '7')) 保留一周内的构建历史及构建制品 |
disableConcurrentBuilds | 禁止流水线并行运行 | options { disableConcurrentBuilds() } |
overrideIndexTriggers | 允许覆盖分支索引触发器的默认处理 | options { overrideIndexTriggers(true) } |
skipDefaultCheckout | 跳过默认源代码检出动作 | options { skipDefaultCheckout() } |
skipStagesAfterUnstable | 当构建状态为 UNSTABLE 跳过该阶段 |
options { skipStagesAfterUnstable() } |
checkoutToSubdirectory | 设置 checkout 源代码到指定目录(工作目录下) | options { checkoutToSubdirectory('foo') } |
timeout | 流水线(或 stages)运行的超时时间 | options { timeout(time: 1, unit: 'HOURS') } |
retry | 失败重试次数 | options { retry(3) } |
timestamps | 日志输入加上时间 | options { timestamps() } |
stages 块
stages 是 pipeline 最核心最重要的段落,我们所有工作都是通过在 stages
中定义完成,一个 stages
至少包含一个 stage 指令用于连续交付过程的每个离散部分,如 检出代码、构建、测试、部署等
示例如下:
pipeline {
agent any
stages {
// stages 部分也可以定义 agent, options 等的指令
stage('阶段1') {
steps {
echo 'Hello'
}
}
stage('阶段2') {
steps {
echo 'World'
}
}
}
}
post 块
post 块中可以定义一个或多个 steps,这些 staps 步骤会在根据流水线或阶段的完成情况而运行,主要包括以下几种 post-condition
条件 | 描述 |
---|---|
success | 当流水线构建成功时,才允许在 post 部分运行该步骤 |
failure | 当流水线构建失败时,才允许在 post 部分运行该步骤 |
aborted | 当流水线构建被取消时,才允许在 post 部分运行该步骤 |
changed | 当流水线完成状态与之前运行不同时才会执行(避免重复构建制品?) |
unstable | 当流水线完成状态为 UNSTABLE 时,才允许在 post 部分运行该步骤 |
always | 无论是怎样都执行 |
- 声明运行选项 options
- 声明执行阶段 stages → stage → steps
- 声明构建后操作 Post
了解完 声明式 Jenkinsfile 组成后,我们看下常用的指令
3.1.2 常用指令(Directives)
environment
定义环境变量,变量作用域 取决于 声明位置,该指令提供 credentials()
方法,用于访问内置的凭证
当凭证类型为 Secret Text 环境变量为文本内容
当凭证类型为 SStandard username and password,Jenkins 自动创建 username:password 变量,并且额外会将用户名密码存入
<VAR_NAME>_USR
和<VAR_NAME>_PSW
环境变量中
示例代码:
pipeline {
agent any
environment {
AN_ACCESS_KEY = credentials('my-secret')
AN_UP = credentials('jenkins-admin-user')
}
stages {
stage('定义打印变量') {
environment {
project_name = 'web-demo1'
}
steps {
script {
println("Secret Text:${AN_ACCESS_KEY}")
println("Username:${AN_UP_USR} Password: ${AN_UP_PSW}")
println("Project: ${project_name}")
}
}
}
}
}
执行效果
Started by user lotusching
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/lib/jenkins/jobs/产品线-A/jobs/demo-01/workspace
[Pipeline] {
[Pipeline] withCredentials
Masking supported pattern matches of $AN_UP or $AN_UP_PSW or $AN_ACCESS_KEY
[Pipeline] {
[Pipeline] stage
[Pipeline] { (定义打印变量)
[Pipeline] withEnv
[Pipeline] {
[Pipeline] script
[Pipeline] {
[Pipeline] echo
Secret Text:****
[Pipeline] echo
Username:**** Password: ****
[Pipeline] echo
Project: web-demo1
[Pipeline] }
# ...
Finished: SUCCESS
parameters
parameters 用于定义用户在触发流水线时需要传递进来的参数,用户可以在 params
对象在流水线步骤中操作使用,支持以下参数类型
类型 | 描述 | 示例 |
---|---|---|
string | 字符串类型参数 | parameters { string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '') } |
text | 文本类型参数,支持多行 | parameters { text(name: 'DEPLOY_TEXT', defaultValue: 'One\nTwo\nThree\n', description: '') } |
booleanParam | 布尔类型参数,true 或 false | parameters { booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '') } |
choice | 选择类型参数,提供几个选项供你选择 | parameters { choice(name: 'CHOICES', choices: ['one', 'two', 'three'], description: '') } |
password | 密码类型参数,不回显明文 | parameters { password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'A secret password') } |
示例如下:
pipeline {
agent any
parameters {
string(name: 'USERNAME', defaultValue: 'Da', description: '用户名')
text(name: 'REMARK', defaultValue: '', description: '备注')
booleanParam(name: 'BACKUP', defaultValue: true, description: '执行前是否备份')
choice(name: 'ENV_CHOICE', choices: ['development', 'stage', 'production'], description: '选择执行环境')
password(name: 'PASSWORD', defaultValue: 'SECRET', description: '输入密码')
}
stages {
stage('Example') {
steps {
echo "执行用户:${params.USERNAME}"
echo "执行备注: ${params.REMARK}"
echo "是否备份: ${params.BACKUP}"
echo "执行环境: ${params.ENV_CHOICE}"
echo "密码: ${params.PASSWORD}"
}
}
}
}
Pipeline WebUI 效果(默认)
补全参数
执行构建
triggers
triggers 用于定义流水线何时会被触发运行,对于已经与 Gitlab 等源实现集成的场景,可以不进行 triggers 配置
Jenkins 支持三种触发器
触发器 | 描述 | 示例 |
---|---|---|
cron | 基于 cron 表达式触发执行流水线 | triggers { cron('H */4 * * 1-5') } ,更多语法规则 |
pollSCM | 基于 cron 表达式触发执行 poll 代码 | triggers { pollSCM('H */4 * * 1-5') } |
upstream | 基于指定上游 job 执行的状态判断是否触发执行 | triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS) } triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.FAILURE) } triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.UNSTABLE) } triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.completeBuild) } triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.NOT_BUILT) } |
下面我们看个例子,上游 job 自动通过 cron 触发,下游 job 通过上游执行状态触发,话不都说,开整
首先,创建一个 pipeline 命名为 trigger-upstream-job1,内容如下:
pipeline {
agent any
triggers {
cron('* * * * *')
}
stages {
stage('upstream-job1') {
steps {
echo "我是上游!"
}
}
}
}
接着,创建下游 pipeline job 命名为 trigger-upstream-job2
pipeline {
agent any
triggers {
upstream(upstreamProjects: 'trigger-upstream-job1', threshold: hudson.model.Result.SUCCESS)
}
stages {
stage('upstream-job2') {
steps {
echo "我是下游!"
}
}
}
}
需要注意一点,包含 cron 触发器的 pipeline job 需要先手动执行,以此将 计划任务注册上去(设计问题)
最终效果,trigger-upstream-job1
trigger-upstream-job2
tools
tools 用于定义辅助工具,如 maven
、jdk
等,如果 agent none
则忽略该操作
使用前需要保证已经将相关工具配置好,路径:配置中心 -> Global Tool Configuration
pipeline {
agent any
tools {
// 工具名称 配置中心中定义的工具名称
jdk 'Openjdk 11.0.13'
maven 'M2'
dockerTool 'docker 20.10.5'
groovy 'groovy3.0'
}
stages {
stage('ENV') {
steps {
sh 'printenv'
}
}
stage('JDK tool') {
steps {
sh 'java -version'
}
}
stage('Maven tool') {
steps {
sh 'mvn --version'
}
}
stage('Docker tool') {
steps {
sh 'docker -v'
}
}
stage('Ansible tool') {
steps {
sh 'ansible --version'
}
}
stage('Groovy version') {
// 经过几番尝试始终是用的默认的 groovy,无法使用已配置 groovy 3.0
// 相关讨论 https://issues.jenkins.io/browse/JENKINS-51823
tools {
groovy 'groovy3.0'
}
steps {
script {
println(GroovySystem.version)
}
}
}
}
}
input
input 用于提示在流水线运行到某一阶段时根据用户输入做后续逻辑处理,包括一下参数
- message:必需的,这将在用户提交时显示给用户
input
- id:可选标识符
input
,默认为stage
名称 - ok:
input
表单上“确定”按钮的可选文本 - submitter:可选的逗号分隔列表,这些列表允许用户提交此 用户 或 外部组名
input
。默认为允许任何用户 - submitterParameter:环境变量的可选名称,用该
submitter
名称设置(如果存在) - parameters:提示提交者提供的可选参数列表
示例如下:
pipeline {
agent any
stages {
stage('input-demo') {
input {
message "是否继续?"
ok "继续"
// 允许执行继续的 jenkins 用户,拥有 Administer 权限的管理员用户不受限
submitter "lotusching"
parameters {
string(name: 'OPREATOR', defaultValue: 'Da', description: '输入操作者用户名')
}
}
steps {
echo "开始执行,${OPREATOR} 批准运行!"
}
}
}
}
when
when 主要用来做条件判断,支持一或多个条件,当(所有)条件为 True 时 Step 才会执行,Jenkins Pipeline 包含一些内置 Conditions,如下所示
Conditions | 描述 | 示例 |
---|---|---|
branch | 如果本次 Git 分支 与 参数设置的 branch 对应时,执行 stage,仅适用于 multi-branch pipeline | when { branch 'master' } when { branch pattern: "release-\\d+", comparator: "REGEXP"} |
tag | 如果本次 SCM 包含特定标识,则执行 stage | when { tag "release-*" } when { tag pattern: "release-\\d+", comparator: "REGEXP"} |
changelog | 如果本次 SCM changelog 包含特定内容,则执行 stage | when { changelog '.*^\\[DEPENDENCY\\] .+$' } |
changeset | 如果本次 SCM 变更列表 包含特定文件,则执行 stage | when { changeset "**/*.js" } when { changeset pattern: " */*TEST.java", caseSensitive: true } when { changeset pattern: ".TEST\\.java", comparator: "REGEXP" } |
changeRequest | 如果本次 SCM PR(MR)符合过滤条件,则执行 stage | when { changeRequest target: 'master' } when { changeRequest authorEmail: "[\\w_-.]+@example.com", comparator: 'REGEXP' } |
environment | 如果 特定环境变量 为 指定内容 时,则执行 stage | when { environment name: 'DEPLOY_TO', value: 'production' } |
equals | 等值判断,当 actual 等于 equals expected 时执行 stage,可以配合 not 做非判断 | when { equals expected: 2, actual: currentBuild.number } when { not { equals expected: 2, actual: currentBuild.number } } |
allOf | 条件判断,当所有条件都为 true 时执行 stage | when { allOf { branch 'master'; environment name: 'DEPLOY_TO', value: 'production' } } |
anyOf | 条件判断,任何一个条件为 true 便执行 stage | when { anyOf { branch 'master'; branch 'staging' } } |
expression | 布尔判断,当 expression 返回为 true时执行 stage,0、null 都视为 false | when { expression { return params.DEBUG_BUILD } } |
not | 非判断,当嵌套条件为 false 时执行 stage | when { not { branch 'master' } } |
triggeredBy | 触发源判断,如果本次 build 是被指定 triggers 触发,则运行 stage | when { triggeredBy 'SCMTrigger' }when { triggeredBy 'TimerTrigger' } when { triggeredBy 'BuildUpstreamCause' } when { triggeredBy cause: "UserIdCause", detail: "vlinde" } |
默认情况下,input、options、agent 指令优先级大于 when,不过我们可以通过参数 beforeInput(beforeAgent、beforeOptions) true
手动调整为 when -> input
3.2.3 并行
parallel
Pipeline 中通常会定义多个 stages、steps,我们可以通过 parallel
实现将多个 stage 分布到各个 agent node 并行运行,以此加快流水线的执行速度
我们先看下具体使用
pipeline {
agent any
options {
timestamps()
}
stages {
stage('代码拉取') {
steps {
echo '拉取最新代码.'
}
}
stage('代码集成') {
parallel {
stage('编译打包') {
agent {
label "built-in"
}
steps {
echo "【Parallel-A】我在编译打包最新代码."
}
}
stage('代码扫描') {
agent {
label "build-server-aliyun-ecs-01"
}
steps {
echo "【Parallel-B】我在扫描最新代码."
}
}
stage('集成测试') {
agent {
label "build-server-aliyun-ecs-01"
}
steps {
echo "【Parallel-C】我在测试最新代码."
}
}
}
}
stage('应用部署') {
steps {
echo '部署更新应用.'
}
}
}
}
执行效果
failFast
failFast true
与 options { parallelsAlwaysFailFast() }
的作用在于当多个 stage 或
step 并行运行时,只要有一个失败,其他的 stage、step 就会终结跳过
示例如下:
pipeline {
agent any
options {
timestamps()
parallelsAlwaysFailFast()
}
stages {
stage('代码拉取') {
steps {
echo '拉取最新代码.'
}
}
stage('代码集成') {
parallel {
stage('编译打包') {
agent {
label "built-in"
}
steps {
script {
sh "echo '【Parallel-A】我在编译打包最新代码.' && exit 1 "
}
}
}
stage('代码扫描') {
agent {
label "build-server-aliyun-ecs-01"
}
steps {
script {
sh "sleep 3 && echo '【Parallel-A】我在扫描最新代码.'"
}
}
}
stage('集成测试') {
agent {
label "build-server-aliyun-ecs-01"
}
steps {
script {
sh "sleep 3 && echo '【Parallel-A】我在测试最新代码.'"
}
}
}
}
}
stage('应用部署') {
steps {
echo '部署更新应用.'
}
}
}
}
执行效果
执行日志
3.2.4 逻辑控制
try/catch
try-catch 捕获异常、throw 抛出异常
示例如下:
pipeline {
agent any
stages {
stage('Example') {
steps {
script {
try {
sh 'echo "test try-catch" && exit 1'
}
catch (e) {
echo "捕获到了一个异常! 错误信息: ${e}"
// throw 抛出异常
throw new Exception("主动抛出一个异常")
}
}
}
}
}
}
if/else 与 switch
声明式 Pipeline 支持通过 scripts
引入 脚本式 语法,以此实现 if/else
或 switch
逻辑控制,如下所示:
String username = "Dayo"
pipeline {
agent any
stages {
stage('if-demo') {
steps {
script {
if ("${username}" == 'Da') {
echo '【if】欢迎你啊,Da!'
} else if ("${username}" == 'Yo') {
echo '【if】欢迎你啊,Yo'
} else {
echo "【if】当前用户:${username}"
}
}
}
}
stage('switch-demo') {
steps {
script {
switch("${username}"){
case "Da":
echo '【switch】欢迎你啊,Da!'
break; // 或者;; 前者跳过 default,后者执行 default
case "Yo":
echo '【switch】欢迎你啊,Yo!'
break;
case "Dayo":
echo '【switch】当前用户,Dayo!'
break;
default:
// 若 case 没有 break 则执行 default 内逻辑
// 处理逻辑
echo '【switch】匿名用户!'
}
}
}
}
}
}
四、创建流水线
4.1 传统方式
New Item → Pipeline → 输入 Pipeline 代码
4.2 Blue Ocean
Blue Ocean 是 Jenkins 为提升用户体验,从新开始设计的一套 WebUI,通过 Blue Ocean 可以让用户快速直观地理解管道状态
Blue Ocean 文档:https://www.jenkins.io/zh/doc/book/blueocean/getting-started/
浏览器 Blue Ocean 仪表板
安装 Blue Ocean 插件
Jenkins 需要安装 Blue Ocean 插件,实例版本需 Jenkins 2.7.x
浏览 Blue Ocean
图片创建后截取的,jenkins-demo1 便是后面通过 Blue Ocean 创建的
创建流水线
在创建流水线时,需要选择一个 Git 仓库,基于此创建我们的流水线项目,创建并填写 Git 仓库地址后,Blue Ocean 会扫描你仓库来获得 Jenkinsfile
,如果 Blue Ocean 找不到任何 Jenkinsfile
, 将提示你开始通过流水线编辑器创建一个
当我们使用 SSH 协议连接仓库时,Blue Ocean 会自动生成一个 SSH 公/私密钥对 (或提供已存在的一个) 作为登录代码仓库的用户
接下来,我们去到代码平台配置添加 SSH 密钥
点击创建流水线后,默认情况下会自动构建一次