Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support withChecks #48

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.1-rc175.bb070c651870</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.1</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.CANCELED, 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("Rejected by bob", completeOutput.getText().get());
}
}