Groovy 和 Jenkins pipeline

公司项目一直使用的 Jenkins 作为 java/Android 的 CI/CD 工具,之前需求比较简单,直接使用 Jenkins 提供的管理后台 UI 操作就能完成。后来有一些新的构建需求,我最初是使用 Python 处理的,近期全部转到了 gradle 脚本 + Jenkins pipeline 的实现。这两者使用的都是 groovy,使用过程中发现,其实 groovy 很多操作比 Python 都要方便。

先介绍一些 groovy 操作便捷和需要注意的地方

File

join file path

1
2
3
4
5
def dataPath = Paths.get("/User/xxx","data/log")
def file = dataPath.toFile() // def file = new File(".")
file.eachFile { f ->

}

读写文件

1
2
new File(".").text
// even use `as String[]`

更多示例可以参考:groovy-io

正则

groovy 提供了很多字符串表示正则的写法:

  • '\d+' 单引号
  • "\d+" 双引号
  • /\d+/ 斜杠
  • $\d+$ 美元符号

*我使用斜杠较多,可以应付很多需要转义的情况

groovy 还提供了很多特有的操作符:

  • ~ 模式符,后面紧跟正则表达式字符串即会转换为 java.util.regex.Pattern 对象, 如:
    1
    2
    def p = ~/\d+/
    // p 就是一个 Pattern 对象了
  • =~ 正则搜寻符,相当与执行正则的 find 操作,返回 Matcher 对象,在 groovy 里可以直接对其进行遍历:
    1
    2
    3
    4
    def matcher = result =~ /versionCode='(\S+)'\s+versionName='(\S+)'/
    // matcher[0] 就是搜索结果,具体group结果在子数组里
    def version_code = matcher[0][1]
    def version_name = matcher[0][2]
  • ==~ 正则匹配符,相当与执行 match 操作,返回值 boolean 类型

执行 shell 命令

直接 'ls -al'.execute().text 就可以执行并拿到结果,非常方便,更 Robust 的代码:

1
2
3
4
5
def result = new StringBuilder()
def error = new StringBuilder()
def cmd_newApkInfo = cmd.execute()
cmd_newApkInfo.consumeProcessOutput(result, error)
cmd_newApkInfo.waitFor()

List/Map

  1. 创建
    • list: def a = []
    • map 就不太一样了: def m = [:], def m = [name: "John", age:123]
  2. find, findAll, collectfindfindAll 起到过滤作用,前者返回一个元素,后者返回集合; 相当于其他语言里的 map 转换方法。

变量作用域的问题

1
2
3
4
5
6
7
8
9
// foo.groovy
x = 42
def y = 40
def f() {
println(x) // 42
println(y) // error
}

f()

这里top-level的俩变量是不一样的,x 没有类型声明,将触发 script binding,变成“全局变量”(script-scope),而 y 是本地变量,除了在top-level直接使用外,无法被其他方法调用。实际上看看 groovy 转换后的代码就一目了然了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class foo{
// 成员变量,“全局可访问”
int x = 42;

void f() {
println(x) // 42
println(y) // error
}

// top-level 的代码都是放到 run 方法里的
void run(){
int y = 40;
f();
}

static main(args){
new foo().run();
}

}

但是,不写任何声明的变量在 gradle 中是不能使用的,Android gradle 中如果想实现 script-scope variable,只能通过给 赋给 project/ext 的方式实现:

1
2
3
4
5
// build.gradle 
ext.x = 42
def f(){
println x //42
}

其他

  • 强制转换符 as, 这和 Kotlin 的还不一样:
    1
    2
    3
    4
    5
    6
    7
    Integer x = 123
    // Java ,直接抛出 ClassCastException 异常
    String s = (String) x
    // Kotlin ,同 Java
    String s = x as String
    // Groovy ,正常转换到 String
    String s = x as String
    如果是不同类型,groovy 这实际上是创建了一个新的对象,而这个转换方法( asType())是需要实现的,如果是自定义类型那需要我们自己实现。具体参考 Coercion operator

Jenkins Pipeline

Pipeline 是 Jenkins 提供的一种持续交付的模式,我们可以通过编写 pipeline 脚本文件 (Jenkinsfile) 来高效地实现我们的 CD 流程。
Jenkinsfile 脚本文件分为两种:

Declarative Pipeline

集成了很多 Jenkins 新提供的很多 Api 功能,可以声明式的便捷调用。
比如 jenkins 构建完成后你要发一条结果通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pipeline {
agent any
stages {
stage('No-op') {
steps {
sh 'ls'
}
}
}
post {
always {
echo 'One way or another, I have finished'
deleteDir() /* clean up our workspace */
}
success {
echo 'I succeeded!'
}
unstable {
echo 'I am unstable :/'
}
failure {
echo 'I failed :('
}
changed {
echo 'Things were different before...'
}
}
}

post 就是完成后会执行的方法,

Scripted Pipeline

纯“脚本式”的风格,Jenkins 提供的 api 很少。

同样的上面的需求, scripted 实现是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
node {
try {
stage('No-op') {
sh 'ls'
}
}
catch (exc) {
echo 'I failed'
} finally {
if (currentBuild.result == 'UNSTABLE') {
echo 'I am unstable :/'
} else {
echo 'One way or another, I have finished'
}
}
}

注意,只能使用 try catch的方式实现(currentBuild 是由 jenkins 提供的,可以直接使用)。

另外,从上面俩例子也可以说明两者的格式也是不一样的,前者整个构建过程需要这样表述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pipeline {
agent any
stages {
stage("Sources"){
steps {

}
}
stage('Test') {
steps {
//
}
}
}
}

pipeline 代码块是声明式的核心主体,其包含多个 stage,表示当前执行的阶段,名称可以自定义,stage 里又由多个 steps 构成,表示执行的具体步骤。

1
2
3
4
5
6
7
8
node {
stage('Build') {
//
}
stage('Test') {
//
}
}

node 代码块则是脚本式的核心主体,其直接包含多个 stage(其实有没有 stage 不重要),我们把具体的执行代码直接放到对应的 stage 下即可,这里没有 step 等内容。写 stage 的主要目的是可以在 jenkins 有直观的 ui 展示,比较方便。

pipeline 脚本主要基于 groovy,声明式和脚本式的环境、插件都是共用的(调用方式上会有些不一样),声明式出现的目的是 Jenkins 团队认为 groovy 的学习曲线还是比较陡峭,所以整了个声明式的,方便不了解 groovy 语法的开发者也可以使用 pipeline。Syntax Comparison

Jenkins 部分 Api 说明

  1. dir
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    node {
    // 工作目录 /workspace
    stage("Build"){
    dir('demo'){
    // 创建并切换到 /workspace/demo 目录下
    // git clone, credentialsId 需预先在 jenkins 中配置
    git branch: 'develop',
    url: '',
    credentialsId: ''
    }
    // 自动回到工作目录
    dir("demolib"){
    // 创建并切换到 /workspace/demolib 目录下
    }
    }
    }
  2. Jenkins pipeline 里执行 shell 脚本注意事项
  • 插入 pipeline 中定义的变量,需要用 '''+variable+'''方式插入:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    node {
    def f = "rtm"
    stage('Build'){
    sh '''

    ./gradlew '''+f+'''Release
    '''
    }

    }

References