Jenkins 从 pipeline A 调用 pipeline B 时为何无法克隆共享库?
结论
日志表明,Windows 拒绝了 Jenkins 启动 git.exe 的请求,这次 Git 命令还没有开始执行。仅凭这段日志,无法认定共享库仓库损坏,也无法确定问题来自路径过长、GitLab 凭据失效或插件缺陷。
针对“手动构建成功,上游触发失败”的情况,应先对比两种触发方式下的构建参数、进程环境和实际执行位置。重点检查 pipeline A 传给 pipeline B 的大字符串参数,以及这些参数是否覆盖了 PATH 等环境变量。
环境变量异常或过长可以作为排查方向,但还需要验证,不能仅凭 CreateProcess error=87 就认定根因。
日志中的“仓库损坏”为什么不能直接采信
最底层的异常是:
Cannot run program "git.exe" ...
CreateProcess error=87, The parameter is incorrect
Windows 错误码 87 表示参数不正确。这里的“参数”指进程创建调用涉及的参数,可能包括启动环境,不一定是 rev-parse 后面的 Git 命令行参数。
调用链中的 hasGitRepo() 尝试执行以下命令,检查已有仓库:
git.exe rev-parse --resolve-git-dir D:\h\workspace\RP@libs\<hash>\.git
由于进程未能启动,仓库检查也就无法完成。上层随后输出:
ERROR: Workspace has a .git repository, but it appears to be corrupt.
这条提示不能证明 .git 已经损坏。Git 仓库本身出错时,通常还会有 Git 输出的诊断信息,例如 fatal: ...。当前应先解决进程启动失败的问题。
前面的 ls-remote 已成功解析出 dev 分支,说明那一次远程访问和认证成功了。不过,远程引用解析与本地共享库检出可能使用不同的启动上下文,后续进程的环境未必与前一次相同。
为什么手动构建正常,上游触发却失败
如果“手动正常”是指登录服务器后,在终端执行 Git 命令正常,这只能证明当前登录用户的环境可用。Jenkins 服务可能使用另一个账户,其 PATH、用户级 Git 配置、目录权限和临时目录都可能不同。
如果是在 Jenkins 页面点击“立即构建”正常,通过 pipeline A 触发同一个 B 却失败,应重点检查两种触发方式的差异:
- A 是否向 B 传递了手动构建时未使用的参数,或者传入了不同的值。
- 参数是否被导出为环境变量,其中是否包含很大的 JSON、文件内容或日志。
- 参数名是否与
PATH、TEMP、TMP等变量冲突。 - 插件是否根据上游构建原因注入环境变量或改变执行身份。
- A 仍在运行时,是否有脚本清理、移动或修改 B 使用的共享库检出目录。
标准的 build 步骤会调度另一个 Jenkins 构建,不会自动将 A 的整个环境复制给 B,也不会因触发者不同而自动改用触发者的 Windows 登录账户。具体情况要看实际传参和 Jenkins 配置。
1. 先用最小调用验证参数差异
在测试任务中,将 A 调用 B 的代码简化为:
build job: 'RP',
wait: true,
propagate: true
将 RP 替换为 B 的实际任务路径。
这样会去掉调用处显式传递的参数,但 B 自身的参数默认值仍然存在,环境注入插件也仍可能影响构建。
如果最小调用能够成功,再逐个恢复参数,直到找到导致失败的参数。重点检查:
- 很长的字符串参数。
- 批量转发 A 的参数或环境变量的代码。
- 与系统环境变量重名的参数。
- 包含异常控制字符的外部输入。
如果确认是某个大参数触发了问题,应把大内容保存为构建产物或其他可读取的文件,只向 B 传递构建号、文件位置等短标识。仅缩短工作区路径不足以处理这类问题。
2. 对比成功与失败构建的环境
通过 @Library 或隐式加载方式引入的共享库,会在 Pipeline 脚本编译阶段加载。B 的第一个 stage 可能还没运行就已经失败,因此在阶段中添加 bat 'set',往往无法查看故障发生前的环境。
管理员可以在 Jenkins 的 Script Console 中读取两次构建的基础环境。使用以下脚本前,先替换任务路径和构建号。脚本只输出变量名、长度及是否变化,不输出变量值。
import jenkins.model.Jenkins
import hudson.model.TaskListener
def job = Jenkins.get().getItemByFullName('RP')
assert job != null : 'Job not found'
// 替换为一次手动成功构建和一次上游触发失败构建的编号
def buildNumbers = [manual: 101, upstream: 102]
def snapshots = [:]
buildNumbers.each { label, number ->
def run = job.getBuildByNumber(number)
assert run != null : "Build not found: ${number}"
snapshots[label] = run.getEnvironment(TaskListener.NULL)
}
def names = new TreeSet<String>()
snapshots.values().each { names.addAll(it.keySet()) }
def nul = String.valueOf((char) 0)
names.each { name ->
def manual = snapshots.manual[name]
def upstream = snapshots.upstream[name]
def hasNul = [manual, upstream].any {
it != null && it.contains(nul)
}
if (manual != upstream || hasNul) {
def manualLength = manual == null ? 'absent' : manual.length()
def upstreamLength = upstream == null ? 'absent' : upstream.length()
println(
"${name}: manualLength=${manualLength}, " +
"upstreamLength=${upstreamLength}, " +
"changed=${manual != upstream}, containsNul=${hasNul}"
)
}
}
构建号、构建 URL 等变量不同是正常的。需要关注的是失败构建中突然变长的变量、被覆盖的系统变量,以及与 A 传参对应的变化。即使长度相同,内容也可能不同,脚本会用 changed 标记这种情况。
这里读取的是 Jenkins 为构建组装的基础环境,不等于失败的 Git 子进程最终收到的完整环境。节点配置、Git 工具配置和插件还可能继续修改环境。如果基础环境没有异常,再检查这些来源。
环境变量可能包含凭据,不要将完整环境直接输出到公开日志。也不能仅凭环境总长度超过某个数字就下结论,Windows API、Unicode 环境块和具体启动链路的限制需要分别判断。
3. 确认共享库实际在哪台机器上加载
堆栈显示,故障发生在共享库加载阶段。使用 @Library 时,不能假定共享库一定在 Jenkinsfile 中 agent 指定的节点上加载。
需要从日志和配置中确认:
D:\h\workspace\RP@libs\...位于控制器还是某个代理节点。- 在该位置运行 Jenkins Java 进程的 Windows 账户。
- 该进程使用的 Java 路径和版本。
- 该进程能否找到预期的
git.exe。 - 服务账户能否访问工作目录和临时目录。
在普通构建代理上执行 Git 成功,未必能证明共享库加载时使用的环境也正常。
修改系统环境变量或服务账户配置后,需要重启实际负责执行的 Jenkins 服务或代理进程,让新进程重新读取环境。
4. 修正 Git 工具配置,再检查本地检出目录
日志中还有以下配置提示:
Selected Git installation does not exist. Using Default
The recommended git tool is: NONE
检查 Jenkins 的 Git 工具配置、任务引用的工具名称,以及执行节点上的工具路径。可以为对应的 Windows 节点配置实际存在的 git.exe 绝对路径,减少通过 PATH 查找时产生的差异。不过,前面已经成功启动过 Git,因此这条提示本身还不足以解释错误 87。
接着,在同一台机器上,使用相同的服务账户环境检查日志中的目录。例如:
$repo = 'D:\h\workspace\RP@libs\4566a0d73a56c847d87178169741ab8291c61f3aad83825fd5ff8f285cbda243'
$git = 'C:\Program Files\Git\cmd\git.exe'
Test-Path -LiteralPath $repo
Test-Path -LiteralPath (Join-Path $repo '.git')
& $git -C $repo rev-parse --resolve-git-dir (Join-Path $repo '.git')
$git 必须替换为该机器上实际使用的路径。如果 Git 在这里报告了仓库本身的错误,再处理仓库内容。
如果怀疑上一次检出留下了残缺目录,可以先确认没有构建正在使用它,再将对应的哈希目录改名保留,让 Jenkins 重新检出。不要直接清空整个 Jenkins 主目录或所有共享库目录。
重新检出后,如果仍然出现相同的 CreateProcess error=87,应继续追查启动环境,反复删除缓存无法解决这类启动问题。
5. 最小调用仍失败时,收集完整版本与复现条件
问题中给出的 Jenkins 和 Git plugin 版本,还不足以确定完整的启动链路。需要补充:
- 实际运行 Jenkins 的 JDK 厂商和完整版本。
git-client插件版本。pipeline-groovy-lib插件版本。- 如果涉及代理节点,代理侧的 Java 和相关运行信息。
堆栈中的 CliGitAPIImpl 属于 git-client 插件,所以只知道 Git plugin 的版本还不够。“最新版本”也不能替代具体版本号。
如果在参数最少、环境已核对、Git 路径明确的情况下仍能复现,再按实际版本组合检查已知问题,并在测试环境验证兼容的更新。提交缺陷报告时,应附上 A 的最小调用代码、B 的共享库配置、完整底层异常,以及两种触发方式的差异。
长路径设置为什么没有解决问题
core.longpaths=true 影响 Git 对长路径的处理。这次调用连 Git 进程都未能启动,该设置无法直接解决所有 Windows 进程创建错误。
日志中的工作目录和命令行都较短,缩短目录后问题也依然存在,因此目前没有足够证据支持长路径是根因。接下来应先去掉 A 的显式传参,对比成功与失败构建的环境,并确认共享库加载时使用的机器、账户和工具路径。判断根因和修复方案,要看相应调整能否稳定改变复现结果。
备注:内容仅供参考。
