From a8fa8667ac47378b9cc649776365b74d86366b43 Mon Sep 17 00:00:00 2001 From: Kezhi Xiong Date: Sun, 20 Dec 2020 14:25:39 +0800 Subject: [PATCH] Add withChecks step (#49) * Add withChecks step * Add test for WithChecksStep. * Add document and snippet generator support. * Bump version to 1.2.0 for adding ChecksInfo and WithChecksStep. * remove unused setter * Remove unused import * Add callback for withChecks. * Add consumer doc. * Remove unnessary denpendency version in pom * Improve consumer doc --- README.md | 10 +- docs/consumers-guide.md | 20 ++ pom.xml | 5 + .../plugins/checks/steps/ChecksInfo.java | 28 +++ .../plugins/checks/steps/WithChecksStep.java | 188 ++++++++++++++++++ .../plugins/checks/steps/package-info.java | 8 + .../checks/steps/WithChecksStep/config.jelly | 8 + .../steps/WithChecksStep/config.properties | 1 + .../checks/steps/WithChecksStepITest.java | 104 ++++++++++ .../checks/steps/WithChecksStepTest.java | 33 +++ 10 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/jenkins/plugins/checks/steps/ChecksInfo.java create mode 100644 src/main/java/io/jenkins/plugins/checks/steps/WithChecksStep.java create mode 100644 src/main/java/io/jenkins/plugins/checks/steps/package-info.java create mode 100644 src/main/resources/io/jenkins/plugins/checks/steps/WithChecksStep/config.jelly create mode 100644 src/main/resources/io/jenkins/plugins/checks/steps/WithChecksStep/config.properties create mode 100644 src/test/java/io/jenkins/plugins/checks/steps/WithChecksStepITest.java create mode 100644 src/test/java/io/jenkins/plugins/checks/steps/WithChecksStepTest.java diff --git a/README.md b/README.md index 3160a063..68764ab6 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,20 @@ If enabled, the statuses will be published in different stages of a Jenkins buil ### Pipeline Usage -Instead of depending on consumers plugins, the users can publish their checks directly in the pipeline script: +- 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' ``` +- withChecks: you can inject the check's name into the closure for other steps to use: + +``` +withChecks(name: 'injected name') { + // some other steps that will extract the name +} +``` + ## Guides - [Consumers Guide](docs/consumers-guide.md) diff --git a/docs/consumers-guide.md b/docs/consumers-guide.md index ed676b51..ddc53412 100644 --- a/docs/consumers-guide.md +++ b/docs/consumers-guide.md @@ -42,3 +42,23 @@ Consumers can set these parameters through the checks models: The publishers are created through the static factory method (`fromRun` or `fromJob`) of `ChecksPublisherFactory`. The factory will iterate all available implementations of the `ChecksPublisher` in order to find the suitable publisher for the Jenkins `Run` or `Job`. + +## Pipeline Step: withChecks + +The `withChecks` step injects a `ChecksInfo` object into its closure by users: + +```groovy +withChecks('MyCheck') { + junit '*.xml' +} +``` + +The injected object can be resolved by other plugin developers in their [Step](https://javadoc.jenkins.io/plugin/workflow-step-api/org/jenkinsci/plugins/workflow/steps/Step.html) implementation: + +``` +getContext().get(ChecksInfo.class) +``` + +Currently, the `ChecksInfo` object only includes a `name` specified by users, +it is recommended that you look for this name and set it over your default checks name + diff --git a/pom.xml b/pom.xml index d486d803..ddb15a6f 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,11 @@ test-jar + + org.jenkins-ci.plugins + display-url-api + + org.jenkins-ci.plugins.workflow workflow-step-api diff --git a/src/main/java/io/jenkins/plugins/checks/steps/ChecksInfo.java b/src/main/java/io/jenkins/plugins/checks/steps/ChecksInfo.java new file mode 100644 index 00000000..f9e3c644 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/checks/steps/ChecksInfo.java @@ -0,0 +1,28 @@ +package io.jenkins.plugins.checks.steps; + +import java.io.Serializable; +import java.util.Objects; + +/** + * A collection of checks properties that will be injected into {@link WithChecksStep} closure. + */ +public class ChecksInfo implements Serializable { + private static final long serialVersionUID = 1L; + + private final String name; + + /** + * Creates a {@link ChecksInfo} with checks name. + * + * @param name + * the name of the check + */ + public ChecksInfo(final String name) { + Objects.requireNonNull(name); + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/io/jenkins/plugins/checks/steps/WithChecksStep.java b/src/main/java/io/jenkins/plugins/checks/steps/WithChecksStep.java new file mode 100644 index 00000000..7bf8d0d0 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/checks/steps/WithChecksStep.java @@ -0,0 +1,188 @@ +package io.jenkins.plugins.checks.steps; + +import edu.hm.hafner.util.VisibleForTesting; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.model.Run; +import hudson.model.TaskListener; +import io.jenkins.plugins.checks.api.*; +import io.jenkins.plugins.util.PluginLogger; +import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider; +import org.jenkinsci.plugins.workflow.steps.*; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.IOException; +import java.io.Serializable; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static hudson.Util.fixNull; + +/** + * Pipeline step that injects a {@link ChecksInfo} into the closure. + */ +public class WithChecksStep extends Step implements Serializable { + private static final long serialVersionUID = 1L; + + private final String name; + + /** + * Creates the step with a name to inject. + * + * @param name name to inject + */ + @DataBoundConstructor + public WithChecksStep(final String name) { + super(); + + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public StepExecution start(final StepContext stepContext) { + return new WithChecksStepExecution(stepContext, this); + } + + /** + * This step's descriptor which defines the function name ("withChecks") and required context. + */ + @Extension + public static class WithChecksStepDescriptor extends StepDescriptor { + @Override + public String getFunctionName() { + return "withChecks"; + } + + @Override + public Set> getRequiredContext() { + return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(Run.class, TaskListener.class))); + } + + @Override + public boolean takesImplicitBlockArgument() { + return true; + } + + @NonNull + @Override + public String getDisplayName() { + return "Inject checks properties into its closure"; + } + } + + /** + * The step's execution to actually inject the {@link ChecksInfo} into the closure. + */ + static class WithChecksStepExecution extends AbstractStepExecutionImpl { + private static final long serialVersionUID = 1L; + private static final Logger SYSTEM_LOGGER = Logger.getLogger(WithChecksStepExecution.class.getName()); + + private final WithChecksStep step; + + WithChecksStepExecution(final StepContext context, final WithChecksStep step) { + super(context); + this.step = step; + } + + @Override + public boolean start() { + ChecksInfo info = extractChecksInfo(); + getContext().newBodyInvoker() + .withContext(info) + .withCallback(new WithChecksCallBack(info)) + .start(); + return false; + } + + @VisibleForTesting + ChecksInfo extractChecksInfo() { + return new ChecksInfo(step.name); + } + + @Override + public void stop(final Throwable cause) { + publish(getContext(), new ChecksDetails.ChecksDetailsBuilder() + .withName(step.getName()) + .withStatus(ChecksStatus.COMPLETED) + .withConclusion(ChecksConclusion.CANCELED)); + } + + private void publish(final StepContext context, final ChecksDetails.ChecksDetailsBuilder builder) { + TaskListener listener = TaskListener.NULL; + try { + listener = fixNull(context.get(TaskListener.class), TaskListener.NULL); + } + catch (IOException | InterruptedException e) { + SYSTEM_LOGGER.log(Level.WARNING, "Failed getting TaskListener from the context: " + e); + } + + PluginLogger pluginLogger = new PluginLogger(listener.getLogger(), "Checks API"); + + Run run; + try { + run = context.get(Run.class); + } + catch (IOException | InterruptedException e) { + String msg = "Failed getting Run from the context on the start of withChecks step: " + e; + pluginLogger.log(msg); + SYSTEM_LOGGER.log(Level.WARNING, msg); + context.onFailure(new IllegalStateException(msg)); + return; + } + + if (run == null) { + String msg = "No Run found in the context."; + pluginLogger.log(msg); + SYSTEM_LOGGER.log(Level.WARNING, msg); + context.onFailure(new IllegalStateException(msg)); + return; + } + + ChecksPublisherFactory.fromRun(run, listener) + .publish(builder.withDetailsURL(DisplayURLProvider.get().getRunURL(run)) + .build()); + } + + class WithChecksCallBack extends BodyExecutionCallback { + private static final long serialVersionUID = 1L; + + private final ChecksInfo info; + + WithChecksCallBack(final ChecksInfo info) { + super(); + + this.info = info; + } + + @Override + public void onStart(final StepContext context) { + publish(context, new ChecksDetails.ChecksDetailsBuilder() + .withName(info.getName()) + .withStatus(ChecksStatus.IN_PROGRESS) + .withConclusion(ChecksConclusion.NONE)); + } + + @Override + public void onSuccess(final StepContext context, final Object result) { + context.onSuccess(result); + } + + @Override + public void onFailure(final StepContext context, final Throwable t) { + publish(context, new ChecksDetails.ChecksDetailsBuilder() + .withName(info.getName()) + .withStatus(ChecksStatus.COMPLETED) + .withConclusion(ChecksConclusion.FAILURE) + .withOutput(new ChecksOutput.ChecksOutputBuilder() + .withSummary("occurred while executing withChecks step.") + .withText(t.toString()).build())); + context.onFailure(t); + } + } + } +} diff --git a/src/main/java/io/jenkins/plugins/checks/steps/package-info.java b/src/main/java/io/jenkins/plugins/checks/steps/package-info.java new file mode 100644 index 00000000..c3a9aa05 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/checks/steps/package-info.java @@ -0,0 +1,8 @@ +/** + * Provides default Findbugs annotations. + */ +@DefaultAnnotation(NonNull.class) +package io.jenkins.plugins.checks.steps; + +import edu.umd.cs.findbugs.annotations.DefaultAnnotation; +import edu.umd.cs.findbugs.annotations.NonNull; diff --git a/src/main/resources/io/jenkins/plugins/checks/steps/WithChecksStep/config.jelly b/src/main/resources/io/jenkins/plugins/checks/steps/WithChecksStep/config.jelly new file mode 100644 index 00000000..88b713b5 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/checks/steps/WithChecksStep/config.jelly @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/io/jenkins/plugins/checks/steps/WithChecksStep/config.properties b/src/main/resources/io/jenkins/plugins/checks/steps/WithChecksStep/config.properties new file mode 100644 index 00000000..d4c852fb --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/checks/steps/WithChecksStep/config.properties @@ -0,0 +1 @@ +title.name=Name diff --git a/src/test/java/io/jenkins/plugins/checks/steps/WithChecksStepITest.java b/src/test/java/io/jenkins/plugins/checks/steps/WithChecksStepITest.java new file mode 100644 index 00000000..a6dc5bab --- /dev/null +++ b/src/test/java/io/jenkins/plugins/checks/steps/WithChecksStepITest.java @@ -0,0 +1,104 @@ +package io.jenkins.plugins.checks.steps; + +import hudson.model.Run; +import io.jenkins.plugins.util.IntegrationTestWithJenkinsPerTest; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.steps.*; +import org.junit.Test; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the "withChecks" step. + */ +public class WithChecksStepITest extends IntegrationTestWithJenkinsPerTest { + /** + * Tests that the step can inject the {@link ChecksInfo} into the closure. + */ + @Test + public void shouldInjectChecksInfoIntoClosure() { + WorkflowJob job = createPipeline(); + job.setDefinition(asStage("withChecks('test injection') { assertChecksInfoInjection('test injection') }")); + + buildSuccessfully(job); + } + + /** + * Assert that the injected {@link ChecksInfo} is as expected. + */ + @TestExtension + public static class AssertChecksInfoInjectionStep extends Step implements Serializable { + private static final long serialVersionUID = 1L; + + private final ChecksInfo expected; + + /** + * Required by {@link TestExtension} annotation. + */ + public AssertChecksInfoInjectionStep() { + this(""); + } + + /** + * Creates the step with expected name injected. + * + * @param expectedName + * expected name that will be injected + */ + @DataBoundConstructor + public AssertChecksInfoInjectionStep(final String expectedName) { + super(); + + expected = new ChecksInfo(expectedName); + } + + @Override + public StepExecution start(final StepContext stepContext) { + return new AssertChecksInfoInjectionStepExecution(stepContext, this); + } + + /** + * Descriptor for {@link AssertChecksInfoInjectionStep} that defines function name and required context. + */ + @TestExtension + public static class AssertChecksInfoStepInjectionDescriptor extends StepDescriptor { + @Override + public String getFunctionName() { + return "assertChecksInfoInjection"; + } + + @Override + public Set> getRequiredContext() { + return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(Run.class, ChecksInfo.class))); + } + } + + static class AssertChecksInfoInjectionStepExecution extends SynchronousNonBlockingStepExecution { + private static final long serialVersionUID = 1L; + + private final AssertChecksInfoInjectionStep step; + + AssertChecksInfoInjectionStepExecution(final StepContext context, final AssertChecksInfoInjectionStep step) { + super(context); + + this.step = step; + } + + @Override + protected Void run() throws Exception { + assertThat(getContext().get(ChecksInfo.class)) + .usingRecursiveComparison() + .isEqualTo(step.expected); + return null; + } + } + } +} diff --git a/src/test/java/io/jenkins/plugins/checks/steps/WithChecksStepTest.java b/src/test/java/io/jenkins/plugins/checks/steps/WithChecksStepTest.java new file mode 100644 index 00000000..86d9941f --- /dev/null +++ b/src/test/java/io/jenkins/plugins/checks/steps/WithChecksStepTest.java @@ -0,0 +1,33 @@ +package io.jenkins.plugins.checks.steps; + +import hudson.model.Run; +import hudson.model.TaskListener; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class WithChecksStepTest { + @Test + void shouldStartWithCorrectExecution() 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); + + assertThat(((WithChecksStep.WithChecksStepExecution) (new WithChecksStep("test").start(context))) + .extractChecksInfo()) + .hasFieldOrPropertyWithValue("name", "test"); + } + + @Test + void shouldDefinePublishChecksStepDescriptorCorrectly() { + WithChecksStep.WithChecksStepDescriptor descriptor = new WithChecksStep.WithChecksStepDescriptor(); + assertThat(descriptor.getFunctionName()).isEqualTo("withChecks"); + assertThat(descriptor.getRequiredContext().toArray()).containsExactlyInAnyOrder(Run.class, TaskListener.class); + } +}