Skip to content

Commit

Permalink
Merge pull request #550 from XiongKezhi/publish-checks
Browse files Browse the repository at this point in the history
[JENKINS-54072] Publish warnings in GitHub pull requests using Checks API
  • Loading branch information
uhafner authored Jul 28, 2020
2 parents 37413d5 + ff27c99 commit e258678
Show file tree
Hide file tree
Showing 18 changed files with 692 additions and 34 deletions.
25 changes: 19 additions & 6 deletions plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@
<form-element-path.version>1.8</form-element-path.version>
<folder.version>6.9</folder.version>
<scm-api.version>2.6.3</scm-api.version>
<checks-api.version>0.1.0</checks-api.version>

<!-- Maven Surefire ArgLine -->
<argLine>-Djava.awt.headless=true -Xmx1024m</argLine>

<useBeta>true</useBeta>
</properties>

<licenses>
Expand Down Expand Up @@ -181,6 +184,11 @@
<artifactId>json-smart</artifactId>
<version>${json-smart.version}</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
</dependency>

<!-- PMD Messages -->
<dependency>
Expand Down Expand Up @@ -285,6 +293,11 @@
<artifactId>antisamy-markup-formatter</artifactId>
<version>${antisamy-markup-formatter.version}</version>
</dependency>
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>checks-api</artifactId>
<version>${checks-api.version}</version>
</dependency>

