使用共享库进行扩展

随着管道被组织中越来越多的项目采用,可能会出现通用模式。通常,在不同项目之间共享部分管道以减少冗余并保持代码 “DRY”1 很有用。

Pipeline 支持创建 “共享库”,可以在外部源代码控制存储库中定义并加载到现有的 Pipelines 中。

定义共享库

共享库由名称、源代码检索方法(如:SCM)和可选的默认版本定义。该名称应该是一个简短的标识符,因为它将在脚本中使用。

该版本可以是该 SCM 可以理解的任何内容;例如:分支、标签和提交哈希都适用于 Git。您还可以声明脚本是否需要显式请求该库(详见下文),或者它是否默认存在。此外,如果您在 Jenkins 配置中指定版本,您可以阻止脚本选择不同的版本。

指定 SCM 的最佳方式是使用 SCM 插件,该插件经过专门更新以支持新的 API 以检查任意命名版本(Modern SCM 选项)。

如果您的 SCM 插件尚未集成,您可以选择 Legacy SCM 并选择提供的任何内容。在这种情况下,您需要包含 ${library.yourLibName.version} 在 SCM 的配置中,以便在检出期间插件将扩展此变量以选择所需的版本。例如:对于 Subversion,您可以将存储库 URL 设置为 svnserver/project/${library.yourLibName.version} 然后使用例如:trunkbranches/devtags/1.0 的版本。

目录结构

共享库存储库的目录结构如下:

