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

Jenkins声明式流水线调用slackSend出现CPS不匹配错误求助

问题:Jenkins声明式流水线中@NonCPS函数调用slackSend报错

我在Jenkins声明式流水线的post always阶段调用了一个带@NonCPS注解的自定义函数,该函数负责读取本地JSON测试结果文件、统计测试情况,并调用slackSend向Slack频道推送消息。

自定义函数代码如下:

import groovy.json.JsonSlurper
import com.cloudbees.groovy.cps.NonCPS
@NonCPS
def call(){
    String path = "C:\\Users\\tests"
    String jsonFileToRead = new File(path).listFiles().findAll { it.name.endsWith(".json") }
        .sort { -it.lastModified() }?.head()
    JsonSlurper jsonSlurper = new JsonSlurper()
    Map data = jsonSlurper.parse(new File(jsonFileToRead))
    Map testResults = [:]
    int totalTests = data.results.size()
    int totalFailures = 0
    int totalSuccess = 0
    def results = []

    //Map test names and results from json file
    for (int i = 0; i < data.results.size(); i++) {
        testResults.put(i, [data['results'][i]['TestName'], data['results'][i]['result']])
    }

    //Iterate through the map and send to slack test name and results and then a summary of the total tests, failures and success
    for (test in testResults.values()){
        String testName =  test[0]
        String testResult = test[1]
        String msg = "Test: " + testName + " Result: " + testResult
        if(testResult == "fail")
        {
            totalFailures++ 
            println msg
            try {
                slackSend color : "danger", message: "${msg}", channel: '#somechannel'
            } catch (Throwable e) {
                error "Caught ${e.toString()}" 
            }   
        }
        else if(testResult == "pass")
        {
            totalSuccess++
            println msg
            try {
                slackSend color : "good", message: "${msg}", channel: '#somechannel'
            } catch (Throwable e) {
                error "Caught ${e.toString()}" 
            }            
        }
        else
        {
            println "Unknown test result: " + testResult
        }
    }
    def resultsSummary = "Total Tests: " + totalTests + " Total Failures: " + totalFailures + " Total Success: " + totalSuccess
    slackSend color : "good", message: "${resultsSummary}", channel: '#somechannel'
    println resultsSummary
}

只要函数中调用slackSend就会触发以下错误,注释掉所有slackSend调用后流水线可正常运行:

13:53:23  [Pipeline] echo
13:53:23  Test: triggers_attackMove Result: pass
13:53:23  [Pipeline] slackSend
13:53:23  Slack Send Pipeline step running, values are - baseUrl: <empty>, teamDomain: forgottenempires, channel: #test_harness_logs_phoenix, color: good, botUser: true, tokenCredentialId: Slack_bot_test, notifyCommitters: false, iconEmoji: :goose, username: Jenkins Wizard, timestamp: <empty>
13:53:23  [Pipeline] error
13:53:24  [Pipeline] }
13:53:24  [Pipeline] // script
13:53:24  Error when executing always post condition:
13:53:24  hudson.AbortException: Caught CpsCallableInvocation{methodName=slackSend, call=com.cloudbees.groovy.cps.impl.CpsFunction@149776fc, receiver=null, arguments=[org.jenkinsci.plugins.workflow.cps.DSL$ThreadTaskImpl@25def8]}
13:53:24    at org.jenkinsci.plugins.workflow.steps.ErrorStep$Execution.run(ErrorStep.java:64)
13:53:24    at org.jenkinsci.plugins.workflow.steps.ErrorStep$Execution.run(ErrorStep.java:51)
13:53:24    at org.jenkinsci.plugins.workflow.steps.SynchronousStepExecution.start(SynchronousStepExecution.java:37)
13:53:24    at org.jenkinsci.plugins.workflow.cps.DSL.invokeStep(DSL.java:322)
13:53:24    at org.jenkinsci.plugins.workflow.cps.DSL.invokeMethod(DSL.java:196)
....

尝试将slackSend移到另一个带@NonCPS注解的函数中调用,仍出现相同错误,求解决方案。


解决方案

核心原因

@NonCPS注解的函数运行在非CPS(Continuations Passing Style)上下文中,无法调用Jenkins流水线提供的CPS风格步骤(比如slackSend)。这些流水线步骤依赖CPS机制处理异步执行和状态持久化,在非CPS函数中调用会触发CpsCallableInvocation异常。

具体解决步骤

1. 拆分函数:IO逻辑与流水线步骤分离

把读取JSON、统计测试结果的纯Groovy逻辑留在@NonCPS函数中,返回可序列化的简单数据(测试结果列表、汇总统计值),然后在CPS上下文(流水线主逻辑或不带@NonCPS的代码块)中调用slackSend。

带@NonCPS的工具函数(仅处理IO和数据统计)

import groovy.json.JsonSlurper
import com.cloudbees.groovy.cps.NonCPS

@NonCPS
def getTestResults() {
    String path = "C:\\Users\\tests"
    File jsonFileToRead = new File(path).listFiles().findAll { it.name.endsWith(".json") }
        .sort { -it.lastModified() }?.head()
    
    if (!jsonFileToRead) {
        return [testList: [], totalTests: 0, totalFailures: 0, totalSuccess: 0]
    }

    JsonSlurper jsonSlurper = new JsonSlurper()
    Map data = jsonSlurper.parse(jsonFileToRead)
    def testList = []
    int totalTests = data.results.size()
    int totalFailures = 0
    int totalSuccess = 0

    for (result in data.results) {
        String testName = result.TestName
        String testResult = result.result
        testList.add([name: testName, result: testResult])
        
        if (testResult == "fail") {
            totalFailures++
        } else if (testResult == "pass") {
            totalSuccess++
        }
    }

    return [
        testList: testList,
        totalTests: totalTests,
        totalFailures: totalFailures,
        totalSuccess: totalSuccess
    ]
}

流水线中的调用逻辑(CPS上下文处理slackSend)

pipeline {
    agent any
    stages {
        stage('Test') {
            steps {
                // 执行测试生成JSON结果文件
            }
        }
    }
    post {
        always {
            script {
                def testData = getTestResults()
                
                // 发送单个测试结果
                for (test in testData.testList) {
                    String msg = "Test: ${test.name} Result: ${test.result}"
                    println msg
                    try {
                        if (test.result == "fail") {
                            slackSend color: "danger", message: msg, channel: '#somechannel'
                        } else if (test.result == "pass") {
                            slackSend color: "good", message: msg, channel: '#somechannel'
                        } else {
                            println "Unknown test result: ${test.result}"
                        }
                    } catch (Throwable e) {
                        error "Caught ${e.toString()}"
                    }
                }
                
                // 发送测试汇总信息
                def resultsSummary = "Total Tests: ${testData.totalTests} | Total Failures: ${testData.totalFailures} | Total Success: ${testData.totalSuccess}"
                slackSend color: "good", message: resultsSummary, channel: '#somechannel'
                println resultsSummary
            }
        }
    }
}

2. 严格遵守@NonCPS函数的使用边界

@NonCPS函数仅用于纯Groovy逻辑(文件IO、数据计算、字符串处理等),绝对不能调用Jenkins流水线DSL方法(如slackSend、sh、bat、echo等),所有与Jenkins流水线交互的操作必须放在CPS上下文中执行。

3. 传递简单可序列化数据

非CPS函数返回的数据必须是可序列化的简单类型(字符串、数字、列表、普通Map),避免传递复杂对象(如File实例),否则可能在CPS上下文中引发序列化异常。

内容的提问来源于stack exchange,提问作者jesus fernandez

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 11:31:02