Jenkins Pipeline #
为什么使用 pipeline?
freestyle 主要使用配置的方式来描述一个 job,刚上手的时候我也是使用这种方式来构建项目、发布。后续熟悉了之后一些其它的构建我使用了 pipeline 之后体会到了完全不同的顺畅。简单来说虽然 freestyle 的学习成本低,但是无法将配置代码化,在各种插件杂糅在 job 后,迁移和版本控制时会增加心智负担,配置的流程很长,各项间隔很远,而不像 pipeline 的形式,皆在一个 groovy 脚本中,上下文前后前后关联都可以一览无余。
其次是 pipeline 中可以定义多个 stage,来获得一些 freestyle 无法实现的行为,例如并行、人工批准、复用等,最终组合成 pipeline 集。综合上面的部分心得,最后我完全废弃 freestyle,仅使用 pipeline 来构建和发布项目。[1]
- https://plugins.jenkins.io/kubernetes/
- https://github.com/jenkinsci/kubernetes-plugin/blob/master/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/declarative.groovy
- https://github.com/jenkinsci/kubernetes-plugin/blob/master/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/jenkinsSecretHidden.groovy
- https://github.com/jenkinsci/kubernetes-plugin/tree/master/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline
- https://plugins.jenkins.io/kubernetes/
优化
其他
build info
- https://testerhome.com/topics/13511
- https://cloud.tencent.com/developer/article/2202789
- https://blog.csdn.net/weixin_39918388/article/details/112462275
k8s
- https://www.chenshaowen.com/blog/creating-jenkins-slave-dynamically-on-kubernetes.html
- https://www.cnblogs.com/cyleon/p/14894586.html
概念[2] #
先来看一个官方的声明式的 pipeline Jenkinsfile[3]:
pipeline {
  [1]
  agent any[2]
  stages {
    stage('Build') {
      [3]
      steps {
        [4]
        sh 'make'
      }
    }
    stage('Test') {
      steps {
        sh 'make check'
        junit 'reports/**/*.xml'
      }
    }
    stage('Deploy') {
      steps {
        sh 'make publish'
      }
    }
  }
}- [1]pipeline 定义了包含执行整个流水线的所有内容和指令的块。
- [2]agent 指示 Jenkins 为整个流水线分配一个执行器(在节点上)和工作区。
- [3]stage 可以理解为 pipeline 流程中的一个阶段,一个或多个阶段实现了整个 pipeline 的功能。例如上例中,pipeline 由构建(Build)、测试(Test)和发布(Deploy)构成。
- [4]steps 中可以描述每个 stage 需要运行的功能。例如上例中的测试(Test)stage 中,需要执行 shell 命令- make check,然后使用 junit 上报测试。
简单了解 pipeline 是大概是什么之后就可以学习 pipeline 的语法。
语法[4] #
agent #
agent
agent 部分指定了整个流水线或特定的部分,将会在 Jenkins 环境中执行的位置,这取决于 agent 区域的位置。该部分必须在 pipeline 块的顶层被定义,但是 stage 级别的使用是可选的。
参数:
- any 在任何可用的代理上执行流水线或阶段 
- none 当在 pipeline 块的顶部没有全局代理,该参数将会被分配到整个流水线的运行中并且每个 stage 部分都需要包含他自己的 agent 部分 
- label 在提供了标签的 Jenkins 环境中可用的代理上执行流水线或阶段 
- node - agent { node { label 'labelName' } }和- agent { label 'labelName' }一样,但是 node 允许额外的选项 ( 比如- customWorkspace)
- docker 使用给定的容器执行流水线或阶段。该容器将在预置的 node 上,或在匹配可选定义的 - label参数上,动态的供应来接受基于 Docker 的流水线。- docker也可以选择的接受- args参数,该参数可能包含直接传递到- docker run调用的参数,以及- alwaysPull选项,该选项强制- docker pull,即使镜像名称已经存在。比如:- agent { docker 'maven:3-alpine' }或- agent { docker { image 'maven:3-alpine' label 'my-defined-label' args '-v /tmp:/tmp' } }
- dockerfile 执行流水线或阶段,使用从源代码库包含的 - Dockerfile构建的容器。为了使用该选项,- Jenkinsfile必须从多个分支流水线中加载,或者加载“Pipeline from SCM.”通常,这是源代码仓库的根目录下的- Dockerfile : agent { dockerfile true }。 如果在另一个目录下构建- Dockerfile,使用 dir 选项:- agent { dockerfile {dir 'someSubDir' } }。如果- Dockerfile有另一个名称,你可以使用- filename选项指定该文件名。你可以传递额外的参数到- docker build ...使用- additionalBuildArgs选项提交,比如- agent { dockerfile {additionalBuildArgs '--build-arg foo=bar' } }。例如,一个带有- build/Dockerfile.build的仓库,期望一个构建参数- version:- agent { // Equivalent to "docker build -f Dockerfile.build --build-arg version=1.0.2 ./build/ dockerfile { filename 'Dockerfile.build' dir 'build' label 'my-defined-label' additionalBuildArgs '--build-arg version=1.0.2' } }
post #
post部分定义一个或多个 steps,这些阶段根据流水线或阶段的完成情况而 运行(取决于流水线中post部分的位置)。post支持以下 post-condition 块中的其中之一:always、changed、failure、success、unstable和aborted。这些条件块允许在post部分的步骤的执行取决于流水线或阶段的完成状态。
Conditions
- always无论流水线或阶段的完成状态如何,都允许在- post部分运行该步骤。
- changed只有当前流水线或阶段的完成状态与它之前的运行不同时,才允许在- post部分运行该步骤。
- failure只有当前流水线或阶段的完成状态为“failure”,才允许在- post部分运行该步骤。
- success只有当前流水线或阶段的完成状态为“success”,才允许在- post部分运行该步骤。
- unstable只有当前流水线或阶段的完成状态为“unstable”,才允许在- post部分运行该步骤,通常由于测试失败,代码违规等造成。
- aborted只有当前流水线或阶段的完成状态为“aborted”,才允许在- post部分运行该步骤,通常由于流水线被手动的 aborted。
Case 处理故障:
测试失败后发送邮件
pipeline {
  agent any
  stages {
    stage('Test') {
      steps {
        sh 'make check'
      }
    }
  }
  post {
    always {
      junit '**/target/*.xml'
    }
    failure {
      mail to: team @example.com, subject: 'The Pipeline failed :('
    }
  }
}stages #
包含一系列一个或多个 stage 指令,
stages部分是流水线描述的大部分“work”的位置。建议stages至少包含一个stage指令用于连续交付过程的每个离散部分,比如构建、测试和部署。
environment #
指令制定一个 键-值对序列,该序列将被定义为所有步骤的环境变量,或者是特定于阶段的步骤,这取决于
environment指令在流水线内的位置。该指令支持一个特殊的助手方法
credentials(),该方法可用于在Jenkins环境中通过标识符访问预定义的凭证。对于类型为“Secret Text”的凭证,credentials()将确保指定的环境变量包含秘密文本内容。对于类型为“SStandard username and password”的凭证,指定的环境变量指定为username:password,并且两个额外的环境变量将被自动定义 :分别为MYVARNAME_USR和MYVARNAME_PSW。
- 顶层流水线块中使用的 environment指令将适用于流水线中的所有步骤。
- 在一个 stage中定义的environment指令只会将给定的环境变量应用于 stage 中的步骤。
- environment块有一个 助手方法- credentials()定义,该方法可以在 Jenkins 环境中用于通过标识符访问预定义的凭证。
TIP
处理凭据 #
- Secret 文本
- 带密码的用户名
- Secret 文件
- 其他凭据类型(SSH 私钥、PKCS、Docker 主机证书)
options #
options指令允许从流水线内部配置特定于流水线的选项。流水线提供了许多这样的选项,比如buildDiscarder,但也可以由插件提供,比如timestamps。
可用选项:
- buildDiscarder 为最近的流水线运行的特定数量保存组件和控制台输出。例如:options { buildDiscarder(logRotator(numToKeepStr: '1')) }
- disableConcurrentBuilds 不允许同时执行流水线。 可被用来防止同时访问共享资源等。 例如:options { disableConcurrentBuilds() }
- overrideIndexTriggers 允许覆盖分支索引触发器的默认处理。如果分支索引触发器在多分支或组织标签中禁用, options { overrideIndexTriggers(true) }将只允许它们用于促工作。否则options { overrideIndexTriggers(false) }只会禁用改作业的分支索引触发器。
- skipDefaultCheckout 在agent指令中,跳过从源代码控制中检出代码的默认情况。例如:options { skipDefaultCheckout() }
- skipStagesAfterUnstable 一旦构建状态变得UNSTABLE,跳过该阶段。例如:options { skipStagesAfterUnstable() }
- checkoutToSubdirectory 在工作空间的子目录中自动地执行源代码控制检出。例如:options { checkoutToSubdirectory('foo') }
- timeout 设置流水线运行的超时时间, 在此之后,Jenkins将中止流水线。例如:options { timeout(time: 1, unit: 'HOURS') }
- retry 在失败时, 重新尝试整个流水线的指定次数。 例如:options { retry(3) }
- timestamps 预谋所有由流水线生成的控制台输出,与该流水线发出的时间一致。例如:options { timestamps() }
trigger #
triggers指令定义了流水线被重新触发的自动化方法。对于集成了源(比如 GitHub 或 BitBucket)的流水线, 可能不需要triggers,因为基于 web 的集成很肯能已经存在。当前可用的触发器是cron,pollSCM和upstream。
- cron 接收 cron 样式的字符串来定义要重新触发流水线的常规间隔,比如:triggers { cron('H */4 * * 1-5') }
- pollSCM 接收 cron 样式的字符串来定义一个固定的间隔,在这个间隔中,Jenkins 会检查新的源代码更新。如果存在更改,流水线就会被重新触发。例如:triggers { pollSCM('H */4 * * 1-5') }
- upstream 接受逗号分隔的工作字符串和阈值。当字符串中的任何作业以最小阈值结束时,流水线被重新触发。例如:triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS) }
when #
when指令允许流水线根据给定的条件决定是否应该执行阶段。when指令必须包含至少一个条件。如果when指令包含多个条件,所有的子条件必须返回 True,阶段才能执行。这与子条件在allOf条件下嵌套的情况相同。 使用诸如not、allOf或 `` 的嵌套条件可以构建更复杂的条件结构 can be built 嵌套条件可以嵌套到任意深度。
内置条件:
- 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' } }
Case:在进入 stage 的 agent 前评估 when
concurrent #
声明式流水线的阶段可以在他们内部声明多隔嵌套阶段,它们将并行执行。注意,一个阶段必须只有一个
steps或parallel的阶段。嵌套阶段本身不能包含进一步的parallel阶段,但是其他的阶段的行为与任何其他stage相同。任何包含parallel的阶段不能包含agent或tools阶段,因为他们没有相关steps。另外,通过添加
failFast true到包含parallel的stage中,当其中一个进程失败时,你可以强制所有的parallel阶段都被终止。
Case:
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"
          }
        }
      }
    }
  }
}案例 #
blog/algorithm #
以前刷过一段时间的 PAT,有一些经典题目记录了下来,后续也会抽空刷 LeetCode,所以使用 jekyll 搭建了一个 IOI 题解的 blog,需要一些环境,这个 case 主要记录将 github 中的代码 build 并发布到服务器。
由于 jekyll 需要一些环境,所以我就做了一个用于 build site 的 docker image(很简陋,后面会优化一下):
pipeline
::: code-tabs
@tab blog
@code
:::
部署 bot-huan #
bot-huan pipeline
pipeline {
  // 设置全局环境变量
  environment {
    url = 'https://gitlab.com/Alomerry/bot-huan.git'
    KAIHEILA_BOT_TOKEN = credentials('kaiheila-bot-token')
    KAIHEILA_BOT_VERIFY_TOKEN = credentials('kaiheila-bot-verify-token')
    KAIHEILA_BOT_ENCRYPT_KEY = credentials('kaiheila-bot-encrypt-key')
  }
  triggers {
    GenericTrigger(
      genericVariables: [
        [
          key           : 'name',
          value         : '$.repository.name',
          expressionType: 'JSONPath',
          regularFilter : '',
          defaultValue  : ''
        ]
      ],
      printContributedVariables: false,
      printPostContent: false,
      tokenCredentialId: 'jenkins-webhook-token',
      regexpFilterText: '$name',
      regexpFilterExpression: '^(B|b)ot-huan$',
      causeString: ' Triggered on $ref',
    )
  }
  agent any
  stages {
    stage('update build image') {
      steps {
        sh 'docker pull registry.cn-hangzhou.aliyuncs.com/alomerry/base-golang:1.18'
        sh 'docker pull registry.cn-hangzhou.aliyuncs.com/alomerry/bot-huan'
      }
    }
    stage('pull code and build') {
      agent {
        docker {
          image 'registry.cn-hangzhou.aliyuncs.com/alomerry/base-golang:1.18'
        }
      }
      steps {
        retry(3) {
          // 拉取代码
          git(url: env.url, branch: 'master')
        }
        // 构建
        dir("backend") {
          sh "go build -mod=vendor -o main"
          stash(name: "bin", includes: "main")
        }
      }
    }
    stage('run bin') {
      steps {
        dir("/var/jenkins_home/build") {
          unstash("bin")
        }
        sh '''
        docker rm $(docker ps -aq --filter name=bot-huan) -f || true
        docker run -d --name bot-huan -p 4376:4376 -v /home/alomerry-home/apps/jenkins/build:/build -e $KAIHEILA_BOT_TOKEN_USR=$KAIHEILA_BOT_TOKEN_PSW -e $KAIHEILA_BOT_VERIFY_TOKEN_USR=$KAIHEILA_BOT_VERIFY_TOKEN_PSW -e $KAIHEILA_BOT_ENCRYPT_KEY_USR=$KAIHEILA_BOT_ENCRYPT_KEY_PSW registry.cn-hangzhou.aliyuncs.com/alomerry/bot-huan
        '''
        // TODO 验证是否正常启动 否则报错
      }
    }
  }
  post {
    always {
      deleteDir()
    }
    failure {
      mail to: 'alomerry.wu@gmail.com',
        subject: "Failed Pipeline: ${currentBuild.fullDisplayName}",
        body: "Something is wrong with ${env.url}"
    }
  }
}
jenkins function #
pipeline {
  agent any
  stages {
    stage('Test') {
      steps {
        whateverFunction()
      }
    }
  }
}
def whateverFunction() {
  sh 'ls /'
}return value
def output // set as global variable
pipeline {
  ...
  stage('Sum')
    {
      steps
        {
          script
            {
              output = sum()
              echo "The sum is ${output}"
            }
        }
    }
  ...Reference #
Jenkinsfile #
通过文件变动来触发其他 job #
stage('check and trigger resume') {
  steps {
    script {
      def resumeChanged = 'git --no-pager diff --name-only HEAD^ HEAD | grep -q "src/about/resume/"'
      if (resumeChanged != "") {
        build job: 'resume', wait: true
      }
    }
  }
}git diff -name-only HEAD^ HEAD 可以输出 HEAD 与 HEAD 前一次的变动文件,通过管道和 grep 来筛选是否包含 src/about/resume 路径下的改动。--no-pager 可以直接输出结果,避免以交互式的形式展示
- jenkins git diff https://sinkcup.github.io/jenkins-git-diffs
Jenkins 插件 #
https://plugins.jenkins.io/build-user-vars-plugin/
SSH #
Docker #
Generic Webhook Trigger[5] #
Case 配合 pipeline 中的 trigger 可以实现仓库有推送后即触发构建
GenericTrigger(
  genericVariables: [
    [
      key           : 'name',
      value         : '$.repository.name',
      expressionType: 'JSONPath',
      regularFilter : '',
      defaultValue  : ''
    ]
  ],
  printContributedVariables: false,
  printPostContent: false,
  tokenCredentialId: 'jenkins-webhook-token',
  regexpFilterText: '$name',
  regexpFilterExpression: '^(B|b)ot-huan$',
  causeString: ' Triggered on $ref',
)genericVariables 中配置一些从 request.body 中获取的变量,上例中读取的是 request.body 中的 repository.name 的值赋到变量 name 中,并使用正则判断是否满足要求
配置后可以使用 gitlab Test push 查看 jenkins 返回值
{
  "jobs": {
    "bot-huan": {
      "regexpFilterExpression": "bot-huan",
      "triggered": true,
      "resolvedVariables": {
        "name": "bot-huan"
      },
      "regexpFilterText": "bot-huan",
      "id": 390,
      "url": "queue/item/390/"
    },
    "blog": {
      "regexpFilterExpression": "^(B|b)log$",
      "triggered": false,
      "resolvedVariables": {
        "name": "bot-huan"
      },
      "regexpFilterText": "bot-huan",
      "id": 0,
      "url": ""
    },
    "algorithm": {
      "regexpFilterExpression": "^(A|a)lgorithm$",
      "triggered": false,
      "resolvedVariables": {
        "name": "bot-huan"
      },
      "regexpFilterText": "bot-huan",
      "id": 0,
      "url": ""
    }
  },
  "message": "Triggered jobs."
}jenkins 通过流水线中配置的正则来匹配触发哪条流水线,可以查看 jenkins 给 gitlab 的返回值看出触发了 bot-huan 的构建
SSH Pipeline Step[6] #
- sshCommand
- sshGet
- sshPut
- sshRemove
- sshScript
Case 将构建好的静态文件发布到服务器:
def remote = [:]
remote.name = 'root'
remote.logLevel = 'FINEST'
remote.host = '[your host]'
remote.allowAnyHosts = true
withCredentials([usernamePassword(credentialsId: 'tencent-ubuntu-root', passwordVariable: 'password', usernameVariable: 'username')]) {
  remote.user = "${username}"
  remote.password = "${password}"
}
sshCommand remote: remote, command: '''#!/bin/bash
    cd /www/wwwroot/[your website]/
    shopt -s extglob
    rm -rf !(.htaccess|.user.ini|.well-known|favicon.ico|algorithm.tar.gz)
    '''
sshPut remote: remote, from: '/var/jenkins_home/workspace/algorithm/docs/_site/algorithm.tar.gz', into: '/www/wwwroot/[your website]/'
sshCommand remote: remote, command: "cd /www/wwwroot/[your website] && tar -xf algorithm.tar.gz"
sshRemove remote: remote, path: '/www/wwwroot/[your website]/algorithm.tar.gz'配合 pipeline 中的 environment,配置好 remote 后,先删除非必要文件,将静态文件压缩包推送到服务器指定位置,解压后删除即可。