diff --git a/pom.xml b/pom.xml
index 38cc735a..134a7d48 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
org.jenkins-ci.plugins
plugin
- 4.6
+ 4.13
pipeline-input-step
@@ -39,16 +39,17 @@
2.13
-SNAPSHOT
- 2.176.4
+ 2.204.6
8
+ 1.2.0
true
io.jenkins.tools.bom
- bom-2.176.x
- 11
+ bom-2.204.x
+ 18
pom
import
@@ -75,6 +76,16 @@
org.jenkins-ci.plugins
credentials
+
+ io.jenkins.plugins
+ plugin-util-api
+ 1.6.0
+
+
+ io.jenkins.plugins
+ checks-api
+ ${checks-api.version}
+
org.jenkins-ci.plugins.workflow
workflow-cps
@@ -116,5 +127,12 @@
credentials-binding
test
+
+ io.jenkins.plugins
+ checks-api
+ ${checks-api.version}
+ tests
+ test
+
diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepExecution.java b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepExecution.java
index 173302bd..8fd55d0f 100644
--- a/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepExecution.java
+++ b/src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepExecution.java
@@ -4,6 +4,7 @@
import com.cloudbees.plugins.credentials.builds.CredentialsParameterBinder;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
+import edu.hm.hafner.util.VisibleForTesting;
import hudson.FilePath;
import hudson.Util;
import hudson.console.HyperlinkNote;
@@ -20,8 +21,14 @@
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.security.SecurityRealm;
-import hudson.security.Permission;
import hudson.util.HttpResponses;
+import io.jenkins.plugins.checks.api.ChecksConclusion;
+import io.jenkins.plugins.checks.api.ChecksDetails;
+import io.jenkins.plugins.checks.api.ChecksOutput;
+import io.jenkins.plugins.checks.api.ChecksPublisher;
+import io.jenkins.plugins.checks.api.ChecksPublisherFactory;
+import io.jenkins.plugins.checks.api.ChecksStatus;
+import io.jenkins.plugins.checks.steps.ChecksInfo;
import jenkins.model.IdStrategy;
import jenkins.model.Jenkins;
import net.sf.json.JSONArray;
@@ -29,6 +36,7 @@
import org.acegisecurity.Authentication;
import org.acegisecurity.GrantedAuthority;
import org.apache.commons.lang.StringUtils;
+import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.support.actions.PauseAction;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
@@ -45,10 +53,13 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
import jenkins.util.Timer;
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
@@ -74,6 +85,10 @@ public class InputStepExecution extends AbstractStepExecutionImpl implements Mod
@Override
public boolean start() throws Exception {
+ if (getChecksName().isPresent()) {
+ getPublisher().publish(extractChecksDetailsStart());
+ }
+
// record this input
getPauseAction().add(this);
@@ -186,6 +201,10 @@ public HttpResponse proceed(@CheckForNull Map params) {
}
node.addAction(new InputSubmittedAction(approverId, params));
+ if (getChecksName().isPresent()) {
+ getPublisher().publish(extractChecksDetailsProceed(user, params));
+ }
+
Object v;
if (params != null && params.size() == 1) {
v = params.values().iterator().next();
@@ -402,5 +421,62 @@ private Object convert(String name, ParameterValue v) throws IOException, Interr
}
}
+ Optional getChecksName() {
+ try {
+ return Optional.ofNullable(getContext().get(ChecksInfo.class))
+ .map(ChecksInfo::getName);
+ } catch (IOException | InterruptedException e) {
+ return Optional.empty();
+ }
+ }
+
+ private ChecksPublisher getPublisher() {
+ return ChecksPublisherFactory.fromRun(run, listener);
+ }
+
+ @VisibleForTesting
+ ChecksDetails extractChecksDetailsStart() {
+ assert getChecksName().isPresent();
+ ChecksOutput output = new ChecksOutput.ChecksOutputBuilder()
+ .withTitle("Input requested")
+ .withSummary(input.getMessage())
+ .build();
+ return new ChecksDetails.ChecksDetailsBuilder()
+ .withName(getChecksName().get())
+ .withStatus(ChecksStatus.COMPLETED)
+ .withConclusion(ChecksConclusion.ACTION_REQUIRED)
+ .withDetailsURL(DisplayURLProvider.get().getRoot() + run.getUrl() + getPauseAction().getUrlName() + "/")
+ .withOutput(output)
+ .build();
+ }
+
+ @VisibleForTesting
+ ChecksDetails extractChecksDetailsProceed(@CheckForNull User user, @CheckForNull Map parameters) {
+ assert getChecksName().isPresent();
+
+ ChecksOutput.ChecksOutputBuilder outputBuilder = new ChecksOutput.ChecksOutputBuilder()
+ .withTitle("Input provided");
+ if (user != null) {
+ outputBuilder.withSummary("Approved by " + user.getDisplayName());
+ }
+ if (parameters != null) {
+ outputBuilder.withText(extractChecksText(parameters));
+ }
+ return new ChecksDetails.ChecksDetailsBuilder()
+ .withName(getChecksName().get())
+ .withStatus(ChecksStatus.COMPLETED)
+ .withConclusion(ChecksConclusion.SUCCESS)
+ .withDetailsURL(DisplayURLProvider.get().getRunURL(run))
+ .withOutput(outputBuilder.build())
+ .build();
+ }
+
+ @VisibleForTesting
+ String extractChecksText(Map parameters) {
+ return parameters.entrySet().stream()
+ .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue()))
+ .collect(Collectors.joining("\n"));
+ }
+
private static final long serialVersionUID = 1L;
}
diff --git a/src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepWithChecksTest.java b/src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepWithChecksTest.java
new file mode 100644
index 00000000..0cd6c6df
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepWithChecksTest.java
@@ -0,0 +1,193 @@
+package org.jenkinsci.plugins.workflow.support.steps.input;
+
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import hudson.model.Job;
+import hudson.model.Result;
+import hudson.model.queue.QueueTaskFuture;
+import io.jenkins.plugins.checks.api.ChecksConclusion;
+import io.jenkins.plugins.checks.api.ChecksDetails;
+import io.jenkins.plugins.checks.api.ChecksOutput;
+import io.jenkins.plugins.checks.api.ChecksStatus;
+import io.jenkins.plugins.checks.util.CapturingChecksPublisher;
+import jenkins.model.Jenkins;
+import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
+import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.jenkinsci.plugins.workflow.job.WorkflowRun;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.MockAuthorizationStrategy;
+import org.jvnet.hudson.test.TestExtension;
+
+import java.net.URI;
+import java.util.List;
+
+public class InputStepWithChecksTest extends Assert {
+
+ private final String CHECKS_NAME = "Input Checks";
+ private final String INPUT_ID = "InputChecks";
+ private final String USER = "bob";
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @TestExtension
+ public static final CapturingChecksPublisher.Factory PUBLISHER_FACTORY = new CapturingChecksPublisher.Factory();
+
+ @Before
+ public void setUpUsers() {
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+ j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
+ .grant(Jenkins.READ, Job.READ, Job.BUILD).everywhere().to(USER));
+ }
+
+ @After
+ public void cleanupFactory() {
+ PUBLISHER_FACTORY.getPublishedChecks().clear();
+ }
+
+ private List runAndSubmit(String script) throws Exception {
+ return run(script, false);
+ }
+
+ private List runAndAbort(String script) throws Exception {
+ return run(script, true);
+ }
+
+ private List run(String script, boolean abort) throws Exception {
+
+ WorkflowJob job = j.createProject(WorkflowJob.class);
+ job.setDefinition(new CpsFlowDefinition(script, true));
+
+ QueueTaskFuture q = job.scheduleBuild2(0);
+ WorkflowRun run = q.getStartCondition().get();
+ CpsFlowExecution e = (CpsFlowExecution) run.getExecutionPromise().get();
+
+ while (run.getAction(InputAction.class) == null) {
+ e.waitForSuspension();
+ }
+
+ List checksDetails = PUBLISHER_FACTORY.getPublishedChecks();
+
+ assertEquals(2, PUBLISHER_FACTORY.getPublishedChecks().size());
+
+ ChecksDetails waiting = checksDetails.get(1);
+ assertTrue(waiting.getDetailsURL().isPresent());
+
+ String url = waiting.getDetailsURL().get();
+
+ assertTrue(url.contains(j.getURL().getHost()));
+
+ String inputUrl = j.getURL().toURI().relativize(new URI(url)).toString();
+
+ JenkinsRule.WebClient c = j.createWebClient();
+ c.login(USER);
+ HtmlPage p = c.goTo(inputUrl);
+ j.submit(p.getFormByName(INPUT_ID), abort ? "abort" : "proceed");
+
+ q.get();
+ j.assertBuildStatus(abort ? Result.ABORTED : Result.SUCCESS, j.waitForCompletion(run));
+
+ return PUBLISHER_FACTORY.getPublishedChecks();
+ }
+
+ @Test
+ public void publishChecksWithNoParameters() throws Exception {
+ String script = "" +
+ "withChecks('" + CHECKS_NAME + "') {\n" +
+ " input message: 'Can you hear me?', id: '" + INPUT_ID + "'\n" +
+ "}";
+
+ List checksDetails = runAndSubmit(script);
+
+ assertEquals(3, checksDetails.size());
+
+ ChecksDetails started = checksDetails.get(0);
+ assertTrue(started.getName().isPresent());
+ assertEquals(CHECKS_NAME, started.getName().get());
+
+ ChecksDetails waiting = checksDetails.get(1);
+ assertTrue(waiting.getName().isPresent());
+ assertEquals(CHECKS_NAME, waiting.getName().get());
+ assertEquals(ChecksStatus.COMPLETED, waiting.getStatus());
+ assertEquals(ChecksConclusion.ACTION_REQUIRED, waiting.getConclusion());
+ assertTrue(waiting.getDetailsURL().isPresent());
+ assertTrue(waiting.getOutput().isPresent());
+
+ ChecksOutput waitingOutput = waiting.getOutput().get();
+ assertTrue(waitingOutput.getTitle().isPresent());
+ assertEquals("Input requested", waitingOutput.getTitle().get());
+ assertTrue(waitingOutput.getSummary().isPresent());
+ assertEquals("Can you hear me?", waitingOutput.getSummary().get());
+ assertFalse(waitingOutput.getText().isPresent());
+
+ ChecksDetails complete = checksDetails.get(2);
+ assertTrue(complete.getName().isPresent());
+ assertEquals(CHECKS_NAME, complete.getName().get());
+ assertEquals(ChecksStatus.COMPLETED, complete.getStatus());
+ assertEquals(ChecksConclusion.SUCCESS, complete.getConclusion());
+ assertTrue(complete.getOutput().isPresent());
+
+ ChecksOutput completeOutput = complete.getOutput().get();
+ assertTrue(completeOutput.getTitle().isPresent());
+ assertEquals("Input provided", completeOutput.getTitle().get());
+ assertTrue(completeOutput.getSummary().isPresent());
+ assertEquals("Approved by bob", completeOutput.getSummary().get());
+ assertFalse(completeOutput.getText().isPresent());
+ }
+
+ @Test
+ public void publishCheckWithParameters() throws Exception {
+ String defaultValue = "A Sensible Default";
+ String paramName = "STRING_PARAM";
+ String script = "" +
+ "withChecks('" + CHECKS_NAME + "') {\n" +
+ " input message: 'Can you hear me?',\n" +
+ " id: '" + INPUT_ID + "',\n" +
+ " parameters: [string(defaultValue: '" + defaultValue + "', name: '" + paramName + "')]\n" +
+ "}";
+
+ List checksDetails = runAndSubmit(script);
+ assertEquals(3, checksDetails.size());
+
+ ChecksDetails waiting = checksDetails.get(1);
+ assertTrue(waiting.getOutput().isPresent());
+
+ ChecksOutput waitingOutput = waiting.getOutput().get();
+ assertFalse(waitingOutput.getText().isPresent());
+
+ ChecksDetails complete = checksDetails.get(2);
+ assertTrue(complete.getOutput().isPresent());
+
+ ChecksOutput completeOutput = complete.getOutput().get();
+ assertTrue(completeOutput.getText().isPresent());
+ assertEquals(String.format("%s: %s", paramName, defaultValue), completeOutput.getText().get());
+ }
+
+ @Test
+ public void publishCheckWithAbort() throws Exception {
+ String script = "" +
+ "withChecks('" + CHECKS_NAME + "') {\n" +
+ " input message: 'Can you hear me?', id: '" + INPUT_ID + "'\n" +
+ "}";
+
+ List checksDetails = runAndAbort(script);
+
+ assertEquals(3, checksDetails.size());
+
+ ChecksDetails complete = checksDetails.get(2);
+ assertEquals(ChecksStatus.COMPLETED, complete.getStatus());
+ assertEquals(ChecksConclusion.FAILURE, complete.getConclusion());
+ assertTrue(complete.getOutput().isPresent());
+
+ ChecksOutput completeOutput = complete.getOutput().get();
+ assertTrue(completeOutput.getSummary().isPresent());
+ assertEquals("occurred while executing withChecks step.", completeOutput.getSummary().get());
+ assertTrue(completeOutput.getText().isPresent());
+ assertEquals("org.jenkinsci.plugins.workflow.steps.FlowInterruptedException", completeOutput.getText().get());
+ }
+}