diff --git a/README.md b/README.md index 68764ab6..2244705a 100644 --- a/README.md +++ b/README.md @@ -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: ``` diff --git a/pom.xml b/pom.xml index d8035ca2..118b3bcc 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 8 - 1.5.1 + 1.6.0 -SNAPSHOT @@ -118,6 +118,12 @@ \d+\.\d+\.\d+ + + java.field.serialVersionUIDUnchanged + field io.jenkins.plugins.checks.steps.PublishChecksStep.serialVersionUID + 1 + Adding actions list with an initial empty value should not break the compatibility. + diff --git a/src/main/java/io/jenkins/plugins/checks/api/ChecksAction.java b/src/main/java/io/jenkins/plugins/checks/api/ChecksAction.java index 0620f345..a47a3ae8 100644 --- a/src/main/java/io/jenkins/plugins/checks/api/ChecksAction.java +++ b/src/main/java/io/jenkins/plugins/checks/api/ChecksAction.java @@ -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; diff --git a/src/main/java/io/jenkins/plugins/checks/steps/PublishChecksStep.java b/src/main/java/io/jenkins/plugins/checks/steps/PublishChecksStep.java index 82d4f479..ebea6acb 100644 --- a/src/main/java/io/jenkins/plugins/checks/steps/PublishChecksStep.java +++ b/src/main/java/io/jenkins/plugins/checks/steps/PublishChecksStep.java @@ -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; @@ -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 actions = Collections.emptyList(); /** * Constructor used for pipeline by Stapler. @@ -86,6 +89,11 @@ public void setConclusion(final ChecksConclusion conclusion) { this.conclusion = conclusion; } + @DataBoundSetter + public void setActions(final List actions) { + this.actions = actions; + } + public String getName() { return name; } @@ -114,6 +122,10 @@ public ChecksConclusion getConclusion() { return conclusion; } + public List getActions() { + return actions; + } + @Override public StepExecution start(final StepContext stepContext) { return new PublishChecksStepExecution(stepContext, this); @@ -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 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 { + } + } } diff --git a/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/config.jelly b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/config.jelly new file mode 100644 index 00000000..8c1cea00 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/config.jelly @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/help-description.html b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/help-description.html new file mode 100644 index 00000000..ed0104a1 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/help-description.html @@ -0,0 +1,3 @@ +
+ Detailed description of the action's purpose, functionality, and so on. +
diff --git a/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/help-identifier.html b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/help-identifier.html new file mode 100644 index 00000000..d41e62d1 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/help-identifier.html @@ -0,0 +1,5 @@ +
+ 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. +
diff --git a/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/help-label.html b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/help-label.html new file mode 100644 index 00000000..4f32e4cc --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/StepChecksAction/help-label.html @@ -0,0 +1,3 @@ +
+ The label to be displayed on the checks report for this action. +
diff --git a/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/config.jelly b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/config.jelly index 4930968a..bfaa7346 100644 --- a/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/config.jelly +++ b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/config.jelly @@ -29,4 +29,16 @@ + +
+ + +
+ +
+
+
+
+
+ diff --git a/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/config.properties b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/config.properties index 1907e359..741e2164 100644 --- a/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/config.properties +++ b/src/main/resources/io/jenkins/plugins/checks/steps/PublishChecksStep/config.properties @@ -5,3 +5,4 @@ title.text=Text title.detailsURL=Details URL title.status=Status title.conclusion=Conclusion +title.actions=Actions diff --git a/src/test/java/io/jenkins/plugins/checks/steps/PublishChecksStepITest.java b/src/test/java/io/jenkins/plugins/checks/steps/PublishChecksStepITest.java index e0807060..75f2505d 100644 --- a/src/test/java/io/jenkins/plugins/checks/steps/PublishChecksStepITest.java +++ b/src/test/java/io/jenkins/plugins/checks/steps/PublishChecksStepITest.java @@ -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; @@ -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"); @@ -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"); diff --git a/src/test/java/io/jenkins/plugins/checks/steps/PublishChecksStepTest.java b/src/test/java/io/jenkins/plugins/checks/steps/PublishChecksStepTest.java index 78d5ad89..f9284282 100644 --- a/src/test/java/io/jenkins/plugins/checks/steps/PublishChecksStepTest.java +++ b/src/test/java/io/jenkins/plugins/checks/steps/PublishChecksStepTest.java @@ -2,6 +2,7 @@ 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; @@ -9,28 +10,21 @@ 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() @@ -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() @@ -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() @@ -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 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() @@ -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()); } @@ -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; }