「Jenkins Pipeline」- 如何编写共享库

  CREATED BY JENKINSBOT

共享库的目录结构

(root)
+- src                     # Groovy source files
|   +- org
|       +- foo
|           +- Bar.groovy  # for org.foo.Bar class
+- vars
|   +- foo.groovy          # for global 'foo' variable
|   +- foo.txt             # help for 'foo' variable
+- resources               # resource files (external libraries only)
|   +- org
|       +- foo
|           +- bar.json    # static helper data for org.foo.Bar

src

像标准的 Java 源码目录结构。执行 Pipeline 时,此目录将添加到 CLASSPATH 中。

vars

保存脚本文件,将在 Pipeline 中作为变量公开,文件名将是在 Pipeline 中变量的名称。什么意思呢?如果你有一个名为vars/log.groovy的文件,其中包含def info(message){}方法,那么可以在 Pipeline 中访问此函数,例如log.info "hello world"。可以在此文件中放置任意数量的函数。请阅读下面的更多示例和选项。

每个.groovy文件的基本名称应该是一个Groovy(~Java)标识符,通常是驼峰命名。可以存在对应的.txt文件,它其中可以包含文档,通过系统配置的「标记格式化程序」处理(因此可能是HTML,Markdown等,但需要.txt扩展名)。此文档仅在「全局变量引用」页面上可见,可以从“导入共享库”的“ Pipeline 作业”的“导航侧栏”进行访问。此外这些作业必须“在生成共享库文档之前”成功运行一次。

这些目录中的 Groovy 源文件与 Scripted Pipeline 获得相同的“CPS transformation”。

resources

允许使用libraryResource步骤,来从外部库加载关联的非Groovy文件。 目前,内部库不支持此功能。

其他目录

其他目录为保留目录,用于以后增强扩展。

共享库的开发语言

任何 Groovy 有效的代码都可以。例如不同的数据结构、工具方法:

// src/org/foo/Point.groovy
package org.foo

// point in 3D space
class Point {
  float x,y,z
}

在类中访问步骤

方法一、“在类外部”

在库类中(src/),不能直接调用步骤(比如 ssh、git 等等)。然而他们可以实现方法,需要在封闭的类的范围外部,在其中调用步骤:

// src/org/foo/Zot.groovy
package org.foo

def checkOutFrom(repo) {
  git url: "git@github.com:jenkinsci/${repo}"
}

return this

然后在 Pipeline Script 中调用:

def z = new org.foo.Zot()
z.checkOutFrom(repo)

这种方法有局限性;例如,它阻止父类的声明。

方法二、使用 this 关键字

另外可以通过 this 关键字将步骤传递到类中。可以在构造器中,也可以是一个方法:

// src/org/foo/Utilities.grovvy
package org.foo
class Utilities implements Serializable {
  def steps

  Utilities(steps) {
    this.steps = steps
  }

  def mvn(args) {
    steps.sh "${steps.tool 'Maven'}/bin/mvn -o ${args}"
  }
}

在类上保存状态时,像上面那样,类必须实现 Serializable 接口,这保证使用该类的 Pipeline 可以在 Jenkins 中休眠和恢复。

调用定义的类:

@Library('utils')
import org.foo.Utilities

def utils = new Utilities(this)
node {
  utils.mvn 'clean package'
}

在类中使用全局变量

如果要使用全局变量(env),应该明确的“传入类”(使用构造器)或“传入方法”(使用方法参数)中。手段是类似的:

package org.foo
class Utilities {
  static def mvn(script, args) {
    script.sh "${script.tool 'Maven'}/bin/mvn -s ${script.env.HOME}/jenkins.xml -o ${args}"
  }
}

上面的代码将环境变量传入静态的方法中,然后在下面的脚本化Pipeline中调用:

@Library('utils') import static org.foo.Utilities.*
node {
  mvn this, 'clean package'
}

注意,不建议将env中的参数取出来挨个传入函数中。

定义全局变量(vars/)

在内部,在 vars/ 中的脚本“按需要”实例化为单例。

在全局变量中定义方法

为方便起见,这允许在单个.groovy文件中定义多个方法。例如:

// vars/log.groovy
def info(message) {
    echo "INFO: ${message}"
}

def warning(message) {
    echo "WARNING: ${message}"
}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

// In Jenkinsfile
@Library('utils') _

log.info 'Starting'
log.warning 'Nothing to do!'

在全局变量中定义变量

注意,如果您希望在全局中使用某个字,用于保存状态,请将其注解为:

@groovy.transform.Field
def yourField = [:]

def yourFunction....

调用全局变量

声明式的 Pipeline 不允许在 script 块外进行对象的方法调用(JENKINS-42360),需要放在 script 中使用:

// In Jenkinsfile
@Library('utils') _

pipeline {
    agent none
    stage ('Example') {
        steps {
            // log.info 'Starting' // 该命令会失败,因为它在 script 块之外
            script { // 需要使用 script 块来访问全局变量
                log.info 'Starting'
                log.warning 'Nothing to do!'
            }
        }
    }
}

// 定义在共享库中的变量要在 Global Variables Reference (在 Pipeline Syntax 中)中显
// 示,前提是Jenkins在一次成功的Pipeline运行中加载并使用该库


// 最佳实践:避免在全局变量中保留状态
// 避免定义“与方法交互或保留状态的”全局变量。应该使用“静态类”或“实例化类的局部变量”

