Skip to content

Commit

Permalink
Support withChecks
Browse files Browse the repository at this point in the history
  • Loading branch information
mrginglymus committed Dec 23, 2020
1 parent ebf8537 commit be273e9
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 5 deletions.
26 changes: 22 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>4.6</version>
<version>4.13</version>
<relativePath />
</parent>
<artifactId>pipeline-input-step</artifactId>
Expand Down Expand Up @@ -39,16 +39,17 @@
<properties>
<revision>2.13</revision>
<changelist>-SNAPSHOT</changelist>
<jenkins.version>2.176.4</jenkins.version>
<jenkins.version>2.204.6</jenkins.version>
<java.level>8</java.level>
<checks-api.version>1.2.0</checks-api.version>
<useBeta>true</useBeta>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.176.x</artifactId>
<version>11</version>
<artifactId>bom-2.204.x</artifactId>
<version>18</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Expand All @@ -75,6 +76,16 @@
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials</artifactId>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>plugin-util-api</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>checks-api</artifactId>
<version>${checks-api.version}</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
Expand Down Expand Up @@ -116,5 +127,12 @@
<artifactId>credentials-binding</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>checks-api</artifactId>
<version>${checks-api.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,15 +21,22 @@
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;
import net.sf.json.JSONObject;
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;
Expand All @@ -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;

Expand All @@ -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);

Expand Down Expand Up @@ -186,6 +201,10 @@ public HttpResponse proceed(@CheckForNull Map<String,Object> 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();
Expand Down Expand Up @@ -402,5 +421,62 @@ private Object convert(String name, ParameterValue v) throws IOException, Interr
}
}

Optional<String> 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<String, Object> 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<String, Object> 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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
The parameter entry screen can be accessed via a link at the bottom of the build console log or
via link in the sidebar for a build.
</p>
<p>
If wrapped in a <pre>withChecks</pre> block (from the <a href="https://plugins.jenkins.io/checks-api/">Checks API plugin</a>)
then input required checks will be published to the relevant platform, if available.
</p>
</div>
Original file line number Diff line number Diff line change
@@ -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<ChecksDetails> runAndSubmit(String script) throws Exception {
return run(script, false);
}

private List<ChecksDetails> runAndAbort(String script) throws Exception {
return run(script, true);
}

private List<ChecksDetails> run(String script, boolean abort) throws Exception {

WorkflowJob job = j.createProject(WorkflowJob.class);
job.setDefinition(new CpsFlowDefinition(script, true));

QueueTaskFuture<WorkflowRun> 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> 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> 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> 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> 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());
}
}

0 comments on commit be273e9

Please sign in to comment.