2017 年一整年我的工作都是围绕 Jenkins,做持续集成相关的一系列工作。支持了公司内部两个客户端平台和三个项目的 APP 打包,静态代码检查,Sonar 平台搭建和维护等工作。
年底统计支持线上手动构建次数达到了几千次。稳定性却并没有达到我的预期,全年只有 92%,最近正好在看 Jenkins Pipeline 相关的语法,然后实践了一下 Jenkins Job 升级的过程,其实并没有想象的那么简单,其中涉及到之前很多低版本的插件甚至已经被 Jenkins 新版本弃用。
这个过程中,首先,需要十分感谢社区的@rocl的帮助,在这个过程中,给予了很多很好的帮助和指导意见。
其实之前对 Jenkins BlueOcean 第一个版本就在本地尝试运行过,但是只一个 DEMO,真正迁移涉及到我们自己线上运行的 Job 时候,还是有些点,需要注意。
Declarative Pipeline 和 Scripted Pipeline 都是 Pipeline 的语法,但是两者是有区别的。
pipeline {
xxx
}
然后在结构体中,可以新增 stages,stages 中需要包含 stage,stage 中必须包含 steps,否则会报错。类似于这样的流程:
pipeline {
agent { node { label 'label1' } }
stages {
stage('build') {
steps {
script {
def test = "1111"
if (test = "1111"){
println "==="
}
}
}
}
}
}
如果需要,增加 Groovy 代码,则需要增加 script 模块包裹代码
而 Scripted Pipeline 则相对简单,如果是刚才同样的代码,实现是:
node("label1"){
def test = "1111"
if (test = "1111"){
println "==="
}
}
区别还是很明显的,但是我觉得对我而言最大的不同点,是两者环境变量的设置。
Declarative Pipeline 设置环境变量是通过 environment,但是 Scripted Pipeline 设置环境变量却是通过 withEnv。
推荐一个快速熟悉 Pipeline 语法的入口,你先新建一个 pipeline job,然后编辑页面,有一个 Pipeline Syntax,点击之后,是如类似的页面。
我这里想强调的是,选择 properties 时候,当前 Pipeline 并不支持了,需要替换成 Options,举个栗子,保留历史构建次数的。将下面的块结构体放入 pipeline 中即可。
// 设置保留构建次数
options {
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '10'))
}
在之前低版本的 Jenkins 内,我的实现是通过插件 Dynamic Choices Paramters Plugin 支持的,加上 Groovy 脚本实现取分支代码。
但是新版本已经弃用了这个插件, https://jenkins.io/security/advisory/2017-04-10/
它们的描述是:
Dynamic Parameter Plugin
SECURITY-462
Dynamic Parameter Plugin allows users with Job/Configure permission to define scripts to be executed on the Build With Parameters form to determine available parameter values.
This allows users with Item/Configure permission to run arbitrary Groovy code inside the Jenkins JVM.
As of publication of this advisory, there is no fix.
那么只有使用 Active Choices Plugin,实现不是很复杂。将取分支的 groovy script 放入Pipeline Syntax
==>Propertis: Set Job propertis
==>This project is parameterized
==>Active Choices Parameters
最后生成:
properties([
parameters(
[
choice
(choices: 'google\nbaidu',
description: '测试一下',
name: 'name'),
[$class: 'ChoiceParameter',
choiceType: 'PT_SINGLE_SELECT',
description: '请选择分支',
filterLength: 1,
filterable: false,
name: 'BRANCH_TO_BUILD',
randomName: 'choice-parameter-13043314732909117',
script: [
$class: 'GroovyScript',
fallbackScript:
[classpath: [], sandbox: false, script: ''],
script:
[classpath: [], sandbox: false,
script: '''
def gitURL = "ssh://git@github"
def command = "git ls-remote -h $gitURL"
def proc = command.execute()
proc.waitFor()
branches = proc.in.text.readLines().grep(~/^((?!
(sandbox)).)*$/).collect {
it.replaceAll(/[a-z0-9]*\\trefs\\/heads\\//,\'\')
}'''
]
]
]
]
)
])
这个结构体需要放入
pipeline{}
前面部分。
这里我想简单过程中我遇到的最大的问题,不是这里,而是因为我们的代码仓库是 Gerrit,需要再取代码前执行 ssh 认证,那么我之前用这个脚本无法取到远程分支名字。
最后的解决方案是,用配置的 crendialId 的对应用户,在安装 Jenkins 的机器上,配置该用户在 Gerrit 对应账号下的id_rsa.pub
。
还有我想强调的是,如果用了 Active Choices Plugin 就无法正常使用 Blue Ocean 进行参数化构建了。会遇到报错。
"This pipeline uses input types that are unsupported. Use Jenkins Classic to resolve parametrized build."
Jenkins Issues 中https://issues.jenkins-ci.org/browse/JENKINS-41709有具体描述,貌似修复茫茫无期。
后来我想到了一个方法绕过。
我只用 groovy 脚本取远程分支名字,但是参数化部分,使用 Blue Ocean 支持的 Choices Parameter。
代码类似于:
def getbranchName(){
def gitURL = "ssh://git@github"
def command = "git ls-remote -h $gitURL"
def proc = command.execute()
proc.waitFor()
branches = proc.in.text.readLines().grep(~/^((?!
(sandbox)).)*$/).collect {
it.replaceAll(/[a-z0-9]*\\trefs\\/heads\\//,\'\')
return branches
}
pipeline{
parameters {
choice(
name: 'BRANCH_TO_BUILD',
choices: getbranchName(),
description: '请选择分支')
}
这样 BlueOcena 参数化选择功能能使用了,但是这样做同样有一个问题,就是远程分支名字同步不能实时,如果分支增加或者减少,只能第一次失败,第二次才能构建成功或者选择到。
properties([
parameters(
[
choice
(choices: 'aaa\nbbb', description: '测试一下', name: 'name'),
[$class: 'ChoiceParameter',
choiceType: 'PT_SINGLE_SELECT',
description: '请选择分支',
filterLength: 1,
filterable: false,
name: 'BRANCH_TO_BUILD',
randomName: 'choice-parameter-13043314732909117',
script: [
$class: 'GroovyScript',
fallbackScript:
[classpath: [], sandbox: false, script: ''],
script:
[classpath: [], sandbox: false,
script: '''
def gitURL = "ssh://git@github"
def command = "git ls-remote -h $gitURL"
def proc = command.execute()
proc.waitFor()
branches = proc.in.text.readLines().grep(~/^((?!(sandbox)).)*$/).collect {
it.replaceAll(/[a-z0-9]*\\trefs\\/heads\\//, \'\')
}'''
]
]
]
]
)
])
pipeline {
// 选择执行机器
agent { node { label 'label1' } }
// 配置环境变量
environment {
KEYSTORE = 'xx'
KEYALIAS = 'xx'
KEYSTORE_PASSWD = 'xx'
KEYALIAS_PASSWD = 'xx'
credentialsId = 'xxx'
}
// 设置保留构建次数
options {
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '10'))
}
stages {
stage('Clone Code') {
steps{
// 取项目代码
checkout([$class: 'GitSCM', branches: [[name: "${params.BRANCH_TO_BUILD}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'CloneOption', depth: 0, noTags: false, reference: '', shallow: false, timeout: 35]],
submoduleCfg: [],
userRemoteConfigs: [[credentialsId: "${credentialsId}",
url: 'ssh://git@github']]])
// 显示打包执行的change log
lastChanges format: 'LINE',
matchWordsThreshold: '0.25', matching: 'NONE',
matchingMaxComparisons: '1000', showFiles: true,
since: 'LAST_SUCCESSFUL_BUILD', specificBuild: '',
specificRevision: '', synchronisedScroll: true, vcsDir: ''
}
}
stage('Build') {
steps{
script{
switch("${params.Name}") {
case "aaa":
PRODUCT_FLAVOR="Normal"
break;
case "bbb":
PRODUCT_FLAVOR="play"
break;
}
// 执行打包
sh "bash ./gradlew clean assembe${PRODUCT_FLAVOR}Release"
// tar
sh "tar czf build/outputs/archive_${PRODUCT_FLAVOR}.tgz build/outputs/apk/*/*/*.apk build/outputs/mapping/*/*/mapping.txt"
// artifacts
archiveArtifacts "build/outputs/archive_${PRODUCT_FLAVOR}_${BUILD_NUMBER}.tgz"
}
}
}
}
post {
always {
build job: 'xxxx', parameters: [
string(name: 'BUILD_TIMESTAMP_BEFORE', value: "${env.BUILD_TIMESTAMP}"),
// 取构建者信息
string(name: 'BUILD_CAUSE_BEFORE', value: "${currentBuild.rawBuild.getCause(hudson.model.Cause$UserIdCause).userName}"),
string(name: 'JOB_NAME_BEFORE', value: "${env.JOB_NAME}"),
string(name: 'BUILD_URL_BEFORE', value: "${env.BUILD_URL}"),
string(name: 'JOB_BASE_NAME_BEFORE', value: "${env.JOB_BASE_NAME}")],
propagate: false, quietPeriod: 0, wait: false
}
}
}
解释一下,最后的 post 部分,为了是我自己写了一个服务收集所有的构建 Job 的信息,因而触发下一个 Job 执行一个向我写的服务,post 信息的任务。
有人可能会问为什么,不直接在打包 job 后面直接向我写的服务 post 信息,是因为为了不影响本身 Build Job 的构建状态。
Jenkins Pipeline 已经迁移了几个手动打包的 Job,但是还是感觉跟预期的有点差距,上手并不是想象的那么容易,但是它值得迁移。