持续集成 使用 Jenkins Pipeline 迁移 Job

diao2007 · 发布于 2018年01月15日 · 最后由 diao2007 回复于 2018年01月23日 · 561 次阅读
本帖已被设为精华帖!

背景

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

参考

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

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

4093
diao2007 · 3楼 · 2018年01月16日 作者
32regend 回复

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

96
4093diao2007 回复

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



4093
diao2007 · 5楼 · 2018年01月16日 作者
32regend 回复

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

96
4093diao2007 回复

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

444b12

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

67c339

hello,想问个问题,我一直不明白
我感觉jenkins pipeline和jenkins定义上下游工程是啥不多的功能,为啥非要用jenkins pipeline呢

4093
diao2007 · 9楼 · 2018年01月16日 作者
67c339nateby 回复

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

444b12
67c339nateby 回复

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

6929
4093diao2007 回复

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

4093
diao2007 · 12楼 · 2018年01月23日 作者
6929stefvsjay 回复

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

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