持续集成 使用 Jenkins Pipeline 迁移 Job

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

背景

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 取分支的代码吗?能贴图看看吗

不二家 回复

不好意思,之前说错是了,我是用的 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 代码那里获取,具体也没深入了解。

不二家 回复

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

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

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

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

不二家 回复

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

李雪原 回复

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

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

Dafeizhi 回复

可以

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 14:44
simple [精彩盘点] TesterHome 社区 2018 年 度精华帖 中提及了此贴 01月07日 12:08
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册