用 Jenkins 共享库复用流水线:作用域、版本、加载方式与测试

原文:Jenkins 文档贡献者,Extending with Shared Libraries。本文为中文翻译整理,依据 2026-10-05 读取的官方手册,保留完整技术流程与重要代码,并补充标明静态安全检查。Jenkins 网站内容采用 CC BY-SA 4.0;本译文按相同许可保留署名及修改说明。原文未提供个人作者署名。

当越来越多项目采用 Pipeline,检出代码、构建、测试和通知往往会出现重复模式。共享库把这些逻辑放进外部源代码仓库,再由各项目的 Jenkinsfile 加载,减少重复维护。这是原文所说的 DRY(Don’t Repeat Yourself)原则。库本身仍然是可执行代码:它的存放位置、权限、加载时机与版本,都会影响流水线行为。

Jenkins 共享库由 src 类、vars 步骤和 resources 资源组成;编译期 Library 与运行期 library 使用不同加载路径,可信库能访问控制器 API。
共享库的目录职责与加载时机。未完纪原创技术示意图。

1. 定义库名、来源与版本

一份共享库至少需要一个简短名称和代码获取方式,通常是 SCM,也可指定默认版本。Git 的分支、标签、提交哈希都可作为版本。配置还决定库是否默认隐式加载,或必须由脚本显式请求;管理员可以禁止脚本覆盖默认版本。

优先选用支持按名称检出任意版本的 Modern SCM,原文列举 Git 与 Subversion 插件。尚未支持这套接口的插件可选择 Legacy SCM,并在分支、标签或引用位置嵌入 ${library.yourLibName.version}。例如 Subversion 仓库 URL 可以设置为:

svnserver/project/${library.yourLibName.version}

随后使用 trunk、branches/dev、tags/1.0 之类的版本,检出时由插件展开。支持情况取决于已安装插件,应按当前实例的 Pipeline Syntax 生成器核对。

2. 仓库目录各司其职

(root)
+- src
|  +- org
|     +- foo
|        +- Bar.groovy
+- vars
|  +- foo.groovy
|  +- foo.txt
+- resources
   +- org
      +- foo
         +- bar.json

src/ 采用 Java 源码目录式的包结构,例如 org/foo/Bar.groovy 定义 org.foo.Bar。执行 Pipeline 时此目录加入 classpath。vars/ 内的脚本以文件名作为流水线可见的全局变量名:vars/log.groovy 中有 info 方法,即可用 log.info "hello world" 调用。同一文件能包含多个方法。

Groovy 文件名应是合法的 Groovy/Java 标识符,惯例是 camelCase。可选的同名 .txt 文件是帮助文档,由系统配置的 markup formatter 处理,因此内容可以实际采用 HTML 或 Markdown,但扩展名必须为 .txt。只有导入该库的 Pipeline 作业成功运行过一次,帮助才会出现在作业侧栏可访问的 Global Variable Reference 中。

src 和 vars 中的 Groovy 代码都经历 Scripted Pipeline 相同的 CPS 转换。resources/ 保存非 Groovy 文件,供外部库的 libraryResource 加载;原文指出内部库不支持此功能。根目录的其他目录保留给未来扩展。

3. 全局、文件夹和自动库的权限差别

在 Manage Jenkins → System → Global Trusted Pipeline Libraries 中可以注册全局可信库,系统中任何 Pipeline 都能调用。可信库可访问 Java、Groovy、Jenkins 内部 API、插件和第三方库,适合把原本危险的底层 API 封装成范围受限的高层功能。但拥有该库仓库推送权限的人,可能获得 Jenkins 的无限制访问能力。配置可信库需要 Overall/RunScripts,通常由管理员拥有;仅有 Overall/Manage 时,只能配置在 Groovy sandbox 中运行的全局不可信库。

文件夹也能配置共享库,其作用域包括该文件夹及子文件夹内的 Pipeline。文件夹级库始终是不可信库。其他插件还可临时定义库,例如 Pipeline: GitHub Groovy Libraries 支持以 github.com/someorg/somerepo 命名,匿名检出该仓库的 master 分支,作为不可信库使用。这里的 master 是原文插件示例,不保证任意仓库都有此分支。

