2

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.

  • The best practices document suggests that creating a step for every little thing might not be the best idea (might also be solved by putting more stuff in batch files, but my builds aren't that big yet)
  • I want to execute some commands in a Visual Studio command prompt, which is achieved by first calling a .bat file that sets the environment, and then doing any necessary commands.

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.

Kees-Jan
  • 518
  • 4
  • 16

2 Answers2

1

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.

Catalin
  • 366
  • 2
  • 8
  • Thanks for your feedback. Your solution is much more elegant than mine, so I'll use it to improve my code. It does nothing, though, to fix my problem. I've added a "minimal failing example" as well as a "curiously succeeding example" to my question to further narrow down the problem I'm having. – Kees-Jan Jan 14 '21 at 10:09
1

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.

  • The language spec claims that Groovy will typically cast between GString and String (as is shown in the ScriptConsole example. Pipeline seems to do so only for non-variadic functions. I'm beginning to consider that omission a bug. So for now, I guess your advice to cast explicitly is the best we can do. Even though I don't very much like the way it clutters the code. https://docs.groovy-lang.org/latest/html/documentation/#_gstrings – Kees-Jan Jan 14 '21 at 13:31
  • Indeed, this seems to be a bug in the Jenkins pipeline interpreter. I have added a link to the bug report in my answer. It also contains another work-around using Collections instead of varargs. – Nick Gubbels Jan 14 '21 at 15:01
  • Thanks for digging up that issue report. I've been meaning to do that myself – Kees-Jan Jan 15 '21 at 07:41