Skip to content

Commit

Permalink
Merge pull request #11 from jglick/return-value-JENKINS-26133
Browse files Browse the repository at this point in the history
[JENKINS-26133] Return exit status or stdout from sh/bat steps
  • Loading branch information
jglick committed Jul 13, 2016
2 parents 8c2c32b + 3887b86 commit 94ad105
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 10 deletions.
9 changes: 8 additions & 1 deletion pom.xml
Expand Up @@ -74,7 +74,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>durable-task</artifactId>
<version>1.11</version>
<version>1.12-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
Expand Down Expand Up @@ -104,6 +104,13 @@
<version>1.15</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>2.2</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
Expand Down
Expand Up @@ -25,14 +25,17 @@
package org.jenkinsci.plugins.workflow.steps.durable_task;

import com.google.inject.Inject;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.AbortException;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.TaskListener;
import hudson.util.FormValidation;
import hudson.util.LogTaskListener;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
Expand All @@ -50,6 +53,8 @@
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;

/**
* Runs an durable task on a slave, such as a shell script.
Expand All @@ -58,19 +63,70 @@ public abstract class DurableTaskStep extends AbstractStepImpl {

private static final Logger LOGGER = Logger.getLogger(DurableTaskStep.class.getName());

private boolean returnStdout;
private String encoding = DurableTaskStepDescriptor.defaultEncoding;
private boolean returnStatus;

protected abstract DurableTask task();

protected abstract static class DurableTaskStepDescriptor extends AbstractStepDescriptorImpl {
public boolean isReturnStdout() {
return returnStdout;
}

@DataBoundSetter public void setReturnStdout(boolean returnStdout) {
this.returnStdout = returnStdout;
}

public String getEncoding() {
return encoding;
}

@DataBoundSetter public void setEncoding(String encoding) {
this.encoding = encoding;
}

public boolean isReturnStatus() {
return returnStatus;
}

@DataBoundSetter public void setReturnStatus(boolean returnStatus) {
this.returnStatus = returnStatus;
}

public abstract static class DurableTaskStepDescriptor extends AbstractStepDescriptorImpl {

public static final String defaultEncoding = "UTF-8";

protected DurableTaskStepDescriptor() {
super(Execution.class);
}

public FormValidation doCheckEncoding(@QueryParameter boolean returnStdout, @QueryParameter String encoding) {
try {
Charset.forName(encoding);
} catch (Exception x) {
return FormValidation.error(x, "Unrecognized encoding");
}
if (!returnStdout && !encoding.equals(DurableTaskStepDescriptor.defaultEncoding)) {
return FormValidation.warning("encoding is ignored unless returnStdout is checked.");
}
return FormValidation.ok();
}

public FormValidation doCheckReturnStatus(@QueryParameter boolean returnStdout, @QueryParameter boolean returnStatus) {
if (returnStdout && returnStatus) {
return FormValidation.error("You may not select both returnStdout and returnStatus.");
}
return FormValidation.ok();
}

}

/**
* Represents one task that is believed to still be running.
*/
@Restricted(NoExternalUse.class)
@edu.umd.cs.findbugs.annotations.SuppressWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") // recurrencePeriod is set in onResume, not deserialization
@SuppressFBWarnings(value="SE_TRANSIENT_FIELD_NOT_RESTORED", justification="recurrencePeriod is set in onResume, not deserialization")
public static final class Execution extends AbstractStepExecutionImpl implements Runnable {

private static final long MIN_RECURRENCE_PERIOD = 250; // ¼s
Expand All @@ -86,10 +142,20 @@ public static final class Execution extends AbstractStepExecutionImpl implements
private Controller controller;
private String node;
private String remote;
private boolean returnStdout; // serialized default is false
private String encoding; // serialized default is irrelevant
private boolean returnStatus; // serialized default is false

@Override public boolean start() throws Exception {
returnStdout = step.returnStdout;
encoding = step.encoding;
returnStatus = step.returnStatus;
node = FilePathUtils.getNodeName(ws);
controller = step.task().launch(env, ws, launcher, listener);
DurableTask task = step.task();
if (returnStdout) {
task.captureOutput();
}
controller = task.launch(env, ws, launcher, listener);
this.remote = ws.getRemote();
setupTimer();
return false;
Expand Down Expand Up @@ -185,7 +251,7 @@ private void check() {
return; // slave not yet ready, wait for another day
}
// Do not allow this to take more than 10s for any given task:
final AtomicReference<Thread> t = new AtomicReference<Thread>(Thread.currentThread());
final AtomicReference<Thread> t = new AtomicReference<>(Thread.currentThread());
Timer.get().schedule(new Runnable() {
@Override public void run() {
Thread _t = t.get();
Expand All @@ -209,8 +275,8 @@ private void check() {
LOGGER.log(Level.FINE, "last-minute output in {0} on {1}", new Object[] {remote, node});
}
t.set(null); // do not interrupt cleanup
if (exitCode == 0) {
getContext().onSuccess(exitCode);
if (returnStatus || exitCode == 0) {
getContext().onSuccess(returnStatus ? exitCode : returnStdout ? new String(controller.getOutput(workspace, launcher), encoding) : null);
} else {
getContext().onFailure(new AbortException("script returned exit code " + exitCode));
}
Expand Down
@@ -0,0 +1,5 @@
<div>
Executes a Batch script. Multiple lines allowed.
When using the <code>returnStdout</code> flag, you probably wish to prefix this with <code>@</code>,
lest the command itself be included in the output.
</div>

This file was deleted.

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
The MIT License
Copyright 2016 CloudBees, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:st="jelly:stapler">
<st:include page="config-details" class="${descriptor.clazz}"/>
<f:advanced>
<f:entry field="returnStdout" title="${%Return standard output}">
<f:checkbox/>
</f:entry>
<f:entry field="encoding" title="${%Encoding of standard output}">
<f:textbox default="${descriptor.defaultEncoding}"/>
</f:entry>
<f:entry field="returnStatus" title="${%Return exit status}">
<f:checkbox/>
</f:entry>
</f:advanced>
</j:jelly>
@@ -0,0 +1,3 @@
<div>
Encoding of standard output, if it is being captured.
</div>
@@ -0,0 +1,5 @@
<div>
Normally, a script which exits with a nonzero status code will cause the step to fail with an exception.
If this option is checked, the return value of the step will instead be the status code.
You may then compare it to zero, for example.
</div>
@@ -0,0 +1,6 @@
<div>
If checked, standard output from the task is returned as the step value as a <code>String</code>,
rather than being printed to the build log.
(Standard error, if any, will still be printed to the log.)
You will often want to call <code>.trim()</code> on the result to strip off a trailing newline.
</div>
Expand Up @@ -18,6 +18,7 @@
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback;
import org.jenkinsci.plugins.workflow.steps.BodyInvoker;
import org.jenkinsci.plugins.workflow.steps.StepConfigTester;
import org.jenkinsci.plugins.workflow.support.visualization.table.FlowGraphTable;
import org.jenkinsci.plugins.workflow.support.visualization.table.FlowGraphTable.Row;
import org.junit.Assert;
Expand All @@ -26,6 +27,7 @@
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.DataBoundConstructor;
Expand Down Expand Up @@ -147,6 +149,44 @@ public DescriptorImpl() {
}
}

@Issue("JENKINS-26133")
@Test public void configRoundTrip() throws Exception {
ShellStep s = new ShellStep("echo hello");
s = new StepConfigTester(j).configRoundTrip(s);
assertEquals("echo hello", s.getScript());
assertFalse(s.isReturnStdout());
assertEquals(DurableTaskStep.DurableTaskStepDescriptor.defaultEncoding, s.getEncoding());
assertFalse(s.isReturnStatus());
s.setReturnStdout(true);
s.setEncoding("ISO-8859-1");
s = new StepConfigTester(j).configRoundTrip(s);
assertEquals("echo hello", s.getScript());
assertTrue(s.isReturnStdout());
assertEquals("ISO-8859-1", s.getEncoding());
assertFalse(s.isReturnStatus());
s.setReturnStdout(false);
s.setEncoding(DurableTaskStep.DurableTaskStepDescriptor.defaultEncoding);
s.setReturnStatus(true);
s = new StepConfigTester(j).configRoundTrip(s);
assertEquals("echo hello", s.getScript());
assertFalse(s.isReturnStdout());
assertEquals(DurableTaskStep.DurableTaskStepDescriptor.defaultEncoding, s.getEncoding());
assertTrue(s.isReturnStatus());
}

@Issue("JENKINS-26133")
@Test public void returnStdout() throws Exception {
WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition("def msg; node {msg = sh(script: 'echo hello world | tr [a-z] [A-Z]', returnStdout: true).trim()}; echo \"it said ${msg}\""));
j.assertLogContains("it said HELLO WORLD", j.assertBuildStatusSuccess(p.scheduleBuild2(0)));
}

@Issue("JENKINS-26133")
@Test public void returnStatus() throws Exception {
WorkflowJob p = j.jenkins.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition("node {echo \"truth is ${sh script: 'true', returnStatus: true} but falsity is ${sh script: 'false', returnStatus: true}\"}"));
j.assertLogContains("truth is 0 but falsity is 1", j.assertBuildStatusSuccess(p.scheduleBuild2(0)));
}

/**
* Asserts that the predicate remains true up to the given timeout.
Expand Down

0 comments on commit 94ad105

Please sign in to comment.