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 actions in publishChecks Step #85

Merged
merged 12 commits into from
Feb 21, 2021
Merged
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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,17 @@ If enabled, the statuses will be published in different stages of a Jenkins buil
- publishChecks: you can publish checks directly in the pipeline script instead of depending on consumer plugins:

```
publishChecks name: 'example', title: 'Pipeline Check', summary: 'check through pipeline', text: 'you can publish checks in pipeline script', detailsURL: 'https://github.com/jenkinsci/checks-api-plugin#pipeline-usage'
publishChecks name: 'example', title: 'Pipeline Check', summary: 'check through pipeline',
text: 'you can publish checks in pipeline script',
detailsURL: 'https://github.com/jenkinsci/checks-api-plugin#pipeline-usage',
actions: [[label:'an-user-request-action', description:'actions allow users to request pre-defined behaviours', identifier:'an unique identifier']]
```

*To use customized actions, you will need to write a Jenkins plugin
If you want to add GitHub checks actions which are basically buttons on the checks report,
you need to extend [GHEventSubscriber](https://github.com/jenkinsci/github-plugin/blob/master/src/main/java/org/jenkinsci/plugins/github/extension/GHEventsSubscriber.java) to handle the event,
see [the handler](https://github.com/jenkinsci/github-checks-plugin/blob/ea060be67dad522ab6c31444fc4274955ac6e918/src/main/java/io/jenkins/plugins/checks/github/CheckRunGHEventSubscriber.java) for re-run requests as an example.*

- withChecks: you can inject the check's name into the closure for other steps to use:

```
Expand Down
8 changes: 7 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<properties>
<java.level>8</java.level>
<revision>1.5.1</revision>
<revision>1.6.0</revision>
<changelist>-SNAPSHOT</changelist>

<!-- Jenkins Plug-in Dependencies Versions -->
Expand Down Expand Up @@ -118,6 +118,12 @@
<versionFormat>\d+\.\d+\.\d+</versionFormat>
<analysisConfiguration>
<revapi.ignore combine.children="append">
<item>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this still needed? I think you adjusted the implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, still needed, since we still add new field to PublishChecks step.

[INFO] API problems found.
[INFO] If you're using the semver-ignore extension, update your module's version to one compatible with the current changes (e.g. mvn package revapi:update-versions). If you want to explicitly ignore these changes or provide justifications for them, add the json snippets to your Revapi configuration for the "revapi.differences" extension.

{
  "code": "java.field.serialVersionUIDUnchanged",
  "old": "field io.jenkins.plugins.checks.steps.PublishChecksStep.serialVersionUID",
  "new": "field io.jenkins.plugins.checks.steps.PublishChecksStep.serialVersionUID",
  "serialVersionUID": "1",
  "justification": "ADD YOUR EXPLANATION FOR THE NECESSITY OF THIS CHANGE"
  /*  "package": "io.jenkins.plugins.checks.steps",
  "classQualifiedName": "io.jenkins.plugins.checks.steps.PublishChecksStep",
  "classSimpleName": "PublishChecksStep",
  "fieldName": "serialVersionUID",
  "elementKind": "field",
  "oldArchive": "io.jenkins.plugins:checks-api:hpi:1.5.0",
  "oldArchiveRole": "primary",
  "newArchive": "io.jenkins.plugins:checks-api:hpi:1.6.0-SNAPSHOT",
  "newArchiveRole": "primary",
  "breaksSemanticVersioning": "true",
  */
},

<code>java.field.serialVersionUIDUnchanged</code>
<old>field io.jenkins.plugins.checks.steps.PublishChecksStep.serialVersionUID</old>
<serialVersionUID>1</serialVersionUID>
<justification>Adding actions list with an initial empty value should not break the compatibility.</justification>
</item>
</revapi.ignore>
</analysisConfiguration>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class ChecksAction {
*/
@SuppressFBWarnings("NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE")
public ChecksAction(@CheckForNull final String label, @CheckForNull final String description,
@CheckForNull final String identifier) {
@CheckForNull final String identifier) {
this.label = label;
this.description = description;
this.identifier = identifier;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import edu.hm.hafner.util.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.util.ListBoxModel;
Expand Down Expand Up @@ -31,6 +33,7 @@ public class PublishChecksStep extends Step implements Serializable {
private String detailsURL = StringUtils.EMPTY;
private ChecksStatus status = ChecksStatus.COMPLETED;
private ChecksConclusion conclusion = ChecksConclusion.SUCCESS;
private List<StepChecksAction> actions = Collections.emptyList();

/**
* Constructor used for pipeline by Stapler.
Expand Down Expand Up @@ -86,6 +89,11 @@ public void setConclusion(final ChecksConclusion conclusion) {
this.conclusion = conclusion;
}

@DataBoundSetter
public void setActions(final List<StepChecksAction> actions) {
this.actions = actions;
}

public String getName() {
return name;
}
Expand Down Expand Up @@ -114,6 +122,10 @@ public ChecksConclusion getConclusion() {
return conclusion;
}

public List<StepChecksAction> getActions() {
return actions;
}

@Override
public StepExecution start(final StepContext stepContext) {
return new PublishChecksStepExecution(stepContext, this);
Expand Down Expand Up @@ -209,7 +221,64 @@ ChecksDetails extractChecksDetails() throws IOException, InterruptedException {
.withSummary(step.getSummary())
.withText(step.getText())
.build())
.withActions(step.getActions().stream()
.map(StepChecksAction::getAction)
.collect(Collectors.toList()))
.build();
}
}

/**
* A simple wrapper for {@link ChecksAction} to allow users add checks actions by {@link PublishChecksStep}.
*/
public static class StepChecksAction extends AbstractDescribableImpl<StepChecksAction> implements Serializable {
private static final long serialVersionUID = 1L;
private final String label;
private final String identifier;
private String description = StringUtils.EMPTY;

/**
* Creates an instance that wraps a newly constructed {@link ChecksAction} with according parameters.
*
* @param label
* label of the action to display in the checks report on SCMs
* @param identifier
* identifier for the action, useful to identify which action is requested by users
*/
@DataBoundConstructor
public StepChecksAction(final String label, final String identifier) {
super();

this.label = label;
this.identifier = identifier;
}

@DataBoundSetter
public void setDescription(final String description) {
this.description = description;
}

public String getLabel() {
return label;
}

public String getDescription() {
return description;
}

public String getIdentifier() {
return identifier;
}

public ChecksAction getAction() {
return new ChecksAction(label, description, identifier);
}

/**
* Descriptor for {@link StepChecksAction}, required for Pipeline Snippet Generator.
*/
@Extension
public static class StepChecksActionDescriptor extends Descriptor<StepChecksAction> {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">

<f:entry title="Label" field="label">
<f:textbox />
</f:entry>

<f:entry title="Identifier" field="identifier">
<f:textbox />
</f:entry>

<f:entry title="Description" field="description">
<f:textbox />
</f:entry>

</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
Detailed description of the action's purpose, functionality, and so on.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div>
The unique identifier for the action. Since for SCM platforms like GitHub, this is the only field that would be
sent back to your Jenkins instance when an action is requested, so you may need to use this field to have more
information besides the basic type of the action.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div>
The label to be displayed on the checks report for this action.
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,16 @@
<f:select default="SUCCESS"/>
</f:entry>

<f:entry title="${%title.actions}">
<div id="actions">
<f:repeatableProperty field="actions" add="${%Add Actions}">
<f:entry>
<div align="right">
<f:repeatableDeleteButton/>
</div>
</f:entry>
</f:repeatableProperty>
</div>
</f:entry>

</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ title.text=Text
title.detailsURL=Details URL
title.status=Status
title.conclusion=Conclusion
title.actions=Actions
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.jenkins.plugins.checks.steps;

import io.jenkins.plugins.checks.api.ChecksAction;
import io.jenkins.plugins.checks.api.ChecksConclusion;
import io.jenkins.plugins.checks.api.ChecksDetails;
import io.jenkins.plugins.checks.api.ChecksOutput;
Expand Down Expand Up @@ -36,7 +37,8 @@ public void shouldPublishChecksWhenUsingPipeline() throws IOException {
WorkflowJob job = createPipeline();
job.setDefinition(asStage("publishChecks name: 'customized-check', "
+ "summary: 'customized check created in pipeline', title: 'Publish Checks Step', "
+ "text: 'Pipeline support for checks', status: 'IN_PROGRESS', conclusion: 'NONE'"));
+ "text: 'Pipeline support for checks', status: 'IN_PROGRESS', conclusion: 'NONE', "
+ "actions: [[label:'test-label', description:'test-desc', identifier:'test-id']]"));

assertThat(JenkinsRule.getLog(buildSuccessfully(job)))
.contains("[Pipeline] publishChecks");
Expand All @@ -49,6 +51,8 @@ public void shouldPublishChecksWhenUsingPipeline() throws IOException {
assertThat(details.getOutput()).isPresent();
assertThat(details.getStatus()).isEqualTo(ChecksStatus.IN_PROGRESS);
assertThat(details.getConclusion()).isEqualTo(ChecksConclusion.NONE);
assertThat(details.getActions()).usingFieldByFieldElementComparator().containsExactlyInAnyOrder(
new ChecksAction("test-label", "test-desc", "test-id"));

ChecksOutput output = details.getOutput().get();
assertThat(output.getTitle()).isPresent().get().isEqualTo("Publish Checks Step");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,29 @@

import hudson.model.Run;
import hudson.model.TaskListener;
import io.jenkins.plugins.checks.api.ChecksAction;
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 org.apache.commons.lang3.StringUtils;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepExecution;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.Objects;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static io.jenkins.plugins.checks.assertions.Assertions.assertThat;
import static org.mockito.Mockito.*;

class PublishChecksStepTest {

StepContext getStepContext() throws IOException, InterruptedException {
StepContext context = mock(StepContext.class);
when(context.get(Run.class)).thenReturn(mock(Run.class));
when(context.get(TaskListener.class)).thenReturn(TaskListener.NULL);
return context;
}

@Test
void shouldPublishCheckWithDefaultValues() throws IOException, InterruptedException {
StepExecution execution = new PublishChecksStep().start(getStepContext());
StepExecution execution = new PublishChecksStep().start(createStepContext());
assertThat(execution).isInstanceOf(PublishChecksStep.PublishChecksStepExecution.class);
assertThat(((PublishChecksStep.PublishChecksStepExecution)execution).extractChecksDetails())
.usingRecursiveComparison()
Expand All @@ -44,15 +38,16 @@ void shouldPublishCheckWithDefaultValues() throws IOException, InterruptedExcept
.withSummary(StringUtils.EMPTY)
.withText(StringUtils.EMPTY)
.build())
.withActions(Collections.emptyList())
.build());
}

@Test
void shouldPublishCheckWithStatusInProgress() throws IOException, InterruptedException {
PublishChecksStep step = getModifiedPublishChecksStepObject("an in progress build",
ChecksStatus.IN_PROGRESS, null);
PublishChecksStep step = createPublishChecksStep("an in progress build", ChecksStatus.IN_PROGRESS,
ChecksConclusion.NONE);

StepExecution execution = step.start(getStepContext());
StepExecution execution = step.start(createStepContext());
assertThat(execution).isInstanceOf(PublishChecksStep.PublishChecksStepExecution.class);
assertThat(((PublishChecksStep.PublishChecksStepExecution)execution).extractChecksDetails())
.usingRecursiveComparison()
Expand All @@ -70,11 +65,11 @@ void shouldPublishCheckWithStatusInProgress() throws IOException, InterruptedExc
}

@Test
void shouldPublishCheckWithStatusQueue() throws IOException, InterruptedException {
PublishChecksStep step = getModifiedPublishChecksStepObject("a queued build",
ChecksStatus.QUEUED, null);
void shouldPublishCheckWithStatusQueued() throws IOException, InterruptedException {
PublishChecksStep step = createPublishChecksStep("a queued build", ChecksStatus.QUEUED,
ChecksConclusion.NONE);

StepExecution execution = step.start(getStepContext());
StepExecution execution = step.start(createStepContext());
assertThat(execution).isInstanceOf(PublishChecksStep.PublishChecksStepExecution.class);
assertThat(((PublishChecksStep.PublishChecksStepExecution)execution).extractChecksDetails())
.usingRecursiveComparison()
Expand All @@ -93,10 +88,23 @@ void shouldPublishCheckWithStatusQueue() throws IOException, InterruptedExceptio

@Test
void shouldPublishCheckWithSetValues() throws IOException, InterruptedException {
PublishChecksStep step = getModifiedPublishChecksStepObject("a failed build",
ChecksStatus.IN_PROGRESS, ChecksConclusion.FAILURE);
PublishChecksStep step = createPublishChecksStep("a failed build", ChecksStatus.IN_PROGRESS,
ChecksConclusion.FAILURE);

List<PublishChecksStep.StepChecksAction> actions = Arrays.asList(
new PublishChecksStep.StepChecksAction("label-1", "identifier-1"),
new PublishChecksStep.StepChecksAction("label-2", "identifier-2"));
actions.get(1).setDescription("description-2");

step.setActions(actions);
assertThat(step.getActions().stream().map(PublishChecksStep.StepChecksAction::getLabel))
.containsExactlyInAnyOrder("label-1", "label-2");
assertThat(step.getActions().stream().map(PublishChecksStep.StepChecksAction::getDescription))
.containsExactlyInAnyOrder(StringUtils.EMPTY, "description-2");
assertThat(step.getActions().stream().map(PublishChecksStep.StepChecksAction::getIdentifier))
.containsExactlyInAnyOrder("identifier-1", "identifier-2");

StepExecution execution = step.start(getStepContext());
StepExecution execution = step.start(createStepContext());
assertThat(execution).isInstanceOf(PublishChecksStep.PublishChecksStepExecution.class);
assertThat(((PublishChecksStep.PublishChecksStepExecution)execution).extractChecksDetails())
.usingRecursiveComparison()
Expand All @@ -110,6 +118,9 @@ void shouldPublishCheckWithSetValues() throws IOException, InterruptedException
.withSummary("a check made by Jenkins")
.withText("a failed build")
.build())
.withActions(Arrays.asList(
new ChecksAction("label-1", "", "identifier-1"),
new ChecksAction("label-2", "description-2", "identifier-2")))
.build());
}

Expand All @@ -121,20 +132,23 @@ void shouldDefinePublishChecksStepDescriptorCorrectly() {
assertThat(descriptor.getRequiredContext().toArray()).containsExactlyInAnyOrder(Run.class, TaskListener.class);
}

private PublishChecksStep getModifiedPublishChecksStepObject(final String stepText, final ChecksStatus status,
final ChecksConclusion conclusion) {
private StepContext createStepContext() throws IOException, InterruptedException {
StepContext context = mock(StepContext.class);
when(context.get(Run.class)).thenReturn(mock(Run.class));
when(context.get(TaskListener.class)).thenReturn(TaskListener.NULL);
return context;
}

private PublishChecksStep createPublishChecksStep(final String stepText, final ChecksStatus status,
final ChecksConclusion conclusion) {
PublishChecksStep step = new PublishChecksStep();
step.setName("Jenkins");
step.setSummary("a check made by Jenkins");
step.setTitle("Jenkins Build");
step.setText(stepText);
if (Objects.nonNull(status)) {
step.setStatus(status);
}
if (Objects.nonNull(conclusion)) {
step.setConclusion(conclusion);
}
step.setDetailsURL("http://ci.jenkins.io");
step.setText(stepText);
step.setStatus(status);
step.setConclusion(conclusion);

return step;
}
Expand Down