编辑补充:“可信”代表给予执行能力,不代表代码经过安全认证。应对库仓库实施评审、受保护分支及最小写入权限;生产消费端宜使用已审核、可追溯的固定提交。将任意构建参数直接变成可信库版本选择,可能绕过原本预期的发布边界。

4. 在编译期用 @Library 加载

勾选 Load implicitly 后,流水线可以直接使用该库中的类和全局变量。其他库需要 Jenkinsfile 注解:

@Library('my-shared-library') _
@Library('my-shared-library@1.0') _
@Library(['my-shared-library', 'otherlib@abc1234']) _

这三行分别展示默认版本、指定版本、一次导入多个库,实际按需选择使用。注解可放在 Groovy 允许注解的位置。涉及 src 类时,惯常附在 import 上:

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

只使用 vars 全局变量时,给符号下划线加注解即可,无需多余的 import。不建议 import 全局变量或函数,因为编译器可能将字段、方法视为 static,导致难以理解的错误。共享库在脚本开始执行之前的编译阶段解析和加载,因此可参与静态类型检查及类型声明:

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

int useSomeLib(Helper helper) {
    helper.prepare()
    return helper.count()
}
echo useSomeLib(new Helper('some text'))

需要区分:类所在的 classpath 在编译前准备好,全局变量本身则在运行时解析。原文另附共享库资源文件视频,视频示例仓库链接位于其说明中;本文按书面正文翻译,未声称观看或复现视频。

5. 用 library 在运行期动态加载

从 Pipeline: Shared Groovy Libraries 插件 2.7 起,可以在构建运行中用 library 步骤加载尚未隐式加载的库。对 vars 函数:

library 'my-shared-library'

后续即可访问库中的全局变量。src 类也可使用,但遇到 library 时脚本已编译完成,不能再 import,也不能把其类作为静态类型声明。应从 library 的返回对象沿完整包名访问:

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

def useSomeLib(helper) {
    helper.prepare()
    return helper.count()
}
def lib = library('my-shared-library').com.mycorp.pipeline
echo useSomeLib(lib.Helper.new(lib.Constants.SOME_TEXT))

这里 helper 不声明 Helper 类型;静态字段可直接访问,构造器以名为 new 的方法调用。动态方式牺牲静态类型检查,以换取运行时选择。

6. 默认版本、运行时版本与获取方式

隐式加载,或只按名称引用库时,使用配置的 Default version。若没有默认版本,使用方必须明确指定,例如:

@Library('my-shared-library@master') _

开启 Allow default version to be overridden 后,@Library 可覆盖默认版本,包括隐式库的版本。library 也支持版本,并可计算版本值:

library 'my-shared-library@master'
library "my-shared-library@$BRANCH_NAME"

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

第二种形式让库使用与多分支 Jenkinsfile 同名的分支;第三种由参数选择。library 无法覆盖已隐式加载的库,因为开始执行时该库已经载入,同一名称的库不能加载两次。

Modern SCM 优先;Legacy SCM 需要在 SCM 配置中使用版本变量,例如 svn://svn.example.com/pipeline-library/branches/${library.my-shared-library.version}。原文截图中的默认版本 Stable、缓存刷新 37 分钟等只用于演示配置,不是推荐通用值。

还可以在 library 步骤中直接给出获取方式,无需事先在 Jenkins 注册:

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

此时必须指定版本。credentialsId 是 Jenkins 凭据存储中的引用,不是私钥内容;精确语法应由当前 SCM 对应的 Pipeline Syntax 生成。仓库地址、身份、可选择的版本都应受控制,不能将不可信输入自由拼接到高权限库来源。

7. 缓存的包含与排除规则

启用库版本缓存可以缩短获取时间。Versions to exclude 接受完整版本名或子串,例如 feature/versions_to_exclude 排除该版本,test/ 排除名称含 test/ 的版本。Versions to include 则仅让匹配版本进入缓存,同样支持完整名称或子串;为空时默认允许缓存。若一个版本同时匹配包含和排除,排除优先。缓存是取回优化,不替代版本固定、评审或访问控制。

