Skip to content

Commit e258678

Browse files
authored
Merge pull request #550 from XiongKezhi/publish-checks
[JENKINS-54072] Publish warnings in GitHub pull requests using Checks API
2 parents 37413d5 + ff27c99 commit e258678

18 files changed

Lines changed: 692 additions & 34 deletions

File tree

plugin/pom.xml

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,12 @@
8282
<form-element-path.version>1.8</form-element-path.version>
8383
<folder.version>6.9</folder.version>
8484
<scm-api.version>2.6.3</scm-api.version>
85+
<checks-api.version>0.1.0</checks-api.version>
8586

8687
<!-- Maven Surefire ArgLine -->
8788
<argLine>-Djava.awt.headless=true -Xmx1024m</argLine>
89+
90+
<useBeta>true</useBeta>
8891
</properties>
8992

9093
<licenses>
@@ -181,6 +184,11 @@
181184
<artifactId>json-smart</artifactId>
182185
<version>${json-smart.version}</version>
183186
</dependency>
187+
<dependency>
188+
<groupId>org.jsoup</groupId>
189+
<artifactId>jsoup</artifactId>
190+
<version>${jsoup.version}</version>
191+
</dependency>
184192

185193
<!-- PMD Messages -->
186194
<dependency>
@@ -285,6 +293,11 @@
285293
<artifactId>antisamy-markup-formatter</artifactId>
286294
<version>${antisamy-markup-formatter.version}</version>
287295
</dependency>
296+
<dependency>
297+
<groupId>io.jenkins.plugins</groupId>
298+
<artifactId>checks-api</artifactId>
299+
<version>${checks-api.version}</version>
300+
</dependency>
288301

289302
<!-- AxivionSuite Dependencies -->
290303
<dependency>
@@ -350,12 +363,6 @@
350363
<version>${json.version}</version>
351364
<scope>test</scope>
352365
</dependency>
353-
<dependency>
354-
<groupId>org.jsoup</groupId>
355-
<artifactId>jsoup</artifactId>
356-
<version>${jsoup.version}</version>
357-
<scope>test</scope>
358-
</dependency>
359366
<dependency>
360367
<groupId>org.jenkins-ci.main</groupId>
361368
<artifactId>jenkins-test-harness-tools</artifactId>
@@ -601,6 +608,12 @@
601608
<checkDependencies>false</checkDependencies>
602609
<analysisConfiguration>
603610
<revapi.ignore combine.children="append">
611+
<item>
612+
<regex>true</regex>
613+
<code>java.field.serialVersionUIDUnchanged</code>
614+
<classQualifiedName>io.jenkins.plugins.analysis.core.steps.*Step</classQualifiedName>
615+
<justification>Serialization is only used in interprocess communication using the same classes</justification>
616+
</item>
604617
<item>
605618
<regex>true</regex>
606619
<code>java.missing.*</code>

plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/IssuesRecorder.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
package io.jenkins.plugins.analysis.core.steps;
32

