Skip to content

Commit 1adb3c8

Browse files
committed
Attach Develocity build scans to GitHub Checks
1 parent 01c2dd5 commit 1adb3c8

File tree

8 files changed

+4524
-4
lines changed

8 files changed

+4524
-4
lines changed

pom.xml

+10-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
3636
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
3737
<quarkus-github-app.version>2.3.3</quarkus-github-app.version>
38+
<quarkus-openapi-generator.version>2.4.1</quarkus-openapi-generator.version>
3839
<!-- Using a single property for both plugin and platform, so that GitHub's Dependabot doesn't get confused -->
3940
<quarkus.version>3.8.3</quarkus.version>
4041
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
@@ -91,6 +92,15 @@
9192
<groupId>io.quarkus</groupId>
9293
<artifactId>quarkus-cache</artifactId>
9394
</dependency>
95+
<dependency>
96+
<groupId>io.quarkiverse.openapi.generator</groupId>
97+
<artifactId>quarkus-openapi-generator</artifactId>
98+
<version>${quarkus-openapi-generator.version}</version>
99+
</dependency>
100+
<dependency>
101+
<groupId>io.quarkus</groupId>
102+
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
103+
</dependency>
94104
<dependency>
95105
<groupId>com.hrakaroo</groupId>
96106
<artifactId>glob</artifactId>
@@ -115,8 +125,6 @@
115125
<artifactId>quarkus-maven-plugin</artifactId>
116126
<version>${quarkus.version}</version>
117127
<extensions>true</extensions>
118-
<configuration>
119-
</configuration>
120128
<executions>
121129
<execution>
122130
<goals>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package org.hibernate.infra.bot;
2+
3+
import java.io.IOException;
4+
import java.util.Comparator;
5+
import java.util.List;
6+
import java.util.function.Predicate;
7+
8+
import jakarta.inject.Inject;
9+
10+
import org.hibernate.infra.bot.config.DeploymentConfig;
11+
import org.hibernate.infra.bot.config.RepositoryConfig;
12+
import org.hibernate.infra.bot.develocity.DevelocityCIBuildScan;
13+
14+
import com.gradle.develocity.api.BuildsApi;
15+
import com.gradle.develocity.model.BuildAttributesValue;
16+
import com.gradle.develocity.model.BuildModelName;
17+
import com.gradle.develocity.model.BuildsQuery;
18+
19+
import io.quarkiverse.githubapp.ConfigFile;
20+
import io.quarkiverse.githubapp.event.CheckRun;
21+
import io.quarkus.logging.Log;
22+
import org.apache.commons.lang3.exception.ExceptionUtils;
23+
import org.eclipse.microprofile.rest.client.inject.RestClient;
24+
import org.kohsuke.github.GHCheckRun;
25+
import org.kohsuke.github.GHCheckRunBuilder;
26+
import org.kohsuke.github.GHEventPayload;
27+
import org.kohsuke.github.GHRepository;
28+
29+
public class ExtractDevelocityBuildScans {
30+
private static final String DEVELOCITY_CHECK_RUN_NAME = "Develocity Build Scans";
31+
32+
@Inject
33+
DeploymentConfig deploymentConfig;
34+
35+
@RestClient
36+
BuildsApi develocityBuildsApi;
37+
38+
void checkRunRerequested(@CheckRun.Rerequested GHEventPayload.CheckRun payload,
39+
@ConfigFile("hibernate-github-bot.yml") RepositoryConfig repositoryConfig) {
40+
var repository = payload.getRepository();
41+
var checkRun = payload.getCheckRun();
42+
if ( !DEVELOCITY_CHECK_RUN_NAME.equals( checkRun.getName() ) ) {
43+
return;
44+
}
45+
String sha = checkRun.getHeadSha();
46+
extractCIBuildScans( repository, sha );
47+
}
48+
49+
void checkRunCompleted(@CheckRun.Completed GHEventPayload.CheckRun payload,
50+
@ConfigFile("hibernate-github-bot.yml") RepositoryConfig repositoryConfig) {
51+
if ( repositoryConfig == null
52+
|| repositoryConfig.develocity == null
53+
|| repositoryConfig.develocity.buildScan == null
54+
|| !repositoryConfig.develocity.buildScan.extract ) {
55+
return;
56+
}
57+
var repository = payload.getRepository();
58+
var checkRun = payload.getCheckRun();
59+
if ( checkRun.getApp().getId() != deploymentConfig.jenkins().githubAppId()
60+
&& !"github-actions".equals( checkRun.getApp().getSlug() ) ) {
61+
return;
62+
}
63+
String sha = checkRun.getHeadSha();
64+
extractCIBuildScans( repository, sha );
65+
}
66+
67+
private void extractCIBuildScans(GHRepository repository, String sha) {
68+
try {
69+
long checkId = createDevelocityCheck( repository, sha );
70+
Throwable failure = null;
71+
List<DevelocityCIBuildScan> buildScans = null;
72+
try {
73+
buildScans = findCIBuildScans( sha );
74+
}
75+
catch (RuntimeException e) {
76+
failure = e;
77+
}
78+
if ( failure != null ) {
79+
Log.errorf( failure, "Failed to extract all build scans from commit %s" + sha );
80+
}
81+
updateDevelocityCheck( repository, checkId, buildScans, failure );
82+
}
83+
catch (IOException | RuntimeException e) {
84+
Log.errorf( e, "Failed to report build scans from commit %s" + sha );
85+
}
86+
}
87+
88+
private List<DevelocityCIBuildScan> findCIBuildScans(String sha) {
89+
return develocityBuildsApi.getBuilds( new BuildsQuery.BuildsQueryQueryParam()
90+
.fromInstant( 0L )
91+
.query( "tag:CI value:\"Git commit id=%s\"".formatted( sha ) )
92+
.models( List.of( BuildModelName.GRADLE_MINUS_ATTRIBUTES, BuildModelName.MAVEN_MINUS_ATTRIBUTES ) ) )
93+
.stream()
94+
.map( build -> {
95+
List<BuildAttributesValue> customValues;
96+
List<String> tags;
97+
List<String> goals;
98+
boolean hasFailed;
99+
Boolean hasVerificationFailure;
100+
var maven = build.getModels().getMavenAttributes();
101+
if ( maven != null ) {
102+
var model = maven.getModel();
103+
tags = model.getTags();
104+
customValues = model.getValues();
105+
goals = model.getRequestedGoals();
106+
hasFailed = model.getHasFailed();
107+
hasVerificationFailure = model.getHasVerificationFailure();
108+
}
109+
else {
110+
var model = build.getModels().getGradleAttributes().getModel();
111+
tags = model.getTags();
112+
customValues = model.getValues();
113+
goals = model.getRequestedTasks();
114+
hasFailed = model.getHasFailed();
115+
hasVerificationFailure = model.getHasVerificationFailure();
116+
}
117+
String provider = "";
118+
String jobOrWorkflow = "";
119+
String stage = "";
120+
for ( BuildAttributesValue customValue : customValues ) {
121+
if ( customValue.getName().equals( "CI provider" ) ) {
122+
provider = customValue.getValue();
123+
}
124+
else if ( customValue.getName().equals( "CI job" )
125+
|| customValue.getName().equals( "CI workflow" ) ) {
126+
jobOrWorkflow = customValue.getValue();
127+
}
128+
else if ( customValue.getName().equals( "CI stage" ) ) {
129+
stage = customValue.getValue();
130+
}
131+
}
132+
tags = tags.stream()
133+
.filter( Predicate.not( Predicate.isEqual( "CI" ) ) )
134+
.sorted()
135+
.toList();
136+
return new DevelocityCIBuildScan(
137+
provider,
138+
jobOrWorkflow,
139+
stage,
140+
build.getAvailableAt(),
141+
tags,
142+
goals,
143+
hasFailed ? DevelocityCIBuildScan.Status.FAILURE : DevelocityCIBuildScan.Status.SUCCESS,
144+
hasVerificationFailure != null && !hasVerificationFailure
145+
? DevelocityCIBuildScan.Status.FAILURE
146+
: DevelocityCIBuildScan.Status.SUCCESS,
147+
deploymentConfig.develocity().uri().resolve( "/s/" + build.getId() ),
148+
deploymentConfig.develocity().uri().resolve( "/s/" + build.getId() + "/tests" ),
149+
deploymentConfig.develocity().uri().resolve( "/s/" + build.getId() + "/console-log" )
150+
);
151+
} )
152+
.sorted( Comparator.comparing( DevelocityCIBuildScan::provider )
153+
.thenComparing( DevelocityCIBuildScan::jobOrWorkflow )
154+
.thenComparing( DevelocityCIBuildScan::stage )
155+
.thenComparing( DevelocityCIBuildScan::availableAt ) )
156+
.toList();
157+
}
158+
159+
private long createDevelocityCheck(GHRepository repository, String sha) throws IOException {
160+
return repository.createCheckRun( DEVELOCITY_CHECK_RUN_NAME, sha )
161+
.withStatus( GHCheckRun.Status.IN_PROGRESS )
162+
.create()
163+
.getId();
164+
}
165+
166+
private void updateDevelocityCheck(GHRepository repository, long checkId,
167+
List<DevelocityCIBuildScan> buildScans, Throwable failure)
168+
throws IOException {
169+
String formattedBuildScanList = "";
170+
try {
171+
formattedBuildScanList = formatBuildScanList( buildScans );
172+
}
173+
catch (RuntimeException e) {
174+
if ( failure == null ) {
175+
failure = e;
176+
}
177+
else {
178+
failure.addSuppressed( e );
179+
}
180+
}
181+
182+
GHCheckRun.Conclusion conclusion;
183+
String title;
184+
String text;
185+
if ( failure == null ) {
186+
conclusion = GHCheckRun.Conclusion.NEUTRAL;
187+
title = "Found %s build scan%s".formatted( buildScans.size(), buildScans.size() != 1 ? "s" : "" );
188+
text = formattedBuildScanList;
189+
}
190+
else {
191+
conclusion = GHCheckRun.Conclusion.FAILURE;
192+
title = "Develocity Build Scans extraction failed with exception";
193+
text = formattedBuildScanList + "\n\n```\n" + ExceptionUtils.getStackTrace( failure ) + "\n```";
194+
}
195+
196+
if ( deploymentConfig.isDryRun() ) {
197+
Log.infof( "SHA %s - Update check run '%s' with conclusion '%s' and text:\n%s",
198+
checkId, DEVELOCITY_CHECK_RUN_NAME,
199+
failure == null ? GHCheckRun.Conclusion.NEUTRAL : GHCheckRun.Conclusion.FAILURE );
200+
return;
201+
}
202+
repository.updateCheckRun( checkId )
203+
.withStatus( GHCheckRun.Status.COMPLETED )
204+
.withConclusion( conclusion )
205+
.add( new GHCheckRunBuilder.Output(
206+
title,
207+
text
208+
) )
209+
.create();
210+
}
211+
212+
private String formatBuildScanList(List<DevelocityCIBuildScan> buildScans) {
213+
if ( buildScans == null || buildScans.isEmpty() ) {
214+
return "No build scan found for this CI run.";
215+
}
216+
217+
StringBuilder summary = new StringBuilder();
218+
summary.append( "\n\n| Status | Job/Workflow | Tags | Goals | Build Scan | Tests | Logs |\n" );
219+
summary.append( "| --- | --- | --- | --- | --- | --- | --- |\n" );
220+
for ( DevelocityCIBuildScan buildScan : buildScans ) {
221+
summary.append(
222+
"| %s | `%s` | `%s` | `%s` | [:mag:](%s) | [%s](%s) | [:page_with_curl:](%s) |\n"
223+
.formatted(
224+
statusToEmoji( buildScan.status() ),
225+
String.join( " ", buildScan.jobOrWorkflow(), buildScan.stage() ),
226+
String.join( "` `", buildScan.tags() ),
227+
String.join( " ", buildScan.goals() ),
228+
buildScan.buildScan(),
229+
statusToEmoji( buildScan.testStatus() ),
230+
buildScan.tests(),
231+
buildScan.logs()
232+
) );
233+
}
234+
return summary.toString();
235+
}
236+
237+
private String statusToEmoji(DevelocityCIBuildScan.Status status) {
238+
return switch ( status ) {
239+
case SUCCESS -> ":heavy_check_mark:";
240+
case FAILURE -> ":x:";
241+
};
242+
}
243+
}

