|
| 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 | +} |
0 commit comments