<!-- AxivionSuite Dependencies -->
<dependency>
Expand Down Expand Up @@ -350,12 +363,6 @@
<version>${json.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.main</groupId>
<artifactId>jenkins-test-harness-tools</artifactId>
Expand Down Expand Up @@ -601,6 +608,12 @@
<checkDependencies>false</checkDependencies>
<analysisConfiguration>
<revapi.ignore combine.children="append">
<item>
<regex>true</regex>
<code>java.field.serialVersionUIDUnchanged</code>
<classQualifiedName>io.jenkins.plugins.analysis.core.steps.*Step</classQualifiedName>
<justification>Serialization is only used in interprocess communication using the same classes</justification>
</item>
<item>
<regex>true</regex>
<code>java.missing.*</code>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package io.jenkins.plugins.analysis.core.steps;

import java.io.IOException;
Expand Down Expand Up @@ -104,6 +103,8 @@ public class IssuesRecorder extends Recorder {
private boolean isBlameDisabled;
private boolean isForensicsDisabled;

private boolean skipPublishingChecks; // by default, checks will be published

private String id;
private String name;

Expand Down Expand Up @@ -362,6 +363,20 @@ public void setForensicsDisabled(final boolean forensicsDisabled) {
isForensicsDisabled = forensicsDisabled;
}

/**
* Returns whether publishing checks should be skipped.
*
* @return {@code true} if publishing checks should be skipped, {@code false} otherwise
*/
public boolean isSkipPublishingChecks() {
return skipPublishingChecks;
}

@DataBoundSetter
public void setSkipPublishingChecks(final boolean skipPublishingChecks) {
this.skipPublishingChecks = skipPublishingChecks;
}

/**
* Determines whether to fail the build on errors during the step of recording issues.
*
Expand Down Expand Up @@ -703,7 +718,12 @@ void publishResult(final Run<?, ?> run, final TaskListener listener, final Strin
reportName, referenceJobName, referenceBuildId, ignoreQualityGate, ignoreFailedBuilds,
getSourceCodeCharset(),
new LogHandler(listener, loggerName, report.getReport()), statusHandler, failOnError);
publisher.attachAction(trendChartType);
ResultAction action = publisher.attachAction(trendChartType);

if (!skipPublishingChecks) {
WarningChecksPublisher checksPublisher = new WarningChecksPublisher(action);
checksPublisher.publishChecks();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ public class PublishIssuesStep extends Step implements Serializable {
private String referenceBuildId = StringUtils.EMPTY;
private boolean failOnError = false; // by default, it should not fail on error

private boolean skipPublishingChecks; // by default, warnings should be published to SCM platforms

private int healthy;
private int unhealthy;
private Severity minimumSeverity = Severity.WARNING_LOW;
Expand Down Expand Up @@ -147,6 +149,21 @@ public boolean getFailOnError() {
return failOnError;
}

/**
* Returns whether publishing checks should be skipped.
*
* @return {@code true} if publishing checks should be skipped, {@code false} otherwise
*/
public boolean isSkipPublishingChecks() {
return skipPublishingChecks;
}

@DataBoundSetter
@SuppressWarnings("unused") // Used by Stapler
public void setSkipPublishingChecks(final boolean skipPublishingChecks) {
this.skipPublishingChecks = skipPublishingChecks;
}

/**
* If {@code true}, then the result of the quality gate is ignored when selecting a reference build. This option is
* disabled by default so a failing quality gate will be passed from build to build until the original reason for
Expand Down Expand Up @@ -811,7 +828,14 @@ protected ResultAction run() throws IOException, InterruptedException, IllegalSt
StringUtils.defaultString(step.getName()), step.getReferenceJobName(), step.getReferenceBuildId(),
step.getIgnoreQualityGate(), step.getIgnoreFailedBuilds(),
getCharset(step.getSourceCodeEncoding()), getLogger(report), statusHandler, step.getFailOnError());
return publisher.attachAction(step.getTrendChartType());
ResultAction action = publisher.attachAction(step.getTrendChartType());

if (!step.isSkipPublishingChecks()) {
WarningChecksPublisher checksPublisher = new WarningChecksPublisher(action);
checksPublisher.publishChecks();
}

return action;
}

private LogHandler getLogger(final AnnotatedReport report) throws InterruptedException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ public class RecordIssuesStep extends Step implements Serializable {
private boolean isBlameDisabled;
private boolean isForensicsDisabled;

private boolean skipPublishingChecks; // by default, checks will be published

private String id;
private String name;

Expand Down Expand Up @@ -760,6 +762,20 @@ public void setForensicsDisabled(final boolean forensicsDisabled) {
isForensicsDisabled = forensicsDisabled;
}

/**
* Returns whether publishing checks should be skipped.
*
* @return {@code true} if publishing checks should be skipped, {@code false} otherwise
*/
public boolean isSkipPublishingChecks() {
return skipPublishingChecks;
}

@DataBoundSetter
public void setSkipPublishingChecks(final boolean skipPublishingChecks) {
this.skipPublishingChecks = skipPublishingChecks;
}

/**
* Returns whether recording should be enabled for failed builds as well.
*
Expand Down Expand Up @@ -993,6 +1009,7 @@ protected Void run() throws IOException, InterruptedException {
recorder.setAggregatingResults(step.getAggregatingResults());
recorder.setBlameDisabled(step.getBlameDisabled());
recorder.setForensicsDisabled(step.getForensicsDisabled());
recorder.setSkipPublishingChecks(step.isSkipPublishingChecks());
recorder.setId(step.getId());
recorder.setName(step.getName());
recorder.setQualityGates(step.getQualityGates());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package io.jenkins.plugins.analysis.core.steps;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.TextNode;

import edu.hm.hafner.analysis.Report;
import edu.hm.hafner.util.VisibleForTesting;

import io.jenkins.plugins.analysis.core.model.AnalysisResult;
import io.jenkins.plugins.analysis.core.model.ResultAction;
import io.jenkins.plugins.analysis.core.model.StaticAnalysisLabelProvider;
import io.jenkins.plugins.analysis.core.util.IssuesStatistics;
import io.jenkins.plugins.analysis.core.util.QualityGateStatus;
import io.jenkins.plugins.checks.api.ChecksAnnotation;
import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationBuilder;
import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationLevel;
import io.jenkins.plugins.checks.api.ChecksConclusion;
import io.jenkins.plugins.checks.api.ChecksDetails;
import io.jenkins.plugins.checks.api.ChecksDetails.ChecksDetailsBuilder;
import io.jenkins.plugins.checks.api.ChecksOutput.ChecksOutputBuilder;
import io.jenkins.plugins.checks.api.ChecksPublisher;
import io.jenkins.plugins.checks.api.ChecksPublisherFactory;
import io.jenkins.plugins.checks.api.ChecksStatus;

/**
* Publishes warnings as checks to scm platforms.
*
* @author Kezhi Xiong
*/
class WarningChecksPublisher {
private final ResultAction action;

WarningChecksPublisher(final ResultAction action) {
this.action = action;
}

/**
* Publishes checks to platforms. Afterwards, all warnings are available in corresponding platform's UI, e.g. GitHub
* checks.
*/
void publishChecks() {
ChecksPublisher publisher = ChecksPublisherFactory.fromRun(action.getOwner());
publisher.publish(extractChecksDetails());
}

@VisibleForTesting
ChecksDetails extractChecksDetails() {
AnalysisResult result = action.getResult();
IssuesStatistics totals = result.getTotals();

StaticAnalysisLabelProvider labelProvider = action.getLabelProvider();

return new ChecksDetailsBuilder()
.withName(labelProvider.getName())
.withStatus(ChecksStatus.COMPLETED)
.withConclusion(extractChecksConclusion(result.getQualityGateStatus()))
.withOutput(new ChecksOutputBuilder()
.withTitle(extractChecksTitle(totals))
.withSummary(extractChecksSummary(totals))
.withText(extractChecksText(totals))
.withAnnotations(extractChecksAnnotations(result.getNewIssues(), labelProvider))
.build())
.withDetailsURL(action.getAbsoluteUrl())
.build();
}

private String extractChecksTitle(final IssuesStatistics statistics) {
if (statistics.getTotalSize() == 0) {
return "No issues.";
}
else if (statistics.getNewSize() == 0) {
return String.format("No new issues, %d total.", statistics.getTotalSize());
}
else if (statistics.getNewSize() == statistics.getTotalSize()) {
if (statistics.getNewSize() == 1) {
return "1 new issue.";
}
return String.format("%d new issues.", statistics.getNewSize());
}
else {
if (statistics.getNewSize() == 1) {
return String.format("1 new issue, %d total.", statistics.getTotalSize());
}
return String.format("%d new issues, %d total.", statistics.getNewSize(), statistics.getTotalSize());
}
}

private String extractChecksSummary(final IssuesStatistics statistics) {
return String.format("## %d issues in total:\n"
+ "- ### %d new issues\n"
+ "- ### %d outstanding issues\n"
+ "- ### %d delta issues\n"
+ "- ### %d fixed issues",
statistics.getTotalSize(), statistics.getNewSize(), statistics.getTotalSize() - statistics.getNewSize(),
statistics.getDeltaSize(), statistics.getFixedSize());
}

private String extractChecksText(final IssuesStatistics statistics) {
return "## Total Issue Statistics:\n"
+ generateSeverityText(statistics.getTotalLowSize(), statistics.getTotalNormalSize(),
statistics.getTotalHighSize(), statistics.getTotalErrorSize())
+ "## New Issue Statistics:\n"
+ generateSeverityText(statistics.getNewLowSize(), statistics.getNewNormalSize(),
statistics.getNewHighSize(), statistics.getNewErrorSize())
+ "## Delta Issue Statistics:\n"
+ generateSeverityText(statistics.getDeltaLowSize(), statistics.getDeltaNormalSize(),
statistics.getDeltaHighSize(), statistics.getDeltaErrorSize());
}

private String generateSeverityText(final int low, final int normal, final int high, final int error) {
return "* Error: " + error + "\n"
+ "* High: " + high + "\n"
+ "* Normal: " + normal + "\n"
+ "* Low: " + low + "\n";
}

private ChecksConclusion extractChecksConclusion(final QualityGateStatus status) {
switch (status) {
case INACTIVE:
case PASSED:
return ChecksConclusion.SUCCESS;
case FAILED:
case WARNING:
return ChecksConclusion.FAILURE;
default:
throw new IllegalArgumentException("Unsupported quality gate status: " + status);
}
}

private List<ChecksAnnotation> extractChecksAnnotations(final Report issues,
final StaticAnalysisLabelProvider labelProvider) {
return issues.stream()
.map(issue -> new ChecksAnnotationBuilder()
.withPath(issue.getFileName())
.withTitle(issue.getType())
.withAnnotationLevel(ChecksAnnotationLevel.WARNING)
.withMessage(issue.getSeverity() + ":\n" + parseHtml(issue.getMessage()))
.withStartLine(issue.getLineStart())
.withEndLine(issue.getLineEnd())
.withStartColumn(issue.getColumnStart())
.withEndColumn(issue.getColumnEnd())
.withRawDetails(StringUtils.normalizeSpace(labelProvider.getDescription(issue)))
.build())
.collect(Collectors.toList());
}

private String parseHtml(final String html) {
Set<String> contents = new HashSet<>();
parseHtml(Jsoup.parse(html), contents);
return String.join("\n", contents);
}

private void parseHtml(final Element html, final Set<String> contents) {
for (TextNode node : html.textNodes()) {
contents.add(node.text().trim());
}

for (Element child : html.children()) {
if (child.hasAttr("href")) {
contents.add(child.text().trim() + ":" + child.attr("href").trim());
}
else {
parseHtml(child, contents);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
If this option is unchecked, then the plugin automatically publishes the issues to corresponding SCM hosting platforms.
For example, if you are using this feature for a GitHub organization project, the warnings will be published to
GitHub through the Checks API. If this operation slows down your build or you don't want to publish the warnings to
SCM platforms, you can use this option to deactivate this feature.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
If this option is unchecked, then the plugin automatically publishes the issues to corresponding SCM hosting platforms.
For example, if you are using this feature for a GitHub organization project, the warnings will be published to
GitHub through the Checks API. If this operation slows down your build or you don't want to publish the warnings to
SCM platforms, you can use this option to deactivate this feature.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
If this option is unchecked, then the plugin automatically publishes the issues to corresponding SCM hosting platforms.
For example, if you are using this feature for a GitHub organization project, the warnings will be published to
GitHub through the Checks API. If this operation slows down your build or you don't want to publish the warnings to
SCM platforms, you can use this option to deactivate this feature.
</div>
Loading

0 comments on commit e258678

Please sign in to comment.