src/main/java/org/hibernate/infra/bot/config/DeploymentConfig.java

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.hibernate.infra.bot.config;
22

3+
import java.net.URI;
34
import java.util.Optional;
45

56
import io.smallrye.config.ConfigMapping;
@@ -9,8 +10,21 @@ public interface DeploymentConfig {
910

1011
Optional<Boolean> dryRun();
1112

13+
Develocity develocity();
14+
15+
Jenkins jenkins();
16+
1217
default boolean isDryRun() {
1318
Optional<Boolean> dryRun = dryRun();
1419
return dryRun.isPresent() && dryRun.get();
1520
}
21+
22+
interface Develocity {
23+
URI uri();
24+
String accessKey();
25+
}
26+
27+
interface Jenkins {
28+
long githubAppId();
29+
}
1630
}

src/main/java/org/hibernate/infra/bot/config/RepositoryConfig.java

+13
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public class RepositoryConfig {
1111

1212
public JiraConfig jira;
1313

14+
public DevelocityConfig develocity;
15+
1416
public static class JiraConfig {
1517
private Optional<Pattern> issueKeyPattern = Optional.empty();
1618

@@ -83,4 +85,15 @@ public void setTitlePattern(String titlePattern) {
8385
this.titlePattern = Pattern.compile( titlePattern );
8486
}
8587
}
88+
89+
public static class DevelocityConfig {
90+
public DevelocityBuildScanConfig buildScan;
91+
}
92+
93+
public static class DevelocityBuildScanConfig {
94+
95+
public boolean extract = false;
96+
97+
}
98+
8699
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.hibernate.infra.bot.develocity;
2+
3+
import java.net.URI;
4+
import java.util.List;
5+
6+
public record DevelocityCIBuildScan(String provider, String jobOrWorkflow, String stage,
7+
Long availableAt, List<String> tags, List<String> goals,
8+
Status status, Status testStatus,
9+
URI buildScan, URI tests, URI logs) {
10+
public enum Status {
11+
SUCCESS,
12+
FAILURE
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.hibernate.infra.bot.util;
2+
3+
import java.util.regex.Pattern;
4+
5+
public record JenkinsRunId(String job, int run) {
6+
7+
private static final Pattern FORMAT = Pattern.compile( "(.+)#(\\d+)" );
8+
9+
public static JenkinsRunId parse(String string) {
10+
var matcher = FORMAT.matcher( string );
11+
if ( !matcher.matches() ) {
12+
throw new IllegalArgumentException( "Invalid format for a Jenkins run ID: " + string );
13+
}
14+
return new JenkinsRunId( matcher.group( 1 ), Integer.parseInt( matcher.group( 2 ) ) );
15+
}
16+
}

0 commit comments

Comments
 (0)