Skip to content

Commit 67dc5bc

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

File tree

9 files changed

+4540
-6
lines changed

9 files changed

+4540
-6
lines changed

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Specifically, most of the GitHub-related features in this bot are powered by
1111

1212
## Features
1313

14+
### Pull request checking
15+
1416
This bot checks various contribution rules on pull requests submitted to Hibernate projects on GitHub,
1517
and notifies the pull request authors of any change they need to work on.
1618

@@ -21,9 +23,16 @@ This includes:
2123
* Proper formatting of commits: every commit message must start with the key of a JIRA ticket.
2224
* Etc.
2325

24-
The bot can also be configured to automatically add links to JIRA issues in PR descriptions. When this is enabled
26+
### Jira link insertion
27+
28+
Optionally, the bot can be configured to automatically add links to JIRA issues in PR descriptions. When this is enabled
2529
links to JIRA tickets will be appended at the bottom of the PR body.
2630

31+
### Develocity build scan extraction
32+
33+
Optionally, the bot can be configured to automatically create a GitHub check listing Develocity build scans
34+
for every commit that has completed checks related to CI (GitHub Actions or Jenkins).
35+
2736
## Configuration
2837

2938
### Enabling the bot in a new repository
@@ -33,7 +42,7 @@ You will need admin rights in the Hibernate organization.
3342
Go to [the installed application settings](https://github.com/organizations/hibernate/settings/installations/15390286)
3443
and add your repository under "Repository access".
3544

36-
If you wish to enable the JIRA-related features as well,
45+
If you wish to enable the JIRA-related or Develocity-related features as well,
3746
create the file `.github/hibernate-github-bot.yml` in default branch of your repository,
3847
with the following content:
3948

@@ -62,6 +71,11 @@ jira:
6271
# Ignore all paths matching a given pattern
6372
- "*/Jenkinsfile"
6473
- "*.Jenkinsfile"
74+
develocity:
75+
buildScan:
76+
# To have the bot create a GitHub check listing Develocity build scans
77+
# for every commit that has completed checks related to CI (GitHub Actions or Jenkins)
78+
check: true
6579
```
6680
6781
### Altering the infrastructure

pom.xml

Lines changed: 10 additions & 2 deletions
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>
Lines changed: 243 additions & 0 deletions
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.check ) {
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

Lines changed: 14 additions & 0 deletions
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

Lines changed: 13 additions & 0 deletions
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 check = false;
96+
97+
}
98+
8699
}
Lines changed: 14 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)