Kees-Jan
Kees-Jan

Reputation: 518

Calling a variadic function in a Jenkinsfile fails unpredictably

Context

I'm running Jenkins on Windows, writing declarative pipelines. I'm trying to put multiple commands in a single bat step, while still making the step fail if any of the included commands fail.

Purpose of this is twofold.

Code

I wrote the following Groovy code in my Jenkinsfile:

def ExecuteMultipleCmdSteps(String... steps)
{
    bat ConcatenateMultipleCmdSteps(steps)
}

String ConcatenateMultipleCmdSteps(String... steps)
{
    String[] commands = []
    steps.each { commands +="echo ^> Now starting: ${it}"; commands += it; }
    return commands.join(" && ")
}

The problem/question

I can't get this to work reliably. That is, in a single Jenkinsfile, I can have multiple calls to ExecuteMultipleCmdSteps(), and some will work as intended, while others will fail with java.lang.NoSuchMethodError: No such DSL method 'ExecuteMultipleCmdSteps' found among steps [addBadge, ...

I have not yet found any pattern in the failures. I thought it only failed when executing from within a warnError block, but now I also have a problem from within a dir() block, while in a different Jenkinsfile, that works fine.

This problem seems to be related to ExecuteMultipleCmdSteps() being a variadic function. If I provide an overload with the correct number of arguments, then that overload is used without problem.

I'm at a loss here. Your input would be most welcome.

Failed solution

At some point I thought it might be a scoping/importing thing, so I enclosed ExecuteMultipleCmdSteps() in a class (code below) as suggested by this answer. Now, the method is called as Helpers.ExecuteMultipleCmdSteps(), and that results in a org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: No such static method found: staticMethod Helpers ExecuteMultipleCmdSteps org.codehaus.groovy.runtime.GStringImpl org.codehaus.groovy.runtime.GStringImpl

public class Helpers {
    public static environment

    public static void ExecuteMultipleCmdSteps(String... steps)
    {
        environment.bat ConcatenateMultipleCmdSteps(steps)
    }

    public static String ConcatenateMultipleCmdSteps(String... steps)
    {
        String[] commands = []
        steps.each { commands +="echo ^> Now starting: ${it}"; commands += it; }
        return commands.join(" && ")
    }

Minimal failing example

Consider the following:

hello = "Hello"

pipeline {
    agent any
    stages {
        stage("Stage") {
            steps {
                SillyEcho("Hello")
                SillyEcho("${hello}" as String)
                SillyEcho("${hello}")
            }
        }
    }
}

def SillyEcho(String... m)
{
    echo m.join(" ")
}

I'd expect all calls to SillyEcho() to result in Hello being echoed. In reality, the first two succeed, and the last one results in java.lang.NoSuchMethodError: No such DSL method 'SillyEcho' found among steps [addBadge, addErrorBadge,...

Curiously succeeding example

Consider the following groovy script, pretty much equivalent to the failing example above:

hello = "Hello"

SillyEcho("Hello")
SillyEcho("${hello}" as String)
SillyEcho("${hello}")

def SillyEcho(String... m)
{
    println m.join(" ")
}

When pasted into a Groovy Script console (for example the one provided by Jenkins), this succeeds (Hello is printed three times).

Even though I'd expect this example to succeed, I'd also expect it to behave consistently with the failing example, above, so I'm a bit torn on this one.

Upvotes: 2

Views: 507

Answers (2)

Nick Gubbels
Nick Gubbels

Reputation: 26

Thank you for adding the failing and succeeding examples. I expect your issues are due to the incompatibility of String and GString.

With respect to the differences between running it as a pipeline job and running the script in the Jenkins Script Console, I assume based on this that the Jenkins Script Console is not as strict with type references or tries to cast parameters based upon the function signature. I base this assumption on this script, based upon your script:

hello = "Hello"
hello2 = "${hello}" as String
hello3 = "${hello}"

println hello.getClass()
println hello2.getClass()
println hello3.getClass()

SillyEcho(hello)
SillyEcho(hello2)
SillyEcho(hello3)

def SillyEcho(String... m)
{
    println m.getClass()
}

This is the output I got in the Jenkins Script Console:

class java.lang.String
class java.lang.String
class org.codehaus.groovy.runtime.GStringImpl
class [Ljava.lang.String;
class [Ljava.lang.String;
class [Ljava.lang.String;

I expect the pipeline doesn't cast the GString to String but just fails as there is no function with the Gstring as parameter.

For debugging you could try to invoke .toString() an all elements you pass on to your function.

Update

This seems to be a known issue (or at least reported) with the pipeline interpreter: JENKINS-56758.

In the ticket an extra work-around has been described using collections instead of varargs. This would omit the need to type-cast everything.

Upvotes: 1

Catalin
Catalin

Reputation: 484

Not sure if this will answer your question, if not, consider it as a bigger comment. I like how you borrowed the 'variadic functions' from C++ :) However, in groovy there is much elegant way how to deal with this.

Try this:

def ExecuteMultipleCmdSteps(steps)
{
    sh steps
        .collect { "echo \\> Now starting: $it && $it" }
        .join(" && ")
}        

pipeline {
    agent any
    stages {
        stage ("test") {
            steps {
                ExecuteMultipleCmdSteps(["pwd", "pwd"])
            }
        }
    }
}

which works just fine for me:

[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /var/lib/jenkins/workspace/TestJob
[Pipeline] {
[Pipeline] stage
[Pipeline] { (test)
[Pipeline] sh
+ echo > Now starting: pwd
> Now starting: pwd
+ pwd
/var/lib/jenkins/workspace/TestJob
+ echo > Now starting: pwd
> Now starting: pwd
+ pwd
/var/lib/jenkins/workspace/TestJob
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

You may want to rewrite your function like this. The 2 errors you mention may have different causes.

The fist one, "No such DSL method ..." is indeed a scoping one, you found yourself the solution, but I do not understand why the overload works in the same scope.

The second error, may be solved with this answer. However, for me your code from the second approach works also just fine.

Upvotes: 1

Related Questions