Skip to content

Commit

Permalink
Add includeStage to allow starting check immediately for JUnit
Browse files Browse the repository at this point in the history
  • Loading branch information
timja committed Apr 3, 2024
1 parent fb71c46 commit 74e1aa1
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 71 deletions.
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,57 @@ see [the handler](https://github.com/jenkinsci/github-checks-plugin/blob/ea060be

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

```
withChecks(name: 'injected name') {
```groovy
withChecks('injected name') {
// some other steps that will extract the name
}
```

`withChecks` will publish an in progress check immediately and then other consuming plugins will publish the final check.

You can also include the checks stage name with `includeStage`:

```groovy
withChecks(name: 'Tests', includeStage: true) {
sh 'mvn -Dmaven.test.failure.ignore=true clean verify'
junit '**/target/surefire-reports/TEST-*.xml'
}
```

Combining `includeStage` with the JUnit plugin works well to publish checks for each test suite:

![With Checks multiple stages](docs/images/github-status.png)

<details>

<summary>Example full pipeline with parallel stages</summary>

```groovy
def axes = [
platforms: ['linux', 'windows'],
jdks: [17, 21],
]
def builds = [:]
axes.values().combinations {
def (platform, jdk) = it
builds["${platform}-jdk${jdk}"] = {
node(platform) {
stage("${platform.capitalize()} - JDK ${jdk} - Test") {
checkout scm
withChecks(name: 'Tests', includeStage: true) {
sh 'mvn -Dmaven.test.failure.ignore=true clean verify'
junit '**/target/surefire-reports/TEST-*.xml'
}
}
}
}
}
parallel builds
```

</details>

## Guides

- [Consumers Guide](docs/consumers-guide.md)
Expand Down
Binary file added docs/images/multiple-with-checks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<description>Defines an API for Jenkins to publish checks to SCM platforms.</description>

<properties>
<revision>2.1.0</revision>
<revision>2.2.0</revision>
<changelist>-SNAPSHOT</changelist>
<gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>

Expand Down Expand Up @@ -166,6 +166,12 @@
<serialVersionUID>1</serialVersionUID>
<justification>Adding actions list with an initial empty value should not break the compatibility.</justification>
</item>
<item>
<code>java.field.serialVersionUIDUnchanged</code>
<old>field io.jenkins.plugins.checks.steps.WithChecksStep.serialVersionUID</old>
<serialVersionUID>1</serialVersionUID>
<justification>Boolean value added, will have no impact on backwards compat.</justification>
</item>
<item>
<regex>true</regex>
<code>java.annotation.*</code>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
package io.jenkins.plugins.checks.status;

import static io.jenkins.plugins.checks.utils.FlowNodeUtils.getEnclosingBlockNames;
import static io.jenkins.plugins.checks.utils.FlowNodeUtils.getEnclosingStagesAndParallels;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.model.Result;
import hudson.model.Run;
import io.jenkins.plugins.checks.api.ChecksOutput;
import io.jenkins.plugins.checks.api.TruncatedString;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import org.apache.commons.collections.iterators.ReverseListIterator;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;

import org.jenkinsci.plugins.workflow.actions.ArgumentsAction;
import org.jenkinsci.plugins.workflow.actions.ErrorAction;
import org.jenkinsci.plugins.workflow.actions.LabelAction;
Expand All @@ -28,14 +30,7 @@
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.graph.BlockStartNode;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.graph.StepNode;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
import org.jenkinsci.plugins.workflow.support.visualization.table.FlowGraphTable;
import hudson.model.Result;
import hudson.model.Run;

import io.jenkins.plugins.checks.api.ChecksOutput;
import io.jenkins.plugins.checks.api.TruncatedString;

@SuppressWarnings("PMD.GodClass")
class FlowExecutionAnalyzer {
Expand Down Expand Up @@ -200,54 +195,6 @@ private String getPotentialTitle(final FlowNode flowNode, final ErrorAction erro
return StringUtils.join(new ReverseListIterator(enclosingBlockNames), "/") + ": " + whereBuildFailed;
}

private static boolean isStageNode(@NonNull final FlowNode node) {
if (node instanceof StepNode) {
StepDescriptor d = ((StepNode) node).getDescriptor();
return d != null && d.getFunctionName().equals("stage");
}
else {
return false;
}
}

/**
* Get the stage and parallel branch start node IDs (not the body nodes) for this node, innermost first.
* @param node A flownode.
* @return A nonnull, possibly empty list of stage/parallel branch start nodes, innermost first.
*/
@NonNull
private static List<FlowNode> getEnclosingStagesAndParallels(final FlowNode node) {
List<FlowNode> enclosingBlocks = new ArrayList<>();
for (FlowNode enclosing : node.getEnclosingBlocks()) {
if (enclosing != null && enclosing.getAction(LabelAction.class) != null
&& (isStageNode(enclosing) || enclosing.getAction(ThreadNameAction.class) != null)) {
enclosingBlocks.add(enclosing);
}
}

return enclosingBlocks;
}

@NonNull
private static List<String> getEnclosingBlockNames(@NonNull final List<FlowNode> nodes) {
List<String> names = new ArrayList<>();
for (FlowNode n : nodes) {
ThreadNameAction threadNameAction = n.getPersistentAction(ThreadNameAction.class);
LabelAction labelAction = n.getPersistentAction(LabelAction.class);
if (threadNameAction != null) {
// If we're on a parallel branch with the same name as the previous (inner) node, that generally
// means we're in a Declarative parallel stages situation, so don't add the redundant branch name.
if (names.isEmpty() || !threadNameAction.getThreadName().equals(names.get(names.size() - 1))) {
names.add(threadNameAction.getThreadName());
}
}
else if (labelAction != null) {
names.add(labelAction.getDisplayName());
}
}
return names;
}

@CheckForNull
private static String getLog(final FlowNode flowNode) {
LogAction logAction = flowNode.getAction(LogAction.class);
Expand Down
42 changes: 37 additions & 5 deletions src/main/java/io/jenkins/plugins/checks/steps/WithChecksStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
import hudson.model.Run;
import hudson.model.TaskListener;
import io.jenkins.plugins.checks.api.*;
import io.jenkins.plugins.checks.utils.FlowNodeUtils;
import io.jenkins.plugins.util.PluginLogger;
import jenkins.model.CauseOfInterruption;
import org.apache.commons.collections.iterators.ReverseListIterator;
import org.apache.commons.lang3.StringUtils;
import org.jenkinsci.plugins.displayurlapi.DisplayURLProvider;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.steps.*;
import org.kohsuke.stapler.DataBoundConstructor;

Expand All @@ -18,6 +22,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.kohsuke.stapler.DataBoundSetter;

import static hudson.Util.fixNull;

Expand All @@ -28,6 +33,7 @@ public class WithChecksStep extends Step implements Serializable {
private static final long serialVersionUID = 1L;

private final String name;
private boolean includeStage;

/**
* Creates the step with a name to inject.
Expand All @@ -45,6 +51,15 @@ public String getName() {
return name;
}

public boolean isIncludeStage() {
return includeStage;
}

@DataBoundSetter
public void setIncludeStage(boolean includeStage) {

Check warning on line 59 in src/main/java/io/jenkins/plugins/checks/steps/WithChecksStep.java

View check run for this annotation

ci.jenkins.io / PMD

MethodArgumentCouldBeFinal

NORMAL: Parameter 'includeStage' is not assigned and could be declared final.
Raw output
A method argument that is never re-assigned within the method can be declared final. <pre> <code> public void foo1 (String param) { // do stuff with param never assigning it } public void foo2 (final String param) { // better, do stuff with param never assigning it } </code> </pre> <a href="https://pmd.github.io/pmd-6.55.0/pmd_rules_java_codestyle.html#methodargumentcouldbefinal"> See PMD documentation. </a>
this.includeStage = includeStage;
}

@Override
public StepExecution start(final StepContext stepContext) {
return new WithChecksStepExecution(stepContext, this);
Expand Down Expand Up @@ -109,7 +124,7 @@ static class WithChecksStepExecution extends AbstractStepExecutionImpl {
}

@Override
public boolean start() {
public boolean start() throws IOException, InterruptedException {
ChecksInfo info = extractChecksInfo();
getContext().newBodyInvoker()
.withContext(info)
Expand All @@ -119,19 +134,36 @@ public boolean start() {
}

@VisibleForTesting
ChecksInfo extractChecksInfo() {
return new ChecksInfo(step.name);
ChecksInfo extractChecksInfo() throws IOException, InterruptedException {
return new ChecksInfo(getName());
}

private String getName() throws IOException, InterruptedException {
if (step.isIncludeStage()) {
FlowNode flowNode = getContext().get(FlowNode.class);
if (flowNode == null) {

Check warning on line 144 in src/main/java/io/jenkins/plugins/checks/steps/WithChecksStep.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 144 is only partially covered, one branch is missing
throw new IllegalArgumentException("No FlowNode found in the context.");

Check warning on line 145 in src/main/java/io/jenkins/plugins/checks/steps/WithChecksStep.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 145 is not covered by tests
}

List<FlowNode> enclosingStagesAndParallels = FlowNodeUtils.getEnclosingStagesAndParallels(flowNode);
List<String> checksComponents = FlowNodeUtils.getEnclosingBlockNames(enclosingStagesAndParallels);

checksComponents.add(step.getName());

return StringUtils.join(new ReverseListIterator(checksComponents), " / ");
}
return step.getName();
}

@Override
public void stop(final Throwable cause) {
try {
publish(getContext(), new ChecksDetails.ChecksDetailsBuilder()
.withName(step.getName())
.withName(getName())
.withStatus(ChecksStatus.COMPLETED)
.withConclusion(ChecksConclusion.CANCELED));
}
catch (WithChecksPublishException e) {
catch (WithChecksPublishException | IOException | InterruptedException e) {

Check warning on line 166 in src/main/java/io/jenkins/plugins/checks/steps/WithChecksStep.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 162-166 are not covered by tests
cause.addSuppressed(e);
}
getContext().onFailure(cause);
Expand Down
71 changes: 71 additions & 0 deletions src/main/java/io/jenkins/plugins/checks/utils/FlowNodeUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.jenkins.plugins.checks.utils;

import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.List;
import org.jenkinsci.plugins.workflow.actions.LabelAction;
import org.jenkinsci.plugins.workflow.actions.ThreadNameAction;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.graph.StepNode;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* Utility methods for working with FlowNodes.
*/
@Restricted(NoExternalUse.class)
public class FlowNodeUtils {

Check warning on line 18 in src/main/java/io/jenkins/plugins/checks/utils/FlowNodeUtils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 18 is not covered by tests
/**
* Get the stage and parallel branch start node IDs (not the body nodes) for this node, innermost first.
* @param node A flownode.
* @return A nonnull, possibly empty list of stage/parallel branch start nodes, innermost first.
*/
@NonNull
public static List<FlowNode> getEnclosingStagesAndParallels(final FlowNode node) {
List<FlowNode> enclosingBlocks = new ArrayList<>();
for (FlowNode enclosing : node.getEnclosingBlocks()) {
if (enclosing != null && enclosing.getAction(LabelAction.class) != null

Check warning on line 28 in src/main/java/io/jenkins/plugins/checks/utils/FlowNodeUtils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 28 is only partially covered, one branch is missing
&& (isStageNode(enclosing) || enclosing.getAction(ThreadNameAction.class) != null)) {

Check warning on line 29 in src/main/java/io/jenkins/plugins/checks/utils/FlowNodeUtils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 29 is only partially covered, one branch is missing
enclosingBlocks.add(enclosing);
}
}

return enclosingBlocks;
}

/**
* Get the stage and parallel branch names for these nodes, innermost first.
* @param nodes A flownode.
* @return A nonnull, possibly empty list of stage/parallel branch names, innermost first.
*/
@NonNull
public static List<String> getEnclosingBlockNames(@NonNull final List<FlowNode> nodes) {
List<String> names = new ArrayList<>();
for (FlowNode n : nodes) {
ThreadNameAction threadNameAction = n.getPersistentAction(ThreadNameAction.class);
LabelAction labelAction = n.getPersistentAction(LabelAction.class);
if (threadNameAction != null) {
// If we're on a parallel branch with the same name as the previous (inner) node, that generally
// means we're in a Declarative parallel stages situation, so don't add the redundant branch name.
if (names.isEmpty() || !threadNameAction.getThreadName().equals(names.get(names.size() - 1))) {

Check warning on line 51 in src/main/java/io/jenkins/plugins/checks/utils/FlowNodeUtils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 51 is only partially covered, 2 branches are missing
names.add(threadNameAction.getThreadName());
}
}
else if (labelAction != null) {

Check warning on line 55 in src/main/java/io/jenkins/plugins/checks/utils/FlowNodeUtils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 55 is only partially covered, one branch is missing
names.add(labelAction.getDisplayName());
}
}
return names;
}

private static boolean isStageNode(@NonNull final FlowNode node) {
if (node instanceof StepNode) {

Check warning on line 63 in src/main/java/io/jenkins/plugins/checks/utils/FlowNodeUtils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 63 is only partially covered, one branch is missing
StepDescriptor d = ((StepNode) node).getDescriptor();
return d != null && d.getFunctionName().equals("stage");

Check warning on line 65 in src/main/java/io/jenkins/plugins/checks/utils/FlowNodeUtils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 65 is only partially covered, one branch is missing
}
else {
return false;

Check warning on line 68 in src/main/java/io/jenkins/plugins/checks/utils/FlowNodeUtils.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 68 is not covered by tests
}
}
}

Check warning on line 71 in src/main/java/io/jenkins/plugins/checks/utils/FlowNodeUtils.java

View check run for this annotation

ci.jenkins.io / PMD

UseUtilityClass

NORMAL: All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning.
Raw output
For classes that only have static methods, consider making them utility classes. Note that this doesn't apply to abstract classes, since their subclasses may well include non-static methods. Also, if you want this class to be a utility class, remember to add a private constructor to prevent instantiation. (Note, that this use was known before PMD 5.1.0 as UseSingleton). <pre> <code> public class MaybeAUtility { public static void foo() {} public static void bar() {} } </code> </pre> <a href="https://pmd.github.io/pmd-6.55.0/pmd_rules_java_design.html#useutilityclass"> See PMD documentation. </a>
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,14 @@ public void shouldNotPublishStatusWhenSkipped() {
* a status checks using the specified name should be published.
*/
@Test
public void shouldPublishStatusWithProperties() {
public void shouldPublishStatusWithProperties() throws Exception {

Check warning on line 91 in src/test/java/io/jenkins/plugins/checks/status/BuildStatusChecksPublisherITest.java

View check run for this annotation

ci.jenkins.io / PMD

SignatureDeclareThrowsException

NORMAL: A method/constructor should not explicitly throw java.lang.Exception.
Raw output
A method/constructor shouldn't explicitly throw the generic java.lang.Exception, since it is unclear which exceptions that can be thrown from the methods. It might be difficult to document and understand such vague interfaces. Use either a class derived from RuntimeException or a checked exception. <pre> <code> public void foo() throws Exception { } </code> </pre> <a href="https://pmd.github.io/pmd-6.55.0/pmd_rules_java_design.html#signaturedeclarethrowsexception"> See PMD documentation. </a>
getProperties().setApplicable(true);
getProperties().setSkipped(false);
getProperties().setName("Test Status");

buildSuccessfully(createFreeStyleProject());

// Wait for the job to finish to work around slow Windows builds sometimes
this.getJenkins().waitUntilNoActivity();
assertThat(getFactory().getPublishedChecks()).hasSize(3);

ChecksDetails details = getFactory().getPublishedChecks().get(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ public void publishChecksShouldTakeNameFromWithChecks() {
assertThat(manualChecks.getConclusion()).isEqualTo(ChecksConclusion.SUCCESS);
}

@Test
public void publishChecksShouldIncludeEnclosingBlocksWhenEnabled() {
WorkflowJob job = createPipeline();
job.setDefinition(asStage("withChecks(name: 'tests', includeStage: true) {}"));

buildSuccessfully(job);

assertThat(getFactory().getPublishedChecks().size()).isEqualTo(1);
ChecksDetails autoChecks = getFactory().getPublishedChecks().get(0);

assertThat(autoChecks.getName()).isPresent().get().isEqualTo("tests / Integration Test");
assertThat(autoChecks.getStatus()).isEqualTo(ChecksStatus.IN_PROGRESS);
assertThat(autoChecks.getConclusion()).isEqualTo(ChecksConclusion.NONE);
}

/**
* Tests that withChecks step ignores names from the withChecks context if one has been explicitly set.
*/
Expand Down
Loading

0 comments on commit 74e1aa1

Please sign in to comment.