8. 编写类,并显式传入 Pipeline 步骤

基础数据结构可以是普通 Groovy 类:

// src/org/foo/Point.groovy
package org.foo
class Point {
    float x, y, z
}

库中的普通类不能直接调用 sh、git 等 Pipeline 步骤。一种方法是在脚本主体定义方法,再返回 this:

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

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

// Jenkinsfile 中调用
def z = new org.foo.Zot()
z.checkOutFrom(repo)

这种方式有局限,例如无法在上述脚本式写法中声明超类。另一种方法把流水线脚本的 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}"
    }}

// Jenkinsfile
@Library('utils') import org.foo.Utilities
def utils = new Utilities(this)
node {
    utils.mvn 'clean package'
}

当类保存状态时,应实现 Serializable,才能让使用它的 Pipeline 正确暂停和恢复。需要 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}"
    }
}

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

静态审查:上两段 mvn 方法保持原文写法,用于说明如何访问步骤。若 args 来自构建参数、分支名或其他不可信输入,插值进入 sh 字符串会产生 Shell 命令注入风险;工具路径、HOME 中的空格也没有得到可靠引用。应把可执行操作限制为受审核的固定命令或严格白名单参数,并正确处理路径及参数引用。这里没有提供一个未经实测的“通用安全 Shell 拼接器”。

9. vars 全局变量要保持无状态

vars 脚本按需实例化为单例,因此可把多个辅助方法放进同一个文件:

// 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!'

Declarative Pipeline 不允许在 script 之外对对象调用方法,见 JENKINS-42360。因此在声明式流水线中应使用:

@Library('utils') _
pipeline {
    agent none
    stages {
        stage('Example') {
            steps {
                script {
                    log.info 'Starting'
                    log.warning 'Nothing to do!'
                }
            }
        }
    }
}

只有库被加载、使用且流水线成功运行后,变量才出现在 Global Variables Reference。共享库全局变量应充当函数集合,不保存构建状态;控制器重启后这种状态可能丢失。应使用适当的类实例及局部变量,并为跨暂停保存的对象满足序列化要求。原文允许只读字段,但不鼓励把可变状态藏在 vars 中:

@groovy.transform.Field
def yourField = "YourConstantValue"

10. 把函数封装成自定义步骤

vars 名称须使用全小写或 camelCase,才能按预期加载。定义 call 方法后,全局变量就能像内置步骤一样调用:

// vars/sayHello.groovy
def call(String name = 'human') {
    echo "Hello, ${name}."
}

// Jenkinsfile
sayHello 'Joe'
sayHello()

如果步骤接受一个代码块,call 接收 Closure,显式写出该类型可以表明意图:

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

// Jenkinsfile
windows {
    bat "cmd /?"
}

多个项目的流水线结构相似时,可进一步构建领域专用语言。原文以 Jenkins 插件构建为例:

// 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(Scripted Pipeline)
buildPlugin name: 'git'

此示例须先配置全局或文件夹库,通知中的省略号是原文示意,使用时需提供真实配置。构建仓库内容及 Maven 构建脚本本身可执行代码,不能因只出现固定 mvn 命令就忽略仓库信任问题。原文也提到利用 Closure.DELEGATE_FIRST 的 builder 模式,但因复杂且容易出错而不推荐。

11. 第三方库与资源文件

可信库理论上可通过 @Grab 从 Maven Central 等来源加载第三方 Java 库:

@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/。推荐把所需库封装为独立可执行程序,安装在流水线使用的 agent 上,再用 sh 或 bat 调用。3.4.1 是原文历史示例版本,并不代表当前安全版本;本文没有检查其依赖漏洞数据库,也没有下载或执行依赖。

外部共享库可从 resources/ 读取辅助文件:

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

返回值是字符串,可传给 API 或用 writeFile 写入工作区。路径类似 Java 资源加载,采用独有包结构可避免多个库发生资源命名冲突。

12. 用 Replay 和 PR 验证修改

