diff --git a/pom.xml b/pom.xml
index 6067afd0..9cd3aeab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -85,12 +85,17 @@
org.jenkins-ci.plugins.workflow
workflow-api
- 1.15
+ 2.1-SNAPSHOT
+
+
+ org.jenkins-ci.plugins
+ script-security
+ 1.18
org.jenkins-ci.plugins.workflow
workflow-cps
- 1.15
+ 2.2-SNAPSHOT
test
diff --git a/src/main/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStep.java b/src/main/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStep.java
new file mode 100644
index 00000000..e4f028a6
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStep.java
@@ -0,0 +1,194 @@
+/*
+ * 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.
+ */
+
+package org.jenkinsci.plugins.workflow.steps;
+
+import com.google.inject.Inject;
+import hudson.AbortException;
+import hudson.Extension;
+import hudson.Functions;
+import hudson.model.Result;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import jenkins.model.Jenkins;
+import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted;
+import org.jenkinsci.plugins.workflow.actions.ErrorAction;
+import org.jenkinsci.plugins.workflow.actions.LogAction;
+import org.jenkinsci.plugins.workflow.flow.FlowExecution;
+import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
+import org.jenkinsci.plugins.workflow.graph.BlockEndNode;
+import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker;
+import org.jenkinsci.plugins.workflow.graph.FlowNode;
+import org.jenkinsci.plugins.workflow.graph.FlowNodeSerialWalker;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+/**
+ * Step to supply contextual information about an error that has been caught.
+ */
+public class ErrorInfoStep extends AbstractStepImpl {
+
+ public final Throwable error;
+
+ @DataBoundConstructor public ErrorInfoStep(Throwable error) {
+ this.error = error;
+ }
+
+ public static class Execution extends AbstractSynchronousStepExecution {
+
+ private static final long serialVersionUID = 1;
+ @Inject private transient ErrorInfoStep step;
+ @StepContextParameter private transient FlowExecution execution;
+
+ @Override protected ErrorInfo run() throws Exception {
+ return new ErrorInfo(step.error, execution);
+ }
+
+ }
+
+ @Extension public static class DescriptorImpl extends AbstractStepDescriptorImpl {
+
+ public DescriptorImpl() {
+ super(Execution.class);
+ }
+
+ @Override public String getFunctionName() {
+ return "errorInfo";
+ }
+
+ @Override public String getDisplayName() {
+ return "Calculate information about an error";
+ }
+
+ // TODO blank config.jelly
+
+ }
+
+ public static class ErrorInfo implements Serializable {
+
+ private static final long serialVersionUID = 1;
+ private final Throwable error;
+ private transient FlowExecution execution;
+ private final FlowExecutionOwner executionOwner;
+
+ ErrorInfo(Throwable error, FlowExecution execution) {
+ this.error = error;
+ this.execution = execution;
+ executionOwner = execution.getOwner();
+ }
+
+ private FlowExecution getExecution() throws IOException {
+ if (execution == null) {
+ execution = executionOwner.get();
+ }
+ return execution;
+ }
+
+ /**
+ * Finds a node which threw this exception or one of its causes.
+ * Note that {@link Throwable#equals} is just pointer equality,
+ * which we cannot use since we may be loading deserialized exceptions,
+ * so we compare by stack trace instead.
+ */
+ private @CheckForNull FlowNode getNode() throws IOException {
+ Set stackTraces = new HashSet<>();
+ for (Throwable t = error; t != null; t = t.getCause()) {
+ stackTraces.add(Functions.printThrowable(t));
+ }
+ for (FlowNode n : new FlowGraphWalker(getExecution())) {
+ if (n instanceof BlockEndNode) {
+ continue; // look for the thing it is enclosing
+ }
+ ErrorAction a = n.getAction(ErrorAction.class);
+ if (a != null) {
+ if (stackTraces.contains(Functions.printThrowable(a.getError()))) {
+ return n;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Whitelisted
+ public @Nonnull Throwable getError() {
+ return error;
+ }
+
+ /**
+ * Gets the stack trace of the error, or just the message in the case of {@link AbortException}.
+ */
+ @Whitelisted
+ public @Nonnull String getStackTrace() {
+ if (error instanceof AbortException) {
+ return error.getMessage();
+ } else {
+ return Functions.printThrowable(error);
+ }
+ }
+
+ /**
+ * Gets the {@link Result} of the build if the error were uncaught.
+ * @return typically {@link Result#FAILURE} but {@link FlowInterruptedException} may override
+ */
+ @Whitelisted
+ public @Nonnull String getResult() {
+ Result r;
+ if (error instanceof FlowInterruptedException) {
+ r = ((FlowInterruptedException) error).getResult();
+ } else {
+ r = Result.FAILURE;
+ }
+ return r.toString();
+ }
+
+ /**
+ * Looks for the URL of the {@link LogAction} last printed before the node which broke.
+ */
+ @Whitelisted
+ public @CheckForNull String getLogURL() throws IOException {
+ FlowNode n = getNode();
+ if (n != null) {
+ for (FlowNode n2 : new FlowNodeSerialWalker(n)) {
+ LogAction a = n2.getAction(LogAction.class);
+ if (a != null) {
+ String u = Jenkins.getActiveInstance().getRootUrl();
+ if (u == null) {
+ u = "http://jenkins/"; // placeholder
+ }
+ return u + n2.getUrl() + a.getUrlName();
+ }
+ }
+ }
+ return null;
+ }
+
+ // TODO tail of log
+ // TODO label
+
+ }
+
+}
diff --git a/src/test/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStepTest.java b/src/test/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStepTest.java
new file mode 100644
index 00000000..79490f3b
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/workflow/steps/ErrorInfoStepTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+
+package org.jenkinsci.plugins.workflow.steps;
+
+import hudson.AbortException;
+import hudson.model.Result;
+import java.net.URL;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
+import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
+import static org.junit.Assert.*;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.runners.model.Statement;
+import org.jvnet.hudson.test.BuildWatcher;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.RestartableJenkinsRule;
+
+public class ErrorInfoStepTest {
+
+ @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher();
+ @Rule public RestartableJenkinsRule s = new RestartableJenkinsRule();
+
+ @Issue("JENKINS-28119")
+ @Test public void smokes() {
+ s.addStep(new Statement() {
+ @Override public void evaluate() throws Throwable {
+ WorkflowJob p = s.j.jenkins.createProject(WorkflowJob.class, "p");
+ p.setDefinition(new CpsFlowDefinition(
+ "try {\n" +
+ " parallel fine: {\n" +
+ " semaphore 'fine'\n" +
+ " }, broken: {\n" +
+ " echo 'erroneous step'\n" +
+ " semaphore 'breaking'\n" +
+ " }\n" +
+ "} catch (e) {\n" +
+ " def info = errorInfo(e)\n" +
+ " semaphore 'caught'\n" +
+ " currentBuild.result = info.result\n" +
+ " echo \"caught an instance of ${info.error.getClass()}\"\n" +
+ " echo info.stackTrace\n" +
+ " echo \"browse to: ${info.logURL}\"\n" +
+ "}", true));
+ WorkflowRun b = p.scheduleBuild2(0).waitForStart();
+ SemaphoreStep.waitForStart("fine/1", b);
+ SemaphoreStep.failure("breaking/1", new AbortException("oops"));
+ SemaphoreStep.success("fine/1", null);
+ SemaphoreStep.waitForStart("caught/1", null);
+ }
+ });
+ s.addStep(new Statement() {
+ @Override public void evaluate() throws Throwable {
+ SemaphoreStep.success("caught/1", null);
+ WorkflowJob p = s.j.jenkins.getItemByFullName("p", WorkflowJob.class);
+ WorkflowRun b = p.getBuildByNumber(1);
+ s.j.assertBuildStatus(Result.FAILURE, s.j.waitForCompletion(b));
+ s.j.waitForMessage("End of Pipeline", b); // TODO why does it sometimes cut off at "Resuming build"? probably because WorkflowRun.finish sets isBuilding() → false before flushing the log
+ s.j.assertLogContains("caught an instance of class hudson.AbortException", b);
+ s.j.assertLogContains("oops", b);
+ s.j.assertLogNotContains("\tat ", b);
+ String log = JenkinsRule.getLog(b);
+ Matcher matcher = Pattern.compile("^browse to: (http.+)$", Pattern.MULTILINE).matcher(log);
+ assertTrue(log, matcher.find());
+ String text = s.j.createWebClient().getPage(new URL(matcher.group(1))).getWebResponse().getContentAsString();
+ assertTrue(text, text.contains("erroneous step"));
+ }
+ });
+ }
+
+}
diff --git a/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepRunTest.java b/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepRunTest.java
index b4608f5d..95ae0f3e 100644
--- a/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepRunTest.java
+++ b/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepRunTest.java
@@ -18,8 +18,6 @@
import org.jvnet.hudson.test.RestartableJenkinsRule;
import java.util.List;
-import org.jenkinsci.plugins.workflow.steps.SleepStep;
-import org.jenkinsci.plugins.workflow.steps.TimeoutStepExecution;
/**
* @author Kohsuke Kawaguchi
@@ -64,7 +62,7 @@ public void evaluate() throws Throwable {
+ " }\n"
+ " echo 'NotHere'\n"
+ "}\n"));
- WorkflowRun b = story.j.assertBuildStatus(/* TODO JENKINS-25894 should really be ABORTED */Result.FAILURE, p.scheduleBuild2(0).get());
+ WorkflowRun b = story.j.assertBuildStatus(Result.ABORTED, p.scheduleBuild2(0).get());
// make sure things that are supposed to run do, and things that are NOT supposed to run do not.
story.j.assertLogNotContains("NotHere", b);