创建自定义步骤

共享库还可以定义全局变量,其行为类似于内置步骤(例如 sh、git 等等)。在共享库中定义的全局变量必须使用所有全小写或驼峰命名,以便通Pipeline 正确加载。

例如,要定义sayHello()步骤,应创建vars/sayHello.groovy文件,并应实现call()方法。call()方法允许以类似于步骤的方式调用全局变量:

// vars/sayHello.groovy
def call(String name = 'human') {
    // Any valid steps can be called from this code, just like in other
    // Scripted Pipeline
    echo "Hello, ${name}."
}

然后 Pipeline 将能够引用和调用此变量:

sayHello 'Joe'
sayHello() /* invoke with default arguments */

如果使用块调用,则call()方法将接收 Closure 对象。应明确定义类型,以阐明该步骤的意图,例如:

// vars/windows.groovy
def call(Closure body) {
    node('windows') {
        body()
    }
}

然后 Pipeline 可以使用此变量,如同接受块的任何内置步骤一样:

windows {
    bat "cmd /?"
}

定义更加结构化的 DSL

如果你有很多大致相似的 Pipeline,全局变量机制提供了便利的工具,用于构建一个捕获相似性的更高级别的 DSL。

例如,所有 Jenkins Plugin 都以相同的方式构建和测试,因此我们可以编写名为 buildPlugin 的步骤:

// vars/buildPlugin.groovy
def call(Map config) {
    node {
        git url: "https://github.com/jenkinsci/${config.name}-plugin.git"
        sh 'mvn install'
        mail to: '...', subject: "${config.name} plugin build", body: '...'
    }
}

假设脚本已作为全局共享库(或文件夹级共享库)加载,最后产生的 Jenkinsfile 将变得非常简单:

// Jenkinsfile (Scripted Pipeline)
buildPlugin name: 'git'

还有使用 Groovy 的 Closure.DELEGATE_FIRST 的“构建器模式”技巧,它允许 Jenkinsfile 看起来更像配置文件而不是程序,但这更复杂且容易出错,不建议使用。

使用第三方库

可以从信任的库代码中使用第三方的 Java 库,一般可以在 Maven Central 中找到,从受信库代码中使用@Grab注解。

有关详细信息,请参阅 Grape 文档,但只需输入:

@Grab('org.apache.commons:commons-math3:3.4.1')
import org.apache.commons.math3.primes.Primes

void parallelize(int count) {
  if (!Primes.isPrime(count)) {
    error "${count} was not prime"
  }
  // …
}

在默认情况下,第三方库会被缓存,在 Jenkins 主节点的~/.groovy/grapes/中。

加载资源文件(resources)

外部库可使用 libraryResource 来加载在 resources/ 中的资源。参数是相对路径,类似于在 Java 中资源加载:

def request = libraryResource 'com/mycorp/pipeline/somelib/request.json'

该文件内容作为字符串加载,可以传入某些 API 中,或者使用 writeFile 保存到工作目录中。

建议,在 resources/ 中使用唯一的包结构来保存文件,防止和其他的类库冲突。

预先测试库修改

如果发现使用不受信任的库在构建中出现错误,只需单击“Replay”链接以尝试编辑其一个或多个源文件,并查看生成的构建是否按预期运行。对结果感到满意后,请从构建的状态页面中按照 diff 链接,将 diff 应用到库存储库并提交。

(即使为库请求的版本是分支,而不是像标记这样的固定版本,Replay版本将使用与原始版本完全相同的版本:库源码不会再次检出。)

目前,受信任的库不支持 Replay 。Replay 期间当前也不支持修改资源文件。

定义声明式流水

从2017年9月下旬发布的 Declarative 1.2 开始,也可以在共享库中定义 Declarative Pipelines 。这是一个示例,它将执行不同的声明性管道,具体取决于构建号是奇数还是偶数:

// vars/evenOrOdd.groovy
def call(int buildNumber) {
  if (buildNumber % 2 == 0) {
    pipeline {
      agent any
      stages {
        stage('Even Stage') {
          steps {
            echo "The build number is even"
          }
        }
      }
    }
  } else {
    pipeline {
      agent any
      stages {
        stage('Odd Stage') {
          steps {
            echo "The build number is odd"
          }
        }
      }
    }
  }
}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

// Jenkinsfile
@Library('my-shared-library') _

evenOrOdd(currentBuild.getNumber())

到目前为止,只能在共享库中定义整个 Pipeline 。这只能在 vars/*.groovy 中完成,并且只能在 call() 中完成。在单个构建中只能执行单个 Declarative Pipeline ,如果您尝试执行第二个,那么构建将因此失败。

构建工具(项目管理)

在开发初期时,我们通过运行调试的方式该修正问题。随着项目的增长,作业运行周期较长,我们需要通过单元测试来对代码进行调试,而不能再通过运行调试的方式来运行测试代码。

Maven + JUnit + GMavenPlus

# 12/20/2022 当前,我们通过 Maven 进行共享库的管理:通过 JUnit 进行单元测试。通过 GMavenPlus Plugin 进行 Groovy 编译;

虽然 GMavenPlus 是 Groovy 官方的插件,但是在技术选型时我们依旧有些担心,日后可能会使用 Gradle 管理。

Jenkins Shared Library Test Harness Example
https://github.com/stchar/pipeline-sharedlib-testharness

参考文献

Pipeline/Extending with Shared Libraries