(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 源目录结构。这个目录在执行 Pipelines 时被添加到类路径中。

vars 目录托管在 Pipelines 中作为变量公开的脚本文件。文件的名称是管道中变量的名称。因此,如果您有一个 vars/log.groovy 使用类似函数调用的文件,则可以像在管道中 def info(message)… ​一样访问此函数。log.info "hello world" 您可以在此文件中放置任意数量的函数。

每个 .groovy 文件的基本名称通常应该是一个 Groovy (~ Java) 标识符,通常是 camelCased。匹配项 .txt(如果存在)可以包含通过系统配置的标记格式化程序处理的文档(实际上可能是 HTMLMarkdown 等,尽管 .txt 需要扩展名)。此文档仅在全局变量参考页面上可见,这些页面可从导入共享库的管道作业的导航侧边栏访问。此外,在生成共享库文档之前,这些作业必须成功运行一次。

这些目录中的 Groovy 源文件与 Scripted Pipeline 中的 “CPS 转换” 一样。

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

根目录下的其他目录保留用于将来的增强。

全球共享库

有几个地方可以定义共享库,具体取决于用例。Manage Jenkins » Configure System » Global Pipeline Libraries 可以配置尽可能多的库。

由于这些库将是全局可用的,因此系统中的任何管道都可以利用这些库中实现的功能。

这些库被认为是 “可信的”:它们可以运行 Java、Groovy、Jenkins 内部 API、Jenkins 插件或第三方库中的任何方法。这允许您定义库,这些库将单独的不安全 API 封装在更高级别的包装器中,以便在任何管道中安全使用。请注意,任何能够将提交推送到此 SCM 存储库的人都可以获得对 Jenkins 的无限访问权限。您需要 Overall/RunScripts 权限来配置这些库(通常这将授予 Jenkins 管理员)。

文件夹级共享库

创建的任何文件夹都可以有与之关联的共享库。此机制允许将特定库的范围限定为文件夹或子文件夹内的所有管道。

基于文件夹的库不被认为是 “可信的”:它们像典型的管道一样在 Groovy 沙箱中运行。

自动共享库

其他插件可能会添加动态定义库的方法。例如:GitHub 分支源插件提供了一个 “GitHub Organization Folder” 项,它允许脚本使用不受信任的库,例如:github.com/someorg/somerepo 无需任何额外配置。master 在这种情况下,将使用匿名签出从分支加载指定的 GitHub 存储库。

使用库

标记为 Load implicitly 的共享库允许管道立即使用任何此类库定义的类或全局变量。要访问其他共享库,Jenkinsfile 需要使用 @Library 注解,指定库的名称:

@Library('my-shared-library') _
/* Using a version specifier, such as branch, tag, etc */
@Library('my-shared-library@1.0') _
/* Accessing multiple libraries with one statement */
@Library(['my-shared-library', 'otherlib@abc1234']) _

注释可以位于脚本的 Groovy 允许注释的任何位置。当引用类库(带有 src/ 目录)时,通常注释会进入 import 语句中:

@Library('somelib')
import com.mycorp.pipeline.somelib.UsefulClass

对于仅定义全局变量(vars/)的共享库或仅需要全局变量的 Jenkinsfile 文件,annotation 模式 @Library('my-shared-library') _ 可能有助于保持代码简洁。本质上,不是注释不必要的 import 语句,而 _ 是注释符号。
不建议 import 使用全局变量/函数,因为这将强制编译器将字段和方法解释为 static 即使它们是实例。在这种情况下,Groovy 编译器会产生令人困惑的错误消息。

在脚本开始执行之前,在脚本编译期间解析并加载库。这允许 Groovy 编译器理解用于静态类型检查的符号的含义,并允许在脚本中的类型声明中使用它们,例如:

@Library('somelib')
import com.mycorp.pipeline.somelib.Helper

int useSomeLib(Helper helper) {
    helper.prepare()
    return helper.count()
}

echo useSomeLib(new Helper('some text'))

然而,全局变量在运行时被解析。

动态加载库

从 Pipeline: Shared Groovy Libraries 插件的 2.7 版开始,有一个用于在脚本中加载(非隐式)库的新选项:在构建期间的任何时间动态加载库 library 的步骤。

如果您只对使用全局变量/函数(来自 vars/ 目录)感兴趣,那么语法非常简单:

library 'my-shared-library'

此后,该库中的任何全局变量都将可供脚本访问。

也可以使用 src/ 目录中的类,但比较棘手。虽然 @Library 注释在编译之前准备了脚本的 “类路径”,但在 library 遇到步骤时,脚本已经被编译。因此,您不能 import 或以其他方式 “静态地” 引用库中的类型。

但是,您可以动态使用库类(无需类型检查),通过步骤返回值中的完全限定名称访问它们 library。可以使用类似 Java 的语法调用 static 方法:

library('my-shared-library').com.mycorp.pipeline.Utils.someStaticMethod()

您还可以访问 static 字段,并调用构造函数,就好像它们是名为 newstatic 方法一样:

def useSomeLib(helper) { // dynamic: cannot declare as Helper
    helper.prepare()
    return helper.count()
}

def lib = library('my-shared-library').com.mycorp.pipeline // preselect the package

echo useSomeLib(lib.Helper.new(lib.Constants.SOME_TEXT))

库版本

当检查 “隐式加载” 时,使用配置的共享库的 “默认版本”,或者仅通过名称引用库 @Library('my-shared-library') _。如果未定义 “默认版本”,则管道必须指定一个版本,例如:@Library('my-shared-library@master') _

如果在共享库的配置中启用了 “允许覆盖默认版本”,则 @Library 注释也可能会覆盖为库定义的默认版本。这还允许在必要时从不同版本加载具有 “隐式加载” 的库。

使用该 library 步骤时,您还可以指定一个版本:

library 'my-shared-library@master'

由于这是一个常规步骤,因此可以计算该版本,而不是像注释那样的常量;例如:

library "my-shared-library@$BRANCH_NAME"

将使用与 multibranch 相同的 SCM 分支加载库 Jenkinsfile。作为另一个示例,您可以通过参数选择一个库:

properties([parameters([string(name: 'LIB_VERSION', defaultValue: 'master')])])
library "my-shared-library@${params.LIB_VERSION}"

请注意,该 library 步骤可能不会用于覆盖隐式加载库的版本。它在脚本启动时已经加载,并且给定名称的库可能不会加载两次。

检索方法

指定 SCM 的最佳方式是使用 SCM 插件,该插件经过专门更新以支持新的 API 以检查任意命名版本(Modern SCM 选项)。

旧版 SCM

尚未更新以支持共享库所需的新功能的 SCM 插件仍可通过 Legacy SCM 选项使用。在这种情况下,包括 ${library.yourlibrarynamehere.version} 可以为该特定 SCM 插件配置分支/标记/引用的任何位置。这确保在签出库的源代码期间,SCM 插件将扩展此变量以签出相应版本的库。

动态检索

如果您在该 library 步骤中仅指定库名称(可选在 @ 之后的版本),Jenkins 将查找该名称的预配置库。(或者在 github.com/owner/repo 自动库的情况下,它将加载它。)

但您也可以动态指定检索方法,在这种情况下,无需在 Jenkins 中预定义库。这是一个例子:

library identifier: 'custom-lib@master', retriever: modernSCM(
  [$class: 'GitSCMSource',
   remote: 'git@git.mycorp.com:my-jenkins-utils.git',
   credentialsId: 'my-private-key'])

请注意,在这些情况下必须指定库版本。

编写库

在基础级别,任何有效的 Groovy 代码 都可以使用。不同的数据结构、实用方法等,例如:

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

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

访问步骤

库类不能直接调用 shgit 等步骤。但是,它们可以实现封闭类范围之外的方法,这些方法又会调用管道步骤,例如:

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

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

return this

然后可以从脚本管道调用它:

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

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

或者,steps 可以将一组显式使用传递 this 给库类、构造函数或仅一个方法:

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 接口。这确保了使用该类的流水线(如下例所示)可以在 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}"
  }
}

