共享库的目录结构
(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