You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

GitLab API获取项目URL脚本在Jenkins控制台可用但流水线报错

解决Jenkins脚本化流水线中GitLab4J递归获取项目的Stream flatMap错误

问题背景

我是Jenkins新手,尝试编写脚本化流水线,通过GitLab API动态获取指定GitLab组及其子组下仓库的Git URL。使用gitlab4j-api工具,由于该工具无法直接获取组及子组的所有项目,参考文件目录递归遍历思路编写了递归flatMap子组的函数。脚本在Jenkins控制台运行正常,但嵌入流水线时出现错误。

原代码

import java.util.stream.Stream
import org.gitlab4j.api.GitLabApi
import org.gitlab4j.api.models.Group
import org.gitlab4j.api.models.Project

url = "https://gitlab.domain.com"
token = "xxxxxxx"
groupPath = 'MY/GROUP'

gitLabApi = new GitLabApi(url,token)
println("Connected to Gitlab, version: "+ gitLabApi.getApiVersion())

getProjectsRecursive(groupPath).each { project -> 
  println project.getSshUrlToRepo()
}

def getProjectsRecursive(path) {
  //println 'getProjectsRecursive : ' + path
  return getDirectProjectsAndSubGroups(path).flatMap { projectOrSubgroup ->      
          projectOrSubgroup instanceof Group ? 
          getProjectsRecursive(((Group)projectOrSubgroup).getFullPath()) : 
          Stream.of((Project) projectOrSubgroup)
  }
}

def getDirectProjectsAndSubGroups(path2) {
    //println 'getDirectProjectsAndSubGroups : ' + path2
    def projectsAndGroups = []
    projectsAndGroups.addAll(gitLabApi.getGroupApi().getProjectsStream(path2).toList())
    projectsAndGroups.addAll(gitLabApi.getGroupApi().getSubGroupsStream(path2).toList())
    return projectsAndGroups.stream()
}

初始报错信息

[Pipeline] Start of Pipeline
[Pipeline] echo
Connected to Gitlab, version: V4
[Pipeline] End of Pipeline
hudson.remoting.ProxyException: groovy.lang.MissingMethodException: No signature of method: java.util.stream.ReferencePipeline$Head.flatMap() is applicable for argument types: (org.jenkinsci.plugins.workflow.cps.CpsClosure2) values: [org.jenkinsci.plugins.workflow.cps.CpsClosure2@2fd9965c]
    at org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SandboxInterceptor.onMethodCall(SandboxInterceptor.java:158)
    at org.kohsuke.groovy.sandbox.impl.Checker$1.call(Checker.java:178)
    at org.kohsuke.groovy.sandbox.impl.Checker.checkedCall(Checker.java:182)
    at com.cloudbees.groovy.cps.sandbox.SandboxInvoker.methodCall(SandboxInvoker.java:17)
    at WorkflowScript.getProjectsRecursive(WorkflowScript:19)
    at WorkflowScript.run(WorkflowScript:13)
    at ___cps.transform___(Native Method)
    at com.cloudbees.groovy.cps.impl.ContinuationGroup.methodCall(ContinuationGroup.java:90)

添加@NonCPS注解后的报错

[Pipeline] Start of Pipeline
[Pipeline] echo
Connected to Gitlab, version: V4
[Pipeline] End of Pipeline
hudson.remoting.ProxyException: groovy.lang.MissingMethodException: No signature of method: java.util.stream.ReferencePipeline$Head.flatMap() is applicable for argument types: (WorkflowScript$_getProjectsRecursive_closure1) values: [WorkflowScript$_getProjectsRecursive_closure1@5795b4cf]

解决方案

问题核心是Jenkins流水线的CPS转换机制会修改Groovy闭包类型,导致Java Stream无法识别其为合法的Function参数。以下两种方案可解决:

方案1:改用Groovy集合递归处理

放弃Java Stream,使用Groovy原生List操作,彻底避免CPS转换冲突:

import org.gitlab4j.api.GitLabApi
import org.gitlab4j.api.models.Group
import org.gitlab4j.api.models.Project

@NonCPS
def getProjectsRecursive(path) {
    def result = []
    // 获取当前组直接项目
    result.addAll(gitLabApi.getGroupApi().getProjects(path))
    // 递归遍历子组并收集项目
    gitLabApi.getGroupApi().getSubGroups(path).each { subGroup ->
        result.addAll(getProjectsRecursive(subGroup.getFullPath()))
    }
    return result
}

url = "https://gitlab.domain.com"
token = "xxxxxxx"
groupPath = 'MY/GROUP'

gitLabApi = new GitLabApi(url, token)
println("Connected to Gitlab, version: " + gitLabApi.getApiVersion())

getProjectsRecursive(groupPath).each { project ->
    println project.getSshUrlToRepo()
}

方案2:使用Java匿名类替代Groovy闭包

若坚持使用Stream,需将Groovy闭包替换为Java的Function匿名类,绕开CPS转换干扰:

import java.util.stream.Stream
import java.util.function.Function
import org.gitlab4j.api.GitLabApi
import org.gitlab4j.api.models.Group
import org.gitlab4j.api.models.Project

@NonCPS
def getProjectsRecursive(path) {
    return getDirectProjectsAndSubGroups(path).flatMap(new Function<Object, Stream<?>>() {
        @Override
        Stream<?> apply(Object projectOrSubgroup) {
            if (projectOrSubgroup instanceof Group) {
                return getProjectsRecursive(((Group) projectOrSubgroup).getFullPath())
            } else {
                return Stream.of((Project) projectOrSubgroup)
            }
        }
    })
}

@NonCPS
def getDirectProjectsAndSubGroups(path2) {
    def projectsAndGroups = []
    projectsAndGroups.addAll(gitLabApi.getGroupApi().getProjectsStream(path2).toList())
    projectsAndGroups.addAll(gitLabApi.getGroupApi().getSubGroupsStream(path2).toList())
    return projectsAndGroups.stream()
}

url = "https://gitlab.domain.com"
token = "xxxxxxx"
groupPath = 'MY/GROUP'

gitLabApi = new GitLabApi(url, token)
println("Connected to Gitlab, version: " + gitLabApi.getApiVersion())

getProjectsRecursive(groupPath).each { project ->
    println project.getSshUrlToRepo()
}

关键注意事项

  • @NonCPS注解必须添加到所有涉及Java API操作的方法上,禁止CPS转换修改这些方法内的代码逻辑;但@NonCPS方法内不能调用任何Jenkins流水线步骤(如echo、sh等)。

内容的提问来源于stack exchange,提问作者Essadiq BAICH

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 09:50:37