持续集成 使用 Jenkins Pipeline 迁移 Job

diao2007 · 2018年01月15日 · 最后由 diao2007 回复于 2018年05月14日 · 2958 次阅读
本帖已被设为精华帖!

背景

2017年一整年我的工作都是围绕Jenkins,做持续集成相关的一系列工作。支持了公司内部两个客户端平台和三个项目的APP打包,静态代码检查,Sonar平台搭建和维护等工作。

年底统计支持线上手动构建次数达到了几千次。稳定性却并没有达到我的预期,全年只有92%,最近正好在看Jenkins Pipeline相关的语法,然后实践了一下Jenkins Job升级的过程,其实并没有想象的那么简单,其中涉及到之前很多低版本的插件甚至已经被Jenkins新版本弃用。

这个过程中,首先,需要十分感谢社区的@rocl的帮助,在这个过程中,给予了很多很好的帮助和指导意见。

注意的点

其实之前对Jenkins BlueOcean第一个版本就在本地尝试运行过,但是只一个DEMO,真正迁移涉及到我们自己线上运行的Job时候,还是有些点,需要注意。

区分Declarative Pipeline和Scripted Pipeline

Declarative Pipeline和Scripted Pipeline都是Pipeline的语法,但是两者是有区别的。

  • Declarative 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 Syntax

推荐一个快速熟悉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参数化选择功能能使用了,但是这样做同样有一个问题,就是远程分支名字同步不能实时,如果分支增加或者减少,只能第一次失败,第二次才能构建成功或者选择到。

完整的Declarative Pipeline脚本

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,但是还是感觉跟预期的有点差距,上手并不是想象的那么容易,但是它值得迁移。

参考

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 13 条回复 时间 点赞
seveniruby 将本帖设为了精华贴 01月16日 01:00

我用Extended Choice Parameter插件,结合git插件,然后在pepeline里通过env.xxx获取用户选择的git版本。这样写会方便一些。

regend 回复

用户点击 build with parameters可以选择分支吗?extended choice paramter中你加了groovy取分支的代码吗?能贴图看看吗

diao2007 回复

不好意思,之前说错是了,我是用的git parament插件。主要是设置pipeline那里要选Pipeline script from SCM 然后jenkins file要在git项目里。然后代码中通过def fullBranch = env.Branch获取选择的branch



regend 回复

谢谢,我理解你的意思了,git parameter相对而言,直接用git ls-remote会慢很多,你可以自己体验一下,我看了git parameter的插件,实现貌似是通过取git clone代码那里获取,具体也没深入了解。

diao2007 回复

我感觉差不了多少,就首次慢一点。主要是在代码部分写起来会简洁许多。但是你那样做的好处就是一个脚本可以多个项目一起使用,改动很少。要用git插件的话,每个项目还要配置比较麻烦。各有优缺点吧

确实哟,Pipeline Syntax的env有BRANCH_NAME这个变量

对我而言最大的好处是Job迁移和可控性更强

我的感觉最大的好处:提升效率。所以
KK说:pipeline+blue Ocean = future
James Whittake说:当你把开发和测试混合在一起搅拌的时候,你就得到了质量

diao2007 回复

可以说的再具体点吗?我现在也体会不到pipeline的好处在哪

stefvsjay 回复

建议你看看这个,https://testerhome.com/topics/10782 , 我的使用还没有接触到更优势的地方。感觉这里讲的更好。

在jenkins的普通工程配置warngins等publisher插件能查看到报表的图形视图,请问改成pipeline之后,选择一个step,它会显示出来图表码?

xuyugang 回复

可以

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册