上面的示例显示了将脚本传递给一个 static 方法,从脚本管道调用如下:

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

定义全局变量

在内部,vars 目录中的脚本按需实例化为单例。为方便起见,这允许在单个 .groovy 文件中定义多个方法。例如:

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

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

// Jenkinsfile
@Library('utils') _

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

请注意,如果您希望将全局中的字段用于某些状态,请按如下方式对其进行注释:

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

def yourFunction....

声明式管道不允许对 script 块之外的对象进行方法调用。上面的方法调用需要放在 script 指令中:

// Jenkinsfile
@Library('utils') _

pipeline {
    agent none
    stages {
        stage ('Example') {
            steps {
                // log.info 'Starting' 
                script { 
                    log.info 'Starting'
                    log.warning 'Nothing to do!'
                }
            }
        }
    }
}
  • // log.info ‘Starting’ 此方法调用将失败,因为它在 script 指令之外。
  • script 访问声明式管道中的全局变量所需的指令。

在 Jenkins 加载并使用该库作为成功流水线运行的一部分后,在共享库中定义的变量只会显示在全局变量参考中。
避免在全局变量中保留状态
避免使用交互或保持状态的方法定义全局变量。改用静态类或实例化类的局部变量。

定义自定义步骤

共享库还可以定义行为类似于内置步骤的全局变量,例如:shgit。共享库中定义的全局变量必须全部使用小写或 “camelCased” 命名,以便管道正确加载2

例如:要定义 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}."
}

然后,管道将能够引用和调用此变量:

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

如果使用块调用,该 call 方法将收到一个 Closure。应明确定义类型以阐明步骤的意图,例如:

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

然后,管道可以像任何接受块的内置步骤一样使用此变量:

windows {
    bat "cmd /?"
}

定义更结构化的 DSL

如果你有很多相似的管道,全局变量机制提供了一个方便的工具来构建一个更高级别的 DSL 来捕获相似性。例如:所有 Jenkins 插件都是以相同的方式构建和测试的,所以我们可以编写一个名为 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 看起来更像是一个配置文件而不是一个程序,但这更复杂且容易出错,因此不推荐使用。

使用第三方库

虽然可能,但使用来自受信任的库访问第三方 @Grab 库存在各种问题,因此不建议这样做。建议的方法不是使用 @Grab,而是使用您选择的编程语言(使用您想要的任何第三方库)创建一个独立的可执行文件,将其安装在您的管道使用的 Jenkins 代理上,然后在您的管道中调用该可执行文件使用 batsh 步骤。

可以使用注释从受信任的库代码中使用通常在 Maven Central 中找到的第三方 Java 库。有关详细信息,请参阅 Grape 文档 ,但简单地说:@Grab

@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"
  }
  // …
}

第三方库默认缓存 ~/.groovy/grapes/ 在 Jenkins 控制器中。

加载资源

resources/ 外部库可以使用该 libraryResource 步骤从目录加载附加文件。该参数是一个相对路径名,类似于 Java 资源加载:

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

该文件作为字符串加载,适合传递给某些 API 或使用 writeFile

建议使用独特的包结构,以免意外与另一个库发生冲突。

预测试库更改

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

(即使为库请求的版本是一个分支,而不是像标签这样的固定版本,重放的构建将使用与原始构建完全相同的修订:库源将不会再次被检出。)

受信任的库当前不支持 “重播”。目前也不支持在 Replay 期间修改资源文件。

定义声明性管道

从 2017 年 9 月下旬发布的声明式 1.2 开始,您也可以在共享库中定义声明式管道。这是一个示例,它将根据内部版本号是奇数还是偶数来执行不同的声明式管道:

// 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 到目前为止,只能在共享库中定义整个s。这只能在 vars/*.groovy 中完成,并且只能在一个 call 方法中完成。在一个构建中只能执行一个声明式管道,如果您尝试执行第二个,您的构建将因此失败。


  1. en.wikipedia.org/wiki/Don’t_repeat_yourself ↩︎

  2. gist.github.com/rtyler/e5e57f075af381fce4ed3ae57aa1f0c2 ↩︎

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