使用不可信库的构建出现问题时,可以通过 Replay 编辑一个或多个库源文件并重新运行;确认行为后,从构建状态页获取差异,应用到库仓库并提交。即使原构建请求的是分支,Replay 仍使用原构建的精确 revision,不会重新检出最新分支。原文指出:可信库不支持 Replay,Replay 也不支持修改资源文件。

如果共享库在 GitHub,且库 SCM 使用 GitHub,可以在消费端 Jenkinsfile 顶部指定待测 PR 的引用:

@Library('my-shared-library@pull/<your-pr-number>/head') _

Assembla、Bitbucket、Gitea、GitLab、Tuleap 等服务需遵循各自 PR/MR 分支命名规则。官方案例是 jenkins-infra/pipeline-library 的 PR 123,由 专用测试仓库 验证:

@Library('pipeline-library@pull/123/head') _
buildPlugin(
    useContainerAgent: true,
    configurations: [
        [platform: 'linux', jdk: 21],
        [platform: 'windows', jdk: 17],
    ])

编辑提醒:待测 PR 等同将未合并代码放进运行环境;需隔离 agent、限制凭据和网络访问,可信库尤其不能允许任意外部贡献直接拿到控制器能力。本文仅说明官方测试方法,未运行该 PR 或示例构建。

13. 在共享库中定义 Declarative Pipeline

从 2017 年 9 月发布的 Declarative 1.2 起,共享库也能定义完整声明式流水线。原文用构建编号奇偶选择两个不同 pipeline:

// 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())

这里必须在 vars/*.groovy 的 call 方法内定义完整 pipeline。一次构建只能执行一个 Declarative Pipeline,尝试执行第二个会失败。不能把“可以写在库里”理解为可以随意拼接多个完整 pipeline。

14. 本文审查范围

本稿依据完整书面源文整理,保留定义、目录、作用域、两种加载方式、版本获取、缓存、步骤访问、全局状态、自定义 DSL、第三方库、资源、Replay、声明式流水线和 PR 测试的全部实质主题。代码仅做静态检查:已指出 args 拼接、可变版本选择和可信库仓库权限的实际风险;未发现真实硬编码秘密,但不代表不存在漏洞。本文没有 Jenkins 控制器、agent、SCM 凭据或插件运行测试,示例输出与成功行为均属于源文说明,不是本次执行结果。

原文配置界面图

以下七幅为Jenkins官方原图,按原比例保留,便于对应前述选项;并非本文执行截图。图像及文档来源:Jenkins文档贡献者,原文。原图未改动,中文图注由未完纪补充。

全局可信库与全局不可信库入口;前者不受Groovy sandbox限制。
全局可信库与全局不可信库入口;前者不受Groovy sandbox限制。 图:Jenkins项目,CC BY-SA 4.0。
共享库名称、默认版本、隐式加载、允许覆盖版本、纳入最近变更与缓存开关。
共享库名称、默认版本、隐式加载、允许覆盖版本、纳入最近变更与缓存开关。 图:Jenkins项目,CC BY-SA 4.0。
Modern SCM使用Git仓库地址、凭据选择和分支发现;截图中的地址仅为示例。
Modern SCM使用Git仓库地址、凭据选择和分支发现;截图中的地址仅为示例。 图:Jenkins项目,CC BY-SA 4.0。
Legacy SCM以Subversion演示版本占位、检出深度和外部定义行为;以实际安装插件为准。
Legacy SCM以Subversion演示版本占位、检出深度和外部定义行为;以实际安装插件为准。 图:Jenkins项目,CC BY-SA 4.0。
缓存排除列表:完整名称或子串匹配。
缓存排除列表:完整名称或子串匹配。 图:Jenkins项目,CC BY-SA 4.0。
缓存包含列表:仅缓存匹配的版本;空列表默认不限制。
缓存包含列表:仅缓存匹配的版本;空列表默认不限制。 图:Jenkins项目,CC BY-SA 4.0。
同一版本同时出现在包含与排除时,排除优先。
同一版本同时出现在包含与排除时,排除优先。 图:Jenkins项目,CC BY-SA 4.0。

本文译文、中文补充及源图按Creative Commons Attribution-ShareAlike 4.0 International发布;修改日期2026-10-05,按原样提供,不作担保,不暗示Jenkins项目认可。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容