Jenkins声明式流水线调用slackSend出现CPS不匹配错误求助
我在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

