Jenkins Shared Library 企业复用
验证:未记录
- 适用版本
- 未记录
- 验证范围
- 尚未登记验证记录。
Shared Library(共享库)把多个 Jenkinsfile 共用的 Pipeline 步骤放进独立 Git 仓库,由使用方按受控版本加载。适合复用测试、报告与制品处理;业务项目仍负责自己的测试脚本和依赖锁文件,生产权限由受控发布环境提供。
调用路径:业务 Jenkinsfile → vars/verifyProject.groovy 的 call → src/ 类、其他 vars/ 全局变量和 Pipeline 步骤。业务 Jenkinsfile 用 @Library('platform-ci') _ 加载库,再调用 verifyProject('go')。
阅读顺序:第 1~5 节完成建库、加载和版本发布;第 6~9 节解释类、CPS、Declarative 与安全边界;最后用第 10~11 节验证和排障。
先记住五条规则#
vars/暴露可直接调用的全局步骤;src/提供需要按包名导入或动态访问的类。@Library在编译前加载;运行时library(...)即使写在文件顶部,也不能支持后续静态import。- Groovy 构造器不经过 CPS 转换,Pipeline 步骤要在对象创建后的普通方法中执行。
- 跨暂停点保留的状态必须可序列化;
@NonCPS方法不能调用 Pipeline 步骤。 - Trusted Library 的仓库写权限近似 Jenkins 管理权限;库检索凭据不是业务发布凭据。
1. 建库并固定接口#
独立 Git 仓库可以使用以下结构:
platform-ci/
├── vars/
│ ├── verifyProject.groovy # 全局步骤 verifyProject(...)
│ ├── verifyProject.txt # 该步骤的帮助说明
│ └── ciLog.groovy # 全局变量 ciLog,提供 info(...)
├── src/
│ └── com/example/ci/
│ └── BuildPlan.groovy # 可 import 的普通 Groovy 类
├── resources/
│ └── com/example/ci/
│ └── notice.txt # libraryResource 可读取的静态文件
└── README.md| 目录 | Jenkins 如何识别 | 适合放什么 |
|---|---|---|
vars/ |
文件名成为全局调用名;call 方法可写成 verifyProject('go') |
面向 Jenkinsfile 的步骤编排;全局变量应保持无状态 |
src/ |
按 package 和类名 import,路径类似 Java 包结构 |
参数校验、纯逻辑、复用的类 |
resources/ |
用 libraryResource 按相对路径读取 |
文本模板等静态资源,不会自动出现在 workspace |
vars/verifyProject.txt 可写明参数、前置工具和失败语义,例如 verifyProject('go') 要求 Go 工具链、verifyProject('node') 要求 Node.js 与项目的 lint/test 脚本。使用该库的 Pipeline 成功运行后,帮助才可能出现在 Jenkins 的 Global Variable Reference。vars/ 文件名通常采用小写开头的 camelCase。
先写负责选择固定命令的 src/com/example/ci/BuildPlan.groovy,package 与目录对应:
package com.example.ci
class BuildPlan {
static String commandFor(String kind) {
switch (kind) {
case 'go':
return 'go test -race ./... && go vet ./...'
case 'node':
return 'npm ci && npm run lint && npm test'
default:
throw new IllegalArgumentException("Unsupported project kind: ${kind}")
}
}
}再写 vars/ciLog.groovy;它提供 ciLog.info(...) 方法,不需要从 src/ 导入:
def info(String message) {
echo "CI: ${message}"
}最后写 vars/verifyProject.groovy,把类、另一个全局变量和 Jenkins 内置步骤串起来:
import com.example.ci.BuildPlan
def call(String kind) {
String command = BuildPlan.commandFor(kind)
ciLog.info("Verifying ${kind} project")
timeout(time: 15, unit: 'MINUTES') {
sh command
}
}BuildPlan 是 src/ 类,所以要 import com.example.ci.BuildPlan;ciLog 是 vars/ 全局变量,直接按名称调用,不要写 import ciLog。timeout、sh 是 Jenkins 的 Pipeline 步骤。BuildPlan 不接受任意 shell 字符串,避免把未经校验的 Jenkinsfile 参数拼进命令;工具版本由 Agent 镜像控制。
src/ 类适合优先写成不依赖 Jenkins 步骤的纯逻辑。如果类确实要调用 sh 或 echo,需从调用方显式传入脚本上下文,例如 new Helper(this),再在类中调用 steps.sh(...);保存跨 Pipeline 暂停点的实例状态时还要考虑 Serializable。简单编排放在 vars/ 更容易维护。
2. 注册受限库#
- 创建 Jenkins Folder(任务目录),在目录设置中注册 Pipeline Library,名称
platform-ci。 - 选择 Modern SCM → Git,填写只读仓库地址和只读凭据。
- Default version 填审核通过的完整 Git commit SHA,关闭允许任务覆盖版本,关闭隐式加载。
- 目录库运行于 Groovy sandbox(受限执行环境)。若没有目录库设置,核对 Pipeline: Shared Groovy Libraries 与 Folders 插件。
platform-ci 是 Jenkins 注册的库名,不是 Groovy package 名,也不要求与 Git 仓库名相同。关闭隐式加载后,业务 Jenkinsfile 必须显式写 @Library('platform-ci') _;关闭版本覆盖后,业务任务不能用 @Library('platform-ci@其他版本') _ 自行选择新提交。@Library 在 Jenkinsfile 编译前加载库,所以能在文件顶部 import src/ 中的类。
全局 Trusted Library 可绕过沙箱,其仓库写权限近似 Jenkins 管理权限;不要为了消除审批提示就改成受信任库。官方说明
3. @Library 的 _、版本和 Git 地址#
@Library('platform-ci') _ 中的 _ 是什么#
Groovy 的 @Library 是注解,必须附着在一个语法元素上。只想使用 vars/ 提供的 verifyProject(...)、ciLog.info(...) 等全局变量时,文件顶部通常没有需要 import 的类,就在注解后写一个 _ 作为占位:
@Library('platform-ci') _
// 后面直接使用 vars/verifyProject.groovy 暴露的名字
verifyProject('go')_ 不是库对象、参数或特殊的 Jenkins 步骤,不会调用任何函数。若下一行要导入 src/ 类,可把注解直接附在 import 上,此时不用 _:
@Library('platform-ci')
import com.example.ci.BuildPlan不能把两种写法理解为“有 _ 才能用 vars/,没有 _ 才能用 src/”:两者都会加载同一个库;差别只是注解附着的位置。单独写一行 @Library('platform-ci') 而后面没有 import 等可附着元素,不是完整写法。上面 verifyProject('go') 是调用示意;Declarative Pipeline 的完整放置方式见下一节。
已注册库:默认版本、指定版本、多个库#
| 写法 | 结果 | 前提 |
|---|---|---|
@Library('platform-ci') _ |
加载 Jenkins 注册库的 Default version | 已注册名为 platform-ci 的库 |
@Library('platform-ci@main') _ |
请求 Git 分支 main |
该库允许覆盖默认版本,且 SCM 能解析此版本 |
@Library('platform-ci@v1.2.0') _ |
请求标签或其他 SCM ref | 同上 |
@Library(['platform-ci', 'another-lib@v2']) _ |
一次加载多个库 | 两个库均已注册;显式版本需允许覆盖 |
@ 后面是库版本,可以是分支、标签或 Git commit SHA,并不是 Git 仓库 URL。当前文档的企业示例将 Default version 固定到审核过的 SHA,且关闭“Allow default version to be overridden”,所以业务 Jenkinsfile 应使用 @Library('platform-ci') _;上表中的 @main/@v1.2.0 在该配置下不能自行切换。若未设置 Default version,则引用时必须显式给版本。
先选加载方式:位置和类访问能力#
| 方式 | 能否写在 Jenkinsfile 顶部 | Git 仓库从哪里来 | src/ 类如何使用 |
|---|---|---|---|
@Library('platform-ci') _ |
可以,编译前加载 | Jenkins 已注册库的 SCM 配置 | 主要用于 vars/;要静态导入类,直接使用下一行的注解加 import 写法 |
@Library('platform-ci') 下一行 import com.example.ci.BuildPlan |
可以,注解附在 import 上 |
Jenkins 已注册库的 SCM 配置 | 直接用 BuildPlan 类型、new BuildPlan(...)、静态方法等 |
library('platform-ci') |
Scripted Pipeline 可作为顶层运行语句;Declarative 放在 script {} |
Jenkins 已注册库的 SCM 配置 | 已编译,不能再写静态 import;从返回值按完整包名动态访问 |
library(identifier: ..., retriever: modernSCM(...)) |
Scripted Pipeline 可作为顶层运行语句;Declarative 放在 script {} |
Jenkinsfile 中的 remote、credentialsId 和版本 |
同上,从返回值动态访问;可不预先注册库 |
| 配置了 Load implicitly 的库 | Jenkinsfile 可不写加载语句 | Jenkins 已注册库的 SCM 配置 | 编译前已加载,可静态 import;是否启用由管理员配置 |
“写在文件顶部”不等于“编译前加载”:
@Library是编译期注解,library(...)是运行时步骤。普通 Groovyimport在 Jenkinsfile 执行前就由编译器处理,因此不能靠前一行的运行时library(...)让后面的import生效。
某些可选插件能提供特殊格式的自动共享库名称,例如 GitHub Groovy Libraries 的 github.com/owner/repo;那是插件提供的专门检索器,并不表示任意 Git URL 都能放进 @Library(...)。私有企业 Git 仓库使用已注册库,或使用下面明确填写 remote 与 credentialsId 的动态检索方式。
直接写 Git 地址和凭据 ID:运行时 library 步骤#
@Library('https://git.example.com/platform-ci.git@main') _ 不是“直接从该 URL 加载”的写法:@Library 参数先按库名查找 Jenkins 已注册的配置。需要在 Jenkinsfile 中提供 Git 地址、版本和检索凭据时,使用动态 library 步骤。下面是 Declarative Pipeline 的完整写法:
pipeline {
agent { label 'linux-test' }
stages {
stage('Verify') {
steps {
checkout scm
script {
def loaded = library(
identifier: 'example-ci@main',
retriever: modernSCM(
[$class: 'GitSCMSource',
remote: 'git@git.example.com:platform/platform-ci.git',
credentialsId: 'platform-ci-git-readonly']
)
)
echo loaded.com.example.ci.BuildPlan.commandFor('go')
verifyProject('go')
}
}
}
}
}| 字段 | 本例作用 | 对应位置 |
|---|---|---|
identifier: 'example-ci@main' |
此次动态加载的库名和 Git 版本 | example-ci 是本次调用名,与上文已注册的 platform-ci 区分;main 是检索的分支,可改为受控标签或提交 |
retriever: modernSCM(...) |
选择共享库的源码检索方式 | Jenkins 的 Modern SCM 检索器 |
$class: 'GitSCMSource' |
使用 Git SCM Source | 需要相应 Git/SCM 插件支持 |
remote: 'git@git.example.com:platform/platform-ci.git' |
共享库的 Git 仓库地址 | 不是业务项目 checkout scm 的仓库地址 |
credentialsId: 'platform-ci-git-readonly' |
用来读取该私有 Git 仓库的 Jenkins 凭据 ID | Jenkins Credentials 中已创建、此 Job 可使用的只读 SSH 私钥凭据;传的是 ID,不是密钥正文 |
loaded.com.example.ci.BuildPlan.commandFor('go') 是动态访问类的静态方法,不是 Groovy import。上例既调用了 src/ 类,也调用了 vars/verifyProject.groovy 暴露的全局步骤。credentialsId 只用于拉取共享库源码,不会自动把凭据传给业务 shell。业务发布/API 凭据应另外按最小范围绑定,见凭据与权限。
如果使用 Scripted Pipeline,library(...) 可以写在 Jenkinsfile 的顶层、node 前面。它依然是运行时加载,并不会让文件顶部的静态 import 生效:
def loaded = library(
identifier: 'example-ci@main',
retriever: modernSCM(
[$class: 'GitSCMSource',
remote: 'git@git.example.com:platform/platform-ci.git',
credentialsId: 'platform-ci-git-readonly']
)
)
node('linux-test') {
checkout scm
echo loaded.com.example.ci.BuildPlan.commandFor('go')
def runner = loaded.com.example.ci.BuildRunner.new(this)
runner.verify('go')
}顶层 library 负责加载共享库,node 才分配 Agent 和 workspace。这里的 BuildRunner.new(this) 动态实例化第 6 节的类,this 是当前 Pipeline 脚本上下文;同一个库的 verifyProject('go') 全局步骤也可在 node 内调用。Declarative Jenkinsfile 以 pipeline { ... } 为主体,不能把 library(...) 当作普通步骤放在它前面;在 Declarative 中使用前一个 script { ... } 示例。企业生产任务若要求统一固定版本和集中审查,继续使用上节已注册并锁定版本的目录库。
如果要调用动态库 src/ 类的构造器,可以把 .new(...) 当作动态代理上的构造方法;下面假设 other-ci 是另一个已注册、允许选择 main 的库,且包含第 6 节的 BuildRunner 类。代码应放在 Scripted Pipeline 的 node { ... } 或 Declarative 的 script { ... } 内:
def loaded = library('other-ci@main')
def runner = loaded.com.example.ci.BuildRunner.new(this)
runner.verify('go')这里的 this 是当前 Pipeline 脚本上下文,让 BuildRunner 能调用 Pipeline 步骤。动态类不能写成 BuildRunner runner 这样的静态类型声明;同一个库名在一次运行中也不能反复加载不同版本。
参考:Jenkins Shared Libraries:Using libraries、Loading libraries dynamically、Dynamic retrieval。
4. 在真实应用验证#
把下列 Jenkinsfile 放入已有 Go 服务仓库:
@Library('platform-ci') _
pipeline {
agent { label 'linux-test' }
options { skipDefaultCheckout(true); timestamps() }
stages {
stage('Verify') {
steps {
deleteDir()
checkout scm
verifyProject('go')
script {
ciLog.info('Verification finished')
}
}
}
}
post { always { deleteDir() } }
}verifyProject('go') 直接调用 vars/verifyProject.groovy 的 call;后者调用 BuildPlan.commandFor('go')、ciLog.info(...) 和 sh。Node.js 服务改成 verifyProject('node'),仓库必须提供实际 lint、test 脚本。ciLog.info(...) 属于对象方法调用,在 Declarative Pipeline 的 steps 中需要放进 script { ... },而 verifyProject('go') 这样的全局步骤调用可直接写在 steps 中。
若 Jenkinsfile 本身需要使用 src/ 类,可把 @Library 注解放在 import 上:
@Library('platform-ci')
import com.example.ci.BuildPlan
pipeline {
agent any
stages {
stage('Show command') {
steps {
script {
echo BuildPlan.commandFor('go')
}
}
}
}
}这是独立的导入示例;实际构建仍应调用 verifyProject('go'),让超时与日志逻辑一同生效。vars/verifyProject.groovy 不用 import;src/com/example/ci/BuildPlan.groovy 则使用包名导入。不要把 vars/ 全局变量当成普通静态类导入。
共享库中的 resources/com/example/ci/notice.txt 可在 vars/ 步骤里这样读取:
def note = libraryResource('com/example/ci/notice.txt').trim()
echo note
// 若外部工具需要文件,再写入当前 workspace:
writeFile file: 'notice.txt', text: notelibraryResource 返回文件文本;资源不会自动复制到业务仓库 workspace。不要把凭据放入共享库的静态资源。运行时 library 'platform-ci' 也能动态加载非隐式库,但发生在 Jenkinsfile 编译之后,不能随后用普通 import 静态引用该库的 src/ 类;固定版本的流水线使用上面的 @Library 更直接。
先验证成功,再提交一个失败的业务单元测试,流水线必须失败;未知项目类型也必须明确失败。还要检查 Agent 缺少工具时的失败信息、vars/verifyProject.txt 帮助显示以及库升级后 Go/Node 两类项目的兼容性。
5. 发布与回退#
先在测试 Folder 将默认提交指向候选版本,跑 Go 和 Node 两类真实项目;验证通过再修改生产 Folder 固定提交。保留上一提交号,失败恢复版本配置并重跑。库默认版本、目录权限与仓库保护规则也纳入变更审计。
验收:应用维护者不能覆盖库版本,库修改不能直接进入生产;破坏性接口变更按新版本逐团队迁移。
6. src/ 类如何调用 Pipeline 步骤#
vars/ 脚本可直接使用 sh、echo、timeout 等步骤;src/ 中的普通类没有这些隐式方法。要让类执行步骤,应把调用方的脚本上下文显式传入,便于看出依赖和测试:
Pipeline 方法会经过 CPS 转换,Groovy 构造器不会。不要在构造器中调用 Pipeline 步骤,也不要间接调用会执行这些步骤的方法。
Jenkins 会对 Pipeline 方法做 CPS 转换,以便在步骤暂停时保存执行状态,并在 Controller 重启后恢复。sh、stage、timeout 等步骤都在这条执行路径上。构造器没有经过该转换;从构造器进入 CPS 方法可能触发 expected to call ...<init> but wound up catching ... 之类的方法不匹配,执行结果也可能异常。构造器只保存参数,创建对象后再调用普通方法执行步骤。
错误与正确写法的差别:
// 错误:构造器直接调用 Pipeline 步骤
class BadRunner {
BadRunner(steps) {
steps.sh('make test')
}
}
// 正确:先构造,再从普通方法调用步骤
class Runner implements Serializable {
def steps
Runner(steps) { this.steps = steps }
void runTests() { steps.sh('make test') }
}
// 在有 Agent 的 Pipeline 步骤中:new Runner(this).runTests()下面的 BuildRunner 是同一正确模式的完整版本,包含命令选择、日志和超时:
// src/com/example/ci/BuildRunner.groovy
package com.example.ci
class BuildRunner implements Serializable {
private final def steps
BuildRunner(steps) {
this.steps = steps
}
void verify(String kind) {
String command = BuildPlan.commandFor(kind)
steps.echo("Verifying ${kind}")
steps.timeout(time: 15, unit: 'MINUTES') {
steps.sh(command)
}
}
}// vars/verifyWithRunner.groovy
import com.example.ci.BuildRunner
def call(String kind) {
new BuildRunner(this).verify(kind)
}Jenkinsfile 加载库后可直接写 verifyWithRunner('go')。this 是 vars/ 脚本上下文;类通过 steps.echo、steps.sh 调用 Pipeline 步骤。跨暂停点保存状态的类及其字段必须能安全序列化。BuildRunner 因此实现 Serializable;简单场景仍用第 1 节的 vars/verifyProject.groovy,不必为了封装而多建一个类。
src/ 类若只做纯计算,可像 BuildPlan 一样使用普通静态方法,无须传 this。如果要读 env.BRANCH_NAME、params 等构建上下文,优先由 vars/ 将所需的字符串/布尔值传给类;不要让纯逻辑类暗中依赖 Jenkins 的全局绑定。import 加载的是类,不会把 src/ 类变成可直接调用的 Pipeline 步骤。
7. CPS、序列化与 @NonCPS#
先把 Pipeline 想成一份可以夹书签的执行清单。执行到耗时的 sh 时,Jenkins 不只要记住“现在在等测试结束”,还要记住“测试结束后该执行哪一行”和“后面还会用到哪些变量”。这样 Controller 重启后,才能从保存的位置继续。
def version = '1.2.3'
echo '开始测试'
sh 'make test' // 可能运行很久;Pipeline 在这里等待
echo "测试完成,版本 ${version}" // 恢复后还要知道下一步和 version 的值普通程序主要靠内存里的调用栈记住执行位置;进程重启后,那份调用栈就没了。Jenkins 把 Jenkinsfile 和共享库的大部分 Groovy 方法改写成可暂停、可保存“接下来做什么”的形式,这种改写叫 CPS(Continuation-Passing Style,续延传递)。它保存的是 Pipeline 的必要执行状态,不是给整个 Controller JVM 或 Agent 进程拍快照;sh 的命令仍在 Agent 上运行。
一句话理解:CPS 让 Pipeline 带着“下一步在哪、还需要什么变量”的书签等待耗时步骤,并在恢复时继续执行。写普通 Jenkinsfile 不需要手工实现 CPS,知道它的边界即可。
由此能推导出两个常见限制:跨暂停点仍要使用的对象必须能被序列化;没有经过 CPS 转换的构造器或 @NonCPS 方法不能调用需要暂停的 Pipeline 步骤。这也是普通 Groovy 在本地能跑、放进 Jenkins Pipeline 却报错的原因。
| 常见现象 | 原因 | 改法 |
|---|---|---|
java.io.NotSerializableException 出现在 sh、sleep 等步骤附近 |
活跃的局部变量或类字段持有不能序列化的对象,如打开的流、正则 Matcher、某些 JSON 解析结果或 Jenkins 内部对象 |
在进入 Pipeline 步骤前提取成 String、数字、简单 List/Map;及时结束对象作用域;不要把大对象放到类字段 |
expected to call ... but wound up catching ... |
CPS 代码和非 CPS 方法/闭包混用 | 定位日志中的两个方法,简化调用;确有必要时只把纯计算封装进 @NonCPS 方法 |
| 构造对象时出现 CPS mismatch | 构造器调用了 CPS 转换的方法或 Pipeline 步骤 | 构造器只赋简单字段,执行步骤放到构造完成后的普通方法 |
@NonCPS 方法内 sh、node、timeout 行为异常 |
非 CPS 方法不能调用 CPS Pipeline 步骤 | 移除 @NonCPS,或把步骤调用移到外层 Pipeline 方法 |
例如排序遇到 CPS 闭包兼容问题时,可把不调用任何 Pipeline 步骤的小段纯计算放入 @NonCPS,并返回易于序列化的结果:
// src/com/example/ci/Names.groovy
package com.example.ci
import com.cloudbees.groovy.cps.NonCPS
class Names {
@NonCPS
static List<String> byLength(List<String> names) {
names.toSorted { a, b -> a.length() <=> b.length() }
}
}不要在 @NonCPS 方法中调用 sh、node、timeout 等 Pipeline 步骤,也不要用它掩盖序列化问题。vars/ 全局变量保持无状态,避免用可变字段缓存构建状态;Controller 重启后这类字段并不可靠。需要处理大量 JSON、HTTP 响应或复杂构建逻辑时,让 Agent 上的脚本/程序处理,只把少量结果返回 Groovy,避免在 Controller 上消耗大量 CPU 和内存。CPS 方法不匹配、Pipeline 最佳实践
8. Declarative Pipeline 与步骤设计边界#
vars/foo.groovy 可以定义 def call(...),让它像步骤一样在 Jenkinsfile 的 steps 中调用。若 vars/foo.groovy 定义的是 def info(...) 等其他方法,Declarative steps 中要通过 script { foo.info(...) } 调用。src/ 类的方法调用也应放进 script。构造器与 Pipeline 步骤的 CPS 限制见第 6 节。
也可以把整条 Declarative Pipeline 定义在共享库的某个 vars/*.groovy 文件的 call 中,由 Jenkinsfile 调用,但一次构建只能执行一条 Declarative pipeline { ... }。例如:
// vars/standardPipeline.groovy
def call(Map config) {
pipeline {
agent { label 'linux-test' }
stages {
stage('Verify') {
steps {
checkout scm
verifyProject(config.kind)
}
}
}
}
}// 业务 Jenkinsfile;这里不能再定义第二条 pipeline { ... }
@Library('platform-ci') _
standardPipeline(kind: 'go')整条流水线模板适合多个项目确实遵守同一阶段和质量门禁时使用;项目有显著差异时,复用较小的 vars/ 步骤更容易看清 Jenkinsfile 的执行路径。无论哪种方式,都不要把用户提供的任意字符串直接拼成 shell 命令。避免把全局变量命名为 sh、echo、timeout 等内置步骤名,也不要把一个 vars/ 文件做成巨大的通用工具箱;小而明确的接口更容易升级和定位错误。共享库定义 Declarative Pipeline、最佳实践
共享库的 src/ 是加载到 Pipeline 的 Groovy 类路径,不等于在 Agent 上安装了第三方工具。避免用 @Grab 在 Controller 中临时下载大量 Java 依赖;需要复杂构建、扫描或网络处理时,把版本固定的可执行工具装到 Agent 镜像,由 sh/bat 调用。第三方库使用说明
9. 安全边界与版本治理#
共享库不是权限系统。vars/ 步骤能调用的 sh、凭据、Agent 和外部服务,仍取决于运行它的 Job 所拥有的权限。审查共享库时重点检查:是否执行了未受控输入、是否访问 Jenkins 内部 API、是否在日志中泄漏秘密、是否把不可信 PR 代码与发布凭据放到同一个执行环境。
| 场景 | 应采取的控制 |
|---|---|
| Folder 级库 | 始终在 Groovy sandbox 中运行;库仓库写权限仍需受控,因为改动会影响该 Folder 下的 Job |
| 全局 Trusted Library | 能访问 Jenkins 内部 API,库仓库写权限近似管理权限;只由可信维护者修改,代码审查后再晋级 |
动态 library 指定 Git URL |
允许的仓库、版本和检索凭据应由 Job 权限及代码审查约束;不要把它当成绕过固定库版本策略的普通发布入口 |
| 沙箱提示需要 Script Approval | 先确认为什么需要该方法;批准签名会影响其他脚本,不要为消除报错而批量批准或改成 Trusted |
共享库调用 withCredentials |
使用方 Job 必须有权读取该凭据;缩小绑定范围,不把秘密存成 vars/ 字段、静态资源或 Groovy 字符串日志 |
全局库的 Git 检索凭据 ID 与业务部署凭据 ID 是两回事;前者只用于拉库,后者应在具体发布步骤短时绑定。Trusted Library 也不宜直接调用 Jenkins.getInstance() 等内部 API;需要稳定、受控的 Jenkins 扩展能力时,应考虑实现专门插件。使用固定 commit SHA 可以重现库版本;如果改用可移动分支或标签,同一 Jenkinsfile 在不同时刻可能加载不同代码。记录每次升级的旧 SHA、新 SHA、消费 Job 和回退方式。共享库信任模型、脚本审批
10. 测试库变更:纯逻辑、步骤契约、真实 Job#
共享库代码和业务项目一样需要测试;只用 Jenkinsfile Linter 检查语法,不能证明运行时的库检索、沙箱、CPS、Agent 工具或凭据可用。
- 纯逻辑测试:对
BuildPlan.commandFor的go、node和未知类型写断言,保证没有把任意用户输入当成命令。 - Pipeline 步骤测试:用 Jenkins Pipeline Unit 等框架模拟
sh、timeout、echo,检查verifyProject('go')调用了预期命令并在未知类型失败;模拟器不能替代真实 Jenkins 的 CPS/沙箱验证。 - 测试 Folder 集成验证:把候选 commit SHA 注册为测试库版本,运行真实 Go/Node Job,检查库检索凭据、checkout、Agent 工具、报告与失败路径;必要时重启测试 Controller 验证暂停/恢复。
- 生产晋级:记录测试结果、候选 SHA 和上一 SHA,修改生产 Folder 的 Default version,先观察少量消费 Job,再扩展;异常时恢复上一 SHA 并重跑。
例如,BuildPlan 的纯逻辑测试可以放在共享库仓库的 test/com/example/ci/BuildPlanTest.groovy,由该仓库的 Groovy 测试构建执行:
package com.example.ci
class BuildPlanTest extends GroovyTestCase {
void testKnownKinds() {
assert BuildPlan.commandFor('go') == 'go test -race ./... && go vet ./...'
assert BuildPlan.commandFor('node') == 'npm ci && npm run lint && npm test'
}
void testUnknownKindFails() {
shouldFail(IllegalArgumentException) {
BuildPlan.commandFor('shell-from-user')
}
}
}此代码展示测试内容;仓库仍需配置适合其 Groovy/Jenkins 版本的测试依赖和构建命令。涉及 Pipeline 步骤时,参考 Jenkins Pipeline Unit 项目的共享库测试示例;上线前仍须使用目标 Jenkins 版本做集成验证。Pipeline 开发工具
11. 常见报错的定位顺序#
先从构建日志确认失败发生在哪一层:加载共享库 → 编译 Jenkinsfile/类 → 进入 Agent → 执行步骤。不要一看到 MissingMethodException 就把库改成 Trusted。
| 现象 | 优先检查 |
|---|---|
Could not find any definition of libraries ... |
@Library 中的名称是否与 Jenkins 注册名一致;Job 是否在配置该 Folder 下;是否关闭隐式加载但忘了显式引用 |
Git 检索失败或 Authentication failed |
库的仓库 URL、版本/ref、只读 credentialsId 的类型和可见范围;这与业务仓库的 checkout scm 凭据是两套配置 |
unable to resolve class com.example... |
src/ 路径与 package 是否一致;是否在编译前用 @Library 加载;运行时 library 加载后不能静态 import |
No such DSL method 'verifyProject' |
是否存在 vars/verifyProject.groovy 且大小写一致;库是否成功加载;是否把 call 写成了其他方法名 |
| Declarative 报方法调用不允许 | ciLog.info(...)、类方法等对象调用是否放进 script { ... };单个 call 全局步骤可直接放 steps |
Scripts not permitted to use ... |
沙箱拒绝了具体方法;检查代码能否改用受支持的 Pipeline 步骤,再由管理员评估是否审批签名 |
NotSerializableException |
看异常中的对象类型和最近的暂停步骤;将非序列化对象缩小到纯计算函数内,只让简单结果跨步骤保留 |
expected to call ... but wound up catching ... |
查 CPS mismatch:构造器、@NonCPS、闭包传给非 CPS 方法、GString 内闭包等 |
sh: command not found |
库已加载,问题在所选 Agent 的工具安装和 PATH;库检索凭据与此无关 |
| 改了共享库却仍执行旧逻辑 | 查看构建记录实际加载的库 SHA、Folder Default version、是否引用了可移动分支以及库缓存配置 |
排障时先复现最小调用,例如只加载库并调用一个 echo 步骤,再逐步加入 src/ 类、资源和业务命令。固定版本策略下,修复代码本身不会改变正在使用的 SHA,必须按第 5 节完成版本晋级。共享库代码运行在 Controller 的 Pipeline 引擎中,而 sh 命令运行在 Agent 上;区分两处日志和文件系统位置,能避免把类加载问题误判为 Agent 问题。