From 3bce6efd1f6957010499a58415b09fff28fbfbff Mon Sep 17 00:00:00 2001 From: Bill Collins Date: Wed, 23 Dec 2020 12:45:49 +0000 Subject: [PATCH] Support withChecks --- pom.xml | 26 ++- .../steps/input/InputStepExecution.java | 78 ++++++- .../steps/input/InputStepWithChecksTest.java | 193 ++++++++++++++++++ 3 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/jenkinsci/plugins/workflow/support/steps/input/InputStepWithChecksTest.java 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()); + } +}