Why does Jenkins Pipeline script returnStatus value not match status examined in shell? This is my little newbie Groovy script, trying to cobble together a very beginner understanding of Jenkins Pipelines:
node {
 stage(&quot;hello&quot;) {
  def var = &quot;val&quot;
  echo &quot;${var}&quot;
  def stdout = sh( script: 'pwd', returnStdout: true ).trim()
  echo &quot;${stdout}&quot;
  def ret_status = sh( script: 'cd subdir', returnStatus: true )
  echo &quot;${ret_status}&quot;
 }
}

The output is:
Started by user unknown or anonymous
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on Jenkins in /home/user/workspace/newbie_dont_know_what_hes_doin
[Pipeline] {
[Pipeline] stage
[Pipeline] { (hello)
[Pipeline] echo
val
[Pipeline] sh
+ pwd
[Pipeline] echo
/home/user/workspace/newbie_dont_know_what_hes_doin
[Pipeline] sh
+ cd subdir
/home/user/workspace/newbie_dont_know_what_hes_doin@tmp/durable-2fa119e0/script.sh: 1: cd: can't cd to subdir
[Pipeline] echo
2
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

The failure to cd subdir is intentional -- to demonstrate the mechanics of capturing shell stdout and status.
Why does the failed cd subdir report a returnStatus of 2 when cding to nonexistent directories in bash returns status-code 1? The Groovy documentation I have as reference says sh runs a Bourne shell script.
$ echo $SHELL
/bin/bash
$ cd subdir
-bash: cd: subdir: No such file or directory
$ echo $?
1
$
There's a few things to note. First is that in the absense of a shebang being supplied, the actual interpreter that will be used is actually &quot;sh&quot;, not &quot;bash&quot;.
Taking a look at the Jenkins source for the BourneShellScript task, what happens is that Jenkins does not actually return the exit code from the subshell - it monitors a file called &quot;jenkins-result.txt for changes (using a FileMonitoringTask) and when that file has been updated, it merely assigns the content of it as the exit status.
From what I can see, when you call &quot;sh&quot; it does not just run your script code, but actually wraps it in a fairly substantial block of boilerplate code (refer line 272 of the above linked BourneShellScript). In effect, something closer to this:
sh -c ({ while [ -d '/home/user/workspace/newbie_dont_know_what_hes_doin@tmp/durable-2fa119e0' -a \\! -f '/home/user/workspace/newbie_dont_know_what_hes_doin@tmp/durable-2fa119e0/jenkins-result.txt' ]; do touch '/home/user/workspace/newbie_dont_know_what_hes_doin@tmp/durable-2fa119e0/jenkins-log.txt'; sleep 3; done } &amp; jsc=durable-2fa119e0; JENKINS_SERVER_COOKIE=$jsc sh -xe '/home/user/workspace/newbie_dont_know_what_hes_doin@tmp/durable-2fa119e0/script.sh' &gt; '/home/user/workspace/newbie_dont_know_what_hes_doin@tmp/durable-2fa119e0/jenkins-output.txt' 2&gt; 'jenkins-log.txt'; echo $? &gt; '/home/user/workspace/newbie_dont_know_what_hes_doin@tmp/durable-2fa119e0/jenkins-result.txt.tmp'; mv '/home/user/workspace/newbie_dont_know_what_hes_doin@tmp/durable-2fa119e0/jenkins-result.txt.tmp' '/home/user/workspace/newbie_dont_know_what_hes_doin\@tmp/durable-2fa119e0/jenkins-result.txt'; wait) &gt;&amp;- 2&gt;&amp;- &amp;

This is likely necessary to support scenarios where the node on which the script is executed is not the node on which the pipeline is executing, however it does leave a lot of room for error in returning the result, depending on the idiosyncracies of whatever interpreter &quot;sh&quot; is aliased to (not necessarily bash) and the OS on which it is running.