背景

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

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

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

注意的点

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

区分 Declarative Pipeline 和 Scripted Pipeline

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

类似于这样的流程:

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

参考


↙↙↙阅读原文可查看相关链接,并与作者交流