43
import java.io.IOException;
@@ -104,6 +103,8 @@ public class IssuesRecorder extends Recorder {
104103
private boolean isBlameDisabled;
105104
private boolean isForensicsDisabled;
106105

106+
private boolean skipPublishingChecks; // by default, checks will be published
107+
107108
private String id;
108109
private String name;
109110

@@ -362,6 +363,20 @@ public void setForensicsDisabled(final boolean forensicsDisabled) {
362363
isForensicsDisabled = forensicsDisabled;
363364
}
364365

366+
/**
367+
* Returns whether publishing checks should be skipped.
368+
*
369+
* @return {@code true} if publishing checks should be skipped, {@code false} otherwise
370+
*/
371+
public boolean isSkipPublishingChecks() {
372+
return skipPublishingChecks;
373+
}
374+
375+
@DataBoundSetter
376+
public void setSkipPublishingChecks(final boolean skipPublishingChecks) {
377+
this.skipPublishingChecks = skipPublishingChecks;
378+
}
379+
365380
/**
366381
* Determines whether to fail the build on errors during the step of recording issues.
367382
*
@@ -703,7 +718,12 @@ void publishResult(final Run<?, ?> run, final TaskListener listener, final Strin
703718
reportName, referenceJobName, referenceBuildId, ignoreQualityGate, ignoreFailedBuilds,
704719
getSourceCodeCharset(),
705720
new LogHandler(listener, loggerName, report.getReport()), statusHandler, failOnError);
706-
publisher.attachAction(trendChartType);
721+
ResultAction action = publisher.attachAction(trendChartType);
722+
723+
if (!skipPublishingChecks) {
724+
WarningChecksPublisher checksPublisher = new WarningChecksPublisher(action);
725+
checksPublisher.publishChecks();
726+
}
707727
}
708728

709729
/**

plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/PublishIssuesStep.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public class PublishIssuesStep extends Step implements Serializable {
5959
private String referenceBuildId = StringUtils.EMPTY;
6060
private boolean failOnError = false; // by default, it should not fail on error
6161

62+
private boolean skipPublishingChecks; // by default, warnings should be published to SCM platforms
63+
6264
private int healthy;
6365
private int unhealthy;
6466
private Severity minimumSeverity = Severity.WARNING_LOW;
@@ -147,6 +149,21 @@ public boolean getFailOnError() {
147149
return failOnError;
148150
}
149151

152+
/**
153+
* Returns whether publishing checks should be skipped.
154+
*
155+
* @return {@code true} if publishing checks should be skipped, {@code false} otherwise
156+
*/
157+
public boolean isSkipPublishingChecks() {
158+
return skipPublishingChecks;
159+
}
160+
161+
@DataBoundSetter
162+
@SuppressWarnings("unused") // Used by Stapler
163+
public void setSkipPublishingChecks(final boolean skipPublishingChecks) {
164+
this.skipPublishingChecks = skipPublishingChecks;
165+
}
166+
150167
/**
151168
* If {@code true}, then the result of the quality gate is ignored when selecting a reference build. This option is
152169
* disabled by default so a failing quality gate will be passed from build to build until the original reason for
@@ -811,7 +828,14 @@ protected ResultAction run() throws IOException, InterruptedException, IllegalSt
811828
StringUtils.defaultString(step.getName()), step.getReferenceJobName(), step.getReferenceBuildId(),
812829
step.getIgnoreQualityGate(), step.getIgnoreFailedBuilds(),
813830
getCharset(step.getSourceCodeEncoding()), getLogger(report), statusHandler, step.getFailOnError());
814-
return publisher.attachAction(step.getTrendChartType());
831+
ResultAction action = publisher.attachAction(step.getTrendChartType());
832+
833+
if (!step.isSkipPublishingChecks()) {
834+
WarningChecksPublisher checksPublisher = new WarningChecksPublisher(action);
835+
checksPublisher.publishChecks();
836+
}
837+
838+
return action;
815839
}
816840

817841
private LogHandler getLogger(final AnnotatedReport report) throws InterruptedException {

plugin/src/main/java/io/jenkins/plugins/analysis/core/steps/RecordIssuesStep.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ public class RecordIssuesStep extends Step implements Serializable {
8484
private boolean isBlameDisabled;
8585
private boolean isForensicsDisabled;
8686

87+
private boolean skipPublishingChecks; // by default, checks will be published
88+
8789
private String id;
8890
private String name;
8991

@@ -760,6 +762,20 @@ public void setForensicsDisabled(final boolean forensicsDisabled) {
760762
isForensicsDisabled = forensicsDisabled;
761763
}
762764

765+
/**
766+
* Returns whether publishing checks should be skipped.
767+
*
768+
* @return {@code true} if publishing checks should be skipped, {@code false} otherwise
769+
*/
770+
public boolean isSkipPublishingChecks() {
771+
return skipPublishingChecks;
772+
}
773+
774+
@DataBoundSetter
775+
public void setSkipPublishingChecks(final boolean skipPublishingChecks) {
776+
this.skipPublishingChecks = skipPublishingChecks;
777+
}
778+
763779
/**
764780
* Returns whether recording should be enabled for failed builds as well.
765781
*
@@ -993,6 +1009,7 @@ protected Void run() throws IOException, InterruptedException {
9931009
recorder.setAggregatingResults(step.getAggregatingResults());
9941010
recorder.setBlameDisabled(step.getBlameDisabled());
9951011
recorder.setForensicsDisabled(step.getForensicsDisabled());
1012+
recorder.setSkipPublishingChecks(step.isSkipPublishingChecks());
9961013
recorder.setId(step.getId());
9971014
recorder.setName(step.getName());
9981015
recorder.setQualityGates(step.getQualityGates());
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package io.jenkins.plugins.analysis.core.steps;
2+
3+
import java.util.HashSet;
4+
import java.util.List;
5+
import java.util.Set;
6+
import java.util.stream.Collectors;
7+
8+
import org.apache.commons.lang3.StringUtils;
9+
import org.jsoup.Jsoup;
10+
import org.jsoup.nodes.Element;
11+
import org.jsoup.nodes.TextNode;
12+
13+
import edu.hm.hafner.analysis.Report;
14+
import edu.hm.hafner.util.VisibleForTesting;
15+
16+
import io.jenkins.plugins.analysis.core.model.AnalysisResult;
17+
import io.jenkins.plugins.analysis.core.model.ResultAction;
18+
import io.jenkins.plugins.analysis.core.model.StaticAnalysisLabelProvider;
19+
import io.jenkins.plugins.analysis.core.util.IssuesStatistics;
20+
import io.jenkins.plugins.analysis.core.util.QualityGateStatus;
21+
import io.jenkins.plugins.checks.api.ChecksAnnotation;
22+
import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationBuilder;
23+
import io.jenkins.plugins.checks.api.ChecksAnnotation.ChecksAnnotationLevel;
24+
import io.jenkins.plugins.checks.api.ChecksConclusion;
25+
import io.jenkins.plugins.checks.api.ChecksDetails;
26+
import io.jenkins.plugins.checks.api.ChecksDetails.ChecksDetailsBuilder;
27+
import io.jenkins.plugins.checks.api.ChecksOutput.ChecksOutputBuilder;
28+
import io.jenkins.plugins.checks.api.ChecksPublisher;
29+
import io.jenkins.plugins.checks.api.ChecksPublisherFactory;
30+
import io.jenkins.plugins.checks.api.ChecksStatus;
31+
32+
/**
33+
* Publishes warnings as checks to scm platforms.
34+
*
35+
* @author Kezhi Xiong
36+
*/
37+
class WarningChecksPublisher {
38+
private final ResultAction action;
39+
40+
WarningChecksPublisher(final ResultAction action) {
41+
this.action = action;
42+
}
43+
44+
/**
45+
* Publishes checks to platforms. Afterwards, all warnings are available in corresponding platform's UI, e.g. GitHub
46+
* checks.
47+
*/
48+
void publishChecks() {
49+
ChecksPublisher publisher = ChecksPublisherFactory.fromRun(action.getOwner());
50+
publisher.publish(extractChecksDetails());
51+
}
52+
53+
@VisibleForTesting
54+
ChecksDetails extractChecksDetails() {
55+
AnalysisResult result = action.getResult();
56+
IssuesStatistics totals = result.getTotals();
57+
58+
StaticAnalysisLabelProvider labelProvider = action.getLabelProvider();
59+
60+
return new ChecksDetailsBuilder()
61+
.withName(labelProvider.getName())
62+
.withStatus(ChecksStatus.COMPLETED)
63+
.withConclusion(extractChecksConclusion(result.getQualityGateStatus()))
64+
.withOutput(new ChecksOutputBuilder()
65+
.withTitle(extractChecksTitle(totals))
66+
.withSummary(extractChecksSummary(totals))
67+
.withText(extractChecksText(totals))
68+
.withAnnotations(extractChecksAnnotations(result.getNewIssues(), labelProvider))
69+
.build())
70+
.withDetailsURL(action.getAbsoluteUrl())
71+
.build();
72+
}
73+
74+
private String extractChecksTitle(final IssuesStatistics statistics) {
75+
if (statistics.getTotalSize() == 0) {
76+
return "No issues.";
77+
}
78+
else if (statistics.getNewSize() == 0) {
79+
return String.format("No new issues, %d total.", statistics.getTotalSize());
80+
}
81+
else if (statistics.getNewSize() == statistics.getTotalSize()) {
82+
if (statistics.getNewSize() == 1) {
83+
return "1 new issue.";
84+
}
85+
return String.format("%d new issues.", statistics.getNewSize());
86+
}
87+
else {
88+
if (statistics.getNewSize() == 1) {
89+
return String.format("1 new issue, %d total.", statistics.getTotalSize());
90+
}
91+
return String.format("%d new issues, %d total.", statistics.getNewSize(), statistics.getTotalSize());
92+
}
93+
}
94+
95+
private String extractChecksSummary(final IssuesStatistics statistics) {
96+
return String.format("## %d issues in total:\n"
97+
+ "- ### %d new issues\n"
98+
+ "- ### %d outstanding issues\n"
99+
+ "- ### %d delta issues\n"
100+
+ "- ### %d fixed issues",
101+
statistics.getTotalSize(), statistics.getNewSize(), statistics.getTotalSize() - statistics.getNewSize(),
102+
statistics.getDeltaSize(), statistics.getFixedSize());
103+
}
104+
105+
private String extractChecksText(final IssuesStatistics statistics) {
106+
return "## Total Issue Statistics:\n"
107+
+ generateSeverityText(statistics.getTotalLowSize(), statistics.getTotalNormalSize(),
108+
statistics.getTotalHighSize(), statistics.getTotalErrorSize())
109+
+ "## New Issue Statistics:\n"
110+
+ generateSeverityText(statistics.getNewLowSize(), statistics.getNewNormalSize(),
111+
statistics.getNewHighSize(), statistics.getNewErrorSize())
112+
+ "## Delta Issue Statistics:\n"
113+
+ generateSeverityText(statistics.getDeltaLowSize(), statistics.getDeltaNormalSize(),
114+
statistics.getDeltaHighSize(), statistics.getDeltaErrorSize());
115+
}
116+
117+
private String generateSeverityText(final int low, final int normal, final int high, final int error) {
118+
return "* Error: " + error + "\n"
119+
+ "* High: " + high + "\n"
120+
+ "* Normal: " + normal + "\n"
121+
+ "* Low: " + low + "\n";
122+
}
123+
124+
private ChecksConclusion extractChecksConclusion(final QualityGateStatus status) {
125+
switch (status) {
126+
case INACTIVE:
127+
case PASSED:
128+
return ChecksConclusion.SUCCESS;
129+
case FAILED:
130+
case WARNING:
131+
return ChecksConclusion.FAILURE;
132+
default:
133+
throw new IllegalArgumentException("Unsupported quality gate status: " + status);
134+
}
135+
}
136+
137+
private List<ChecksAnnotation> extractChecksAnnotations(final Report issues,
138+
final StaticAnalysisLabelProvider labelProvider) {
139+
return issues.stream()
140+
.map(issue -> new ChecksAnnotationBuilder()
141+
.withPath(issue.getFileName())
142+
.withTitle(issue.getType())
143+
.withAnnotationLevel(ChecksAnnotationLevel.WARNING)
144+
.withMessage(issue.getSeverity() + ":\n" + parseHtml(issue.getMessage()))
145+
.withStartLine(issue.getLineStart())
146+
.withEndLine(issue.getLineEnd())
147+
.withStartColumn(issue.getColumnStart())
148+
.withEndColumn(issue.getColumnEnd())
149+
.withRawDetails(StringUtils.normalizeSpace(labelProvider.getDescription(issue)))
150+
.build())
151+
.collect(Collectors.toList());
152+
}
153+
154+
private String parseHtml(final String html) {
155+
Set<String> contents = new HashSet<>();
156+
parseHtml(Jsoup.parse(html), contents);
157+
return String.join("\n", contents);
158+
}
159+
160+
private void parseHtml(final Element html, final Set<String> contents) {
161+
for (TextNode node : html.textNodes()) {
162+
contents.add(node.text().trim());
163+
}
164+
165+
for (Element child : html.children()) {
166+
if (child.hasAttr("href")) {
167+
contents.add(child.text().trim() + ":" + child.attr("href").trim());
168+
}
169+
else {
170+
parseHtml(child, contents);
171+
}
172+
}
173+
}
174+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<div>
2+
If this option is unchecked, then the plugin automatically publishes the issues to corresponding SCM hosting platforms.
3+
For example, if you are using this feature for a GitHub organization project, the warnings will be published to
4+
GitHub through the Checks API. If this operation slows down your build or you don't want to publish the warnings to
5+
SCM platforms, you can use this option to deactivate this feature.
6+
</div>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<div>
2+
If this option is unchecked, then the plugin automatically publishes the issues to corresponding SCM hosting platforms.
3+
For example, if you are using this feature for a GitHub organization project, the warnings will be published to
4+
GitHub through the Checks API. If this operation slows down your build or you don't want to publish the warnings to
5+
SCM platforms, you can use this option to deactivate this feature.
6+
</div>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<div>
2+
If this option is unchecked, then the plugin automatically publishes the issues to corresponding SCM hosting platforms.
3+
For example, if you are using this feature for a GitHub organization project, the warnings will be published to
4+
GitHub through the Checks API. If this operation slows down your build or you don't want to publish the warnings to
5+
SCM platforms, you can use this option to deactivate this feature.
6+
</div>

0 commit comments

Comments
 (0)