This is an automated email from the ASF dual-hosted git repository.

olamy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-dist-tool.git


The following commit(s) were added to refs/heads/master by this push:
     new cb20e2b  Use Jenkins Json API rather than html scrapping (#161)
cb20e2b is described below

commit cb20e2b2f1415471e1d63299f161aee365d73685
Author: Olivier Lamy <[email protected]>
AuthorDate: Sun Mar 15 19:12:06 2026 +1000

    Use Jenkins Json API rather than html scrapping (#161)
    
    * Use Json data for ListMasterJobsReport
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * real testing
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * limit concurrency call
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * fix this withMaven reporting
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * Make ListBranchesReport faster
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * spotless
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * use virtual thread
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * upgrade github api to fix incompatibility with jackson
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * add needed jenkins repo
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * spotless
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * add timeout
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * we will need some token
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * spotless
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    * Makes MLStats way faster (reactive and parallel)
    
    Signed-off-by: Olivier Lamy <[email protected]>
    
    ---------
    
    Signed-off-by: Olivier Lamy <[email protected]>
---
 pom.xml                                            |  30 ++++-
 .../org/apache/maven/dist/tools/JsonRetry.java     |  84 ++++++++++++
 .../tools/committers/CommittersStatsReport.java    |  25 ++--
 .../maven/dist/tools/committers/MLStats.java       | 121 ++++++++---------
 .../maven/dist/tools/jobs/AbstractJobsReport.java  |   4 +
 .../tools/jobs/branches/ListBranchesReport.java    | 143 ++++++++++++---------
 .../tools/jobs/master/ListMasterJobsReport.java    |  85 ++++++------
 .../dist/tools/memorycheck/MemoryCheckReport.java  |   8 +-
 .../committers/MavenCommittersRepositoryTest.java  |  63 ++++++---
 9 files changed, 364 insertions(+), 199 deletions(-)

diff --git a/pom.xml b/pom.xml
index cebeb27..4ba5f08 100644
--- a/pom.xml
+++ b/pom.xml
@@ -152,13 +152,28 @@
     <dependency>
       <groupId>org.kohsuke</groupId>
       <artifactId>github-api</artifactId>
-      <version>1.129</version>
+      <version>2.0-rc.5</version>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit</artifactId>
       <version>7.5.0.202512021534-r</version>
     </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-client</artifactId>
+      <version>12.1.7</version>
+    </dependency>
+    <dependency>
+      <groupId>io.projectreactor</groupId>
+      <artifactId>reactor-core</artifactId>
+      <version>3.7.3</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-reactive-httpclient</artifactId>
+      <version>4.1.4</version>
+    </dependency>
     <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-core</artifactId>
@@ -187,9 +202,9 @@
       <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>org.wiremock</groupId>
-      <artifactId>wiremock</artifactId>
-      <version>3.13.2</version>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-server</artifactId>
+      <version>12.1.7</version>
       <scope>test</scope>
     </dependency>
     <dependency>
@@ -199,6 +214,13 @@
       <scope>test</scope>
     </dependency>
   </dependencies>
+  <repositories>
+    <!-- https://github.com/hub4j/github-api/issues/2206 -->
+    <repository>
+      <id>repo.jenkins-ci.org</id>
+      <url>https://repo.jenkins-ci.org/public/</url>
+    </repository>
+  </repositories>
 
   <build>
     <pluginManagement>
diff --git a/src/main/java/org/apache/maven/dist/tools/JsonRetry.java 
b/src/main/java/org/apache/maven/dist/tools/JsonRetry.java
new file mode 100644
index 0000000..7948a91
--- /dev/null
+++ b/src/main/java/org/apache/maven/dist/tools/JsonRetry.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.dist.tools;
+
+import java.util.concurrent.TimeUnit;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.Request;
+import org.eclipse.jetty.reactive.client.ReactiveRequest;
+import org.eclipse.jetty.reactive.client.ReactiveResponse;
+import reactor.core.publisher.Mono;
+
+public class JsonRetry {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    private HttpClient httpClient = new HttpClient();
+
+    private JsonRetry() {
+        try {
+            this.httpClient.start();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static class JsonRetryHolder {
+        private static final JsonRetry INSTANCE = new JsonRetry();
+    }
+
+    public static JsonRetry getInstance() {
+        return JsonRetryHolder.INSTANCE;
+    }
+
+    public HttpClient getHttpClient() {
+        return httpClient;
+    }
+
+    public static JsonNode get(String url) throws Exception {
+        Request request = getInstance().httpClient.newRequest(url);
+        String apiToken = System.getenv("API_TOKEN");
+        if (StringUtils.isNotBlank(apiToken)) {
+            request.headers(httpFields -> httpFields.add("Authorization", 
"Basic " + apiToken));
+        }
+        String json = request.send().getContentAsString();
+        return json != null ? OBJECT_MAPPER.readTree(json) : null;
+    }
+
+    public static Mono<JsonNode> getAsync(String url) {
+        Request request = getInstance().httpClient.newRequest(url).timeout(60, 
TimeUnit.SECONDS);
+        String apiToken = System.getenv("API_TOKEN");
+        if (StringUtils.isNotBlank(apiToken)) {
+            request.headers(httpFields -> httpFields.add("Authorization", 
"Basic " + apiToken));
+        }
+        ReactiveRequest reactiveRequest = 
ReactiveRequest.newBuilder(request).build();
+        return 
Mono.from(reactiveRequest.response(ReactiveResponse.Content.asString()))
+                .flatMap(json -> {
+                    try {
+                        return Mono.justOrEmpty(OBJECT_MAPPER.readTree(json));
+                    } catch (Exception e) {
+                        return Mono.error(e);
+                    }
+                });
+    }
+}
diff --git 
a/src/main/java/org/apache/maven/dist/tools/committers/CommittersStatsReport.java
 
b/src/main/java/org/apache/maven/dist/tools/committers/CommittersStatsReport.java
index 5794e63..1265826 100644
--- 
a/src/main/java/org/apache/maven/dist/tools/committers/CommittersStatsReport.java
+++ 
b/src/main/java/org/apache/maven/dist/tools/committers/CommittersStatsReport.java
@@ -38,9 +38,11 @@ import 
org.apache.maven.dist.tools.committers.MavenCommittersRepository.Committe
 import org.apache.maven.doxia.sink.Sink;
 import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet.Semantics;
 import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.reporting.AbstractMavenReport;
 import org.apache.maven.reporting.AbstractMavenReportRenderer;
 import org.apache.maven.reporting.MavenReportException;
+import reactor.core.publisher.Flux;
 
 /**
  * Generate a Committers statistic
@@ -52,6 +54,9 @@ public class CommittersStatsReport extends 
AbstractMavenReport {
 
     public static final int LAST_ACTIVITY_MONTHS_WARNING = 2 * 12;
 
+    @Parameter(defaultValue = "4", property = 
"dist-tool.committers.concurrency")
+    private int concurrency;
+
     private final Map<String, MLStats> mlStats;
 
     private final MavenCommittersRepository mavenCommitters;
@@ -110,14 +115,18 @@ public class CommittersStatsReport extends 
AbstractMavenReport {
         }
 
         private Map<Committer, List<String>> retrieveCommitterStats() {
-            Map<Committer, List<String>> result = new LinkedHashMap<>();
-            for (Committer committer : mavenCommitters.getCommitters()) {
-                List<String> lastDateList = mlStats.values().stream()
-                        .map(ml -> ml.getLast(committer))
-                        .toList();
-                result.put(committer, lastDateList);
-            }
-            return result;
+            List<Committer> committers = new 
ArrayList<>(mavenCommitters.getCommitters());
+            List<MLStats> statsList = new ArrayList<>(mlStats.values());
+
+            return Flux.fromIterable(committers)
+                    .flatMapSequential(
+                            committer -> Flux.fromIterable(statsList)
+                                    .flatMapSequential(ml -> 
ml.getLastAsync(committer))
+                                    .collectList()
+                                    .map(dates -> Map.entry(committer, dates)),
+                            concurrency)
+                    .collectMap(Map.Entry::getKey, Map.Entry::getValue, 
LinkedHashMap::new)
+                    .block();
         }
 
         private void renderStatsTable(Map<Committer, List<String>> 
committerStats) {
diff --git a/src/main/java/org/apache/maven/dist/tools/committers/MLStats.java 
b/src/main/java/org/apache/maven/dist/tools/committers/MLStats.java
index ede54c7..b3d4ea9 100644
--- a/src/main/java/org/apache/maven/dist/tools/committers/MLStats.java
+++ b/src/main/java/org/apache/maven/dist/tools/committers/MLStats.java
@@ -18,29 +18,29 @@
  */
 package org.apache.maven.dist.tools.committers;
 
-import java.io.IOException;
-import java.io.InputStream;
 import java.net.URI;
 import java.net.URLEncoder;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
 import java.nio.charset.StandardCharsets;
-import java.time.Duration;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import com.fasterxml.jackson.core.JsonFactory;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.maven.dist.tools.JsonRetry;
 import 
org.apache.maven.dist.tools.committers.MavenCommittersRepository.Committer;
 import org.apache.maven.doxia.sink.Sink;
+import org.eclipse.jetty.client.Request;
+import org.eclipse.jetty.reactive.client.ReactiveRequest;
+import org.eclipse.jetty.reactive.client.ReactiveResponse;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
 
 import static java.util.Map.entry;
 
@@ -48,6 +48,8 @@ public abstract class MLStats {
 
     private final Logger log = LoggerFactory.getLogger(MLStats.class);
 
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
     private static final String ML_STATS_ADDRES = 
"https://lists.apache.org/api/stats.lua";;
 
     private static final Map<String, String> STANDARD_QUERY_PARAMS = 
Map.ofEntries(
@@ -70,72 +72,55 @@ public abstract class MLStats {
         sink.text(" and header_from " + (name ? "committer name" : 
"<committerId>@apache.org"));
     }
 
-    public String getLast(Committer committer) {
-
-        List<Map<String, String>> queryParamsList = 
getQueryParamsList(committer);
-        return queryParamsList.stream()
+    public Mono<String> getLastAsync(Committer committer) {
+        List<URI> uris = getQueryParamsList(committer).stream()
                 .map(this::prepareStatsURI)
-                .map(this::getLastFromML)
-                .filter(Optional::isPresent)
-                .map(Optional::get)
-                .max(Comparator.naturalOrder())
-                .orElse("-");
+                .toList();
+
+        return Flux.fromIterable(uris)
+                .flatMapSequential(uri -> {
+                    Request request = JsonRetry.getInstance()
+                            .getHttpClient()
+                            .newRequest(uri.toString())
+                            .headers(f -> f.add("Accept", "application/json"))
+                            .timeout(60, TimeUnit.SECONDS);
+                    ReactiveRequest reactiveRequest =
+                            ReactiveRequest.newBuilder(request).build();
+                    return 
Mono.from(reactiveRequest.response(ReactiveResponse.Content.asString()))
+                            .flatMap(json -> {
+                                try {
+                                    Optional<String> last = 
parseLastFromNode(OBJECT_MAPPER.readTree(json));
+                                    log.info("Query: {}, returns: {}", uri, 
last);
+                                    return Mono.justOrEmpty(last);
+                                } catch (Exception e) {
+                                    return Mono.error(e);
+                                }
+                            })
+                            .onErrorResume(e -> {
+                                log.warn("Query: {}, error: {}", uri, 
e.getMessage());
+                                return Mono.empty();
+                            });
+                })
+                .collect(Collectors.maxBy(Comparator.naturalOrder()))
+                .map(opt -> opt.orElse("-"));
     }
 
-    private Optional<String> getLastFromML(URI statsURI) {
-        try (HttpClient client = HttpClient.newHttpClient()) {
-            HttpRequest request = HttpRequest.newBuilder()
-                    .GET()
-                    .header("Accept", "application/json")
-                    .uri(statsURI)
-                    .timeout(Duration.ofSeconds(60))
-                    .build();
-            HttpResponse<InputStream> response = client.send(request, 
HttpResponse.BodyHandlers.ofInputStream());
-            Optional<String> last = parseLast(response.body());
-            log.info("Query: {}, returns: {}", statsURI, last);
-            return last;
-
-        } catch (IOException e) {
-            log.warn("Query: {}, error: {}", statsURI, e.getMessage());
-            // try next one ...
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new RuntimeException(e);
-        }
-        return Optional.empty();
+    public String getLast(Committer committer) {
+        return getLastAsync(committer).block();
     }
 
-    private Optional<String> parseLast(InputStream input) throws IOException {
-        JsonFactory factory = new JsonFactory();
-        Integer lastYear = null;
-        Integer lastMonth = null;
-
-        try (JsonParser parser = factory.createParser(input)) {
-            while ((lastYear == null || lastMonth == null) && 
parser.nextToken() != JsonToken.END_OBJECT) {
-                if (parser.currentToken() != JsonToken.VALUE_NUMBER_INT) {
-                    continue;
-                }
-                String name = parser.currentName();
-                switch (name) {
-                    case "lastYear":
-                        lastYear = parser.getValueAsInt();
-                        break;
-                    case "lastMonth":
-                        lastMonth = parser.getValueAsInt();
-                        break;
-                    default:
-                    // ignore
-                }
-            }
+    private Optional<String> parseLastFromNode(JsonNode node) {
+        JsonNode lastYearNode = node.get("lastYear");
+        JsonNode lastMonthNode = node.get("lastMonth");
+        if (lastYearNode == null || lastMonthNode == null) {
+            return Optional.empty();
         }
-
-        if (lastYear != null && lastMonth != null) {
-            if (lastYear == 1970 && lastMonth == 1) {
-                return Optional.empty();
-            }
-            return Optional.of(String.format("%04d-%02d", lastYear, 
lastMonth));
+        int lastYear = lastYearNode.asInt();
+        int lastMonth = lastMonthNode.asInt();
+        if (lastYear == 1970 && lastMonth == 1) {
+            return Optional.empty();
         }
-        return Optional.empty();
+        return Optional.of(String.format("%04d-%02d", lastYear, lastMonth));
     }
 
     private URI prepareStatsURI(Map<String, String> queryParams) {
diff --git 
a/src/main/java/org/apache/maven/dist/tools/jobs/AbstractJobsReport.java 
b/src/main/java/org/apache/maven/dist/tools/jobs/AbstractJobsReport.java
index 3b5bd9d..eb7e0c6 100644
--- a/src/main/java/org/apache/maven/dist/tools/jobs/AbstractJobsReport.java
+++ b/src/main/java/org/apache/maven/dist/tools/jobs/AbstractJobsReport.java
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.stream.Collectors;
 
 import org.apache.maven.dist.tools.JsoupRetry;
+import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.reporting.AbstractMavenReport;
 import org.apache.maven.reporting.MavenReportException;
 import org.jsoup.nodes.Document;
@@ -35,6 +36,9 @@ import org.jsoup.select.Elements;
 public abstract class AbstractJobsReport extends AbstractMavenReport {
     protected static final String GITBOX_URL = 
"https://gitbox.apache.org/repos/asf";;
 
+    @Parameter(defaultValue = "8", property = "dist-tool.jobs.concurrency")
+    protected int concurrency;
+
     protected static final String MAVENBOX_JOBS_BASE_URL = 
"https://ci-maven.apache.org/job/Maven/job/maven-box/job/";;
 
     private static final Collection<String> EXCLUDED = Arrays.asList(
diff --git 
a/src/main/java/org/apache/maven/dist/tools/jobs/branches/ListBranchesReport.java
 
b/src/main/java/org/apache/maven/dist/tools/jobs/branches/ListBranchesReport.java
index 631a158..c749f4b 100644
--- 
a/src/main/java/org/apache/maven/dist/tools/jobs/branches/ListBranchesReport.java
+++ 
b/src/main/java/org/apache/maven/dist/tools/jobs/branches/ListBranchesReport.java
@@ -21,7 +21,6 @@ package org.apache.maven.dist.tools.jobs.branches;
 import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.net.URL;
-import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
@@ -29,10 +28,12 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
-import org.apache.maven.dist.tools.JsoupRetry;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.maven.dist.tools.JsonRetry;
 import org.apache.maven.dist.tools.jobs.AbstractJobsReport;
 import org.apache.maven.doxia.sink.Sink;
 import org.apache.maven.doxia.sink.SinkEventAttributes;
@@ -42,7 +43,10 @@ import org.apache.maven.reporting.MavenReportException;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.Ref;
-import org.jsoup.nodes.Document;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Scheduler;
+import reactor.core.scheduler.Schedulers;
 
 /**
  * Generate report with build status of the Jenkins job for the master branch 
of every Git repository in
@@ -183,66 +187,87 @@ public class ListBranchesReport extends 
AbstractJobsReport {
     protected void executeReport(Locale locale) throws MavenReportException {
         Collection<String> repositoryNames = repositoryNames();
 
-        List<Result> repoStatus = new ArrayList<>(repositoryNames.size());
-
-        for (String repository : repositoryNames) {
-            getLog().info("processing " + repository);
-            final String repositoryJobUrl = MAVENBOX_JOBS_BASE_URL + 
repository;
-
-            try {
-                Document jenkinsBranchesDoc = JsoupRetry.get(repositoryJobUrl);
-                Result result = new Result(repository, repositoryJobUrl);
-                int masterBranchesGit = 0;
-                int masterBranchesJenkins = 0;
-                Collection<String> jiraBranchesGit = new ArrayList<>();
-                Collection<String> jiraBranchesJenkins = new ArrayList<>();
-                Collection<String> dependabotBranchesGit = new ArrayList<>();
-                Collection<String> dependabotBranchesJenkins = new 
ArrayList<>();
-                Collection<String> restGit = new ArrayList<>();
-                Collection<String> restJenkins = new ArrayList<>();
-
-                for (String branch : getBranches(repository)) {
-                    if ("master".equals(branch)) {
-                        masterBranchesGit++;
-
-                        if (jenkinsBranchesDoc.getElementById("job_master") != 
null) {
-                            masterBranchesJenkins++;
-                        }
-                    } else if (JIRAPROJECTS.containsKey(repository)
-                            && 
branch.toUpperCase().startsWith(JIRAPROJECTS.get(repository) + '-')) {
-                        jiraBranchesGit.add(branch);
-                        if 
(jenkinsBranchesDoc.getElementById(URLEncoder.encode("job_" + branch, "UTF-8")) 
!= null) {
-                            jiraBranchesJenkins.add(branch);
-                        }
-                    } else if (branch.startsWith("dependabot/")) {
-                        dependabotBranchesGit.add(branch);
-                        if 
(jenkinsBranchesDoc.getElementById(URLEncoder.encode("job_" + branch, "UTF-8")) 
!= null) {
-                            dependabotBranchesJenkins.add(branch);
-                        }
-                    } else {
-                        restGit.add(branch);
-                        if 
(jenkinsBranchesDoc.getElementById(URLEncoder.encode("job_" + branch, "UTF-8")) 
!= null) {
-                            restJenkins.add(branch);
-                        }
+        List<Result> repoStatus = Flux.fromIterable(repositoryNames)
+                .flatMap(
+                        repo -> fetchResult(repo).onErrorResume(e -> {
+                            getLog().warn("Failed to read status for " + repo 
+ " Jenkins job " + MAVENBOX_JOBS_BASE_URL
+                                    + repo);
+                            return Mono.empty();
+                        }),
+                        concurrency)
+                .collectList()
+                .block();
+
+        generateReport(repoStatus);
+    }
+
+    private Mono<Result> fetchResult(String repository) {
+        String repositoryJobUrl = MAVENBOX_JOBS_BASE_URL + repository + 
"/api/json?tree=jobs[name]";
+
+        Mono<JsonNode> jenkinsMono = JsonRetry.getAsync(repositoryJobUrl);
+        Scheduler scheduler = 
Schedulers.fromExecutorService(Executors.newVirtualThreadPerTaskExecutor());
+        Mono<Collection<String>> branchesMono =
+                Mono.fromCallable(() -> 
getBranches(repository)).subscribeOn(scheduler);
+
+        return Mono.zip(jenkinsMono, branchesMono).map(tuple -> {
+            JsonNode jenkinsBranchesDoc = tuple.getT1();
+            Collection<String> gitBranches = tuple.getT2();
+
+            Result result = new Result(repository, repositoryJobUrl);
+            int masterBranchesGit = 0;
+            int masterBranchesJenkins = 0;
+            Collection<String> jiraBranchesGit = new ArrayList<>();
+            Collection<String> jiraBranchesJenkins = new ArrayList<>();
+            Collection<String> dependabotBranchesGit = new ArrayList<>();
+            Collection<String> dependabotBranchesJenkins = new ArrayList<>();
+            Collection<String> restGit = new ArrayList<>();
+            Collection<String> restJenkins = new ArrayList<>();
+
+            for (String branch : gitBranches) {
+                if ("master".equals(branch)) {
+                    masterBranchesGit++;
+
+                    if 
(jenkinsBranchesDoc.get("jobs").valueStream().anyMatch(n -> n.get("name")
+                            .asText()
+                            .equals("master"))) {
+                        masterBranchesJenkins++;
+                    }
+                } else if (JIRAPROJECTS.containsKey(repository)
+                        && 
branch.toUpperCase().startsWith(JIRAPROJECTS.get(repository) + '-')) {
+                    jiraBranchesGit.add(branch);
+                    if 
(jenkinsBranchesDoc.get("jobs").valueStream().anyMatch(n -> n.get("name")
+                            .asText()
+                            .equals(branch))) {
+                        jiraBranchesJenkins.add(branch);
+                    }
+                } else if (branch.startsWith("dependabot/")) {
+                    dependabotBranchesGit.add(branch);
+                    if 
(jenkinsBranchesDoc.get("jobs").valueStream().anyMatch(n -> n.get("name")
+                            .asText()
+                            .equals(branch))) {
+                        dependabotBranchesJenkins.add(branch);
+                    }
+                } else {
+                    restGit.add(branch);
+                    if 
(jenkinsBranchesDoc.get("jobs").valueStream().anyMatch(n -> n.get("name")
+                            .asText()
+                            .equals(branch))) {
+                        restJenkins.add(branch);
                     }
                 }
-
-                result.setMasterBranchesGit(masterBranchesGit);
-                result.setMasterBranchesJenkins(masterBranchesJenkins);
-                result.setJiraBranchesGit(jiraBranchesGit);
-                result.setJiraBranchesJenkins(jiraBranchesJenkins);
-                result.setDependabotBranchesGit(dependabotBranchesGit);
-                result.setDependabotBranchesJenkins(dependabotBranchesJenkins);
-                result.setRestGit(restGit);
-                result.setRestJenkins(restJenkins);
-
-                repoStatus.add(result);
-            } catch (IOException | GitAPIException e) {
-                getLog().warn("Failed to read status for " + repository + " 
Jenkins job " + repositoryJobUrl);
             }
-        }
 
-        generateReport(repoStatus);
+            result.setMasterBranchesGit(masterBranchesGit);
+            result.setMasterBranchesJenkins(masterBranchesJenkins);
+            result.setJiraBranchesGit(jiraBranchesGit);
+            result.setJiraBranchesJenkins(jiraBranchesJenkins);
+            result.setDependabotBranchesGit(dependabotBranchesGit);
+            result.setDependabotBranchesJenkins(dependabotBranchesJenkins);
+            result.setRestGit(restGit);
+            result.setRestJenkins(restJenkins);
+
+            return result;
+        });
     }
 
     private String getGitHubBranchesUrl(String repository) {
diff --git 
a/src/main/java/org/apache/maven/dist/tools/jobs/master/ListMasterJobsReport.java
 
b/src/main/java/org/apache/maven/dist/tools/jobs/master/ListMasterJobsReport.java
index c53cc30..92b9b17 100644
--- 
a/src/main/java/org/apache/maven/dist/tools/jobs/master/ListMasterJobsReport.java
+++ 
b/src/main/java/org/apache/maven/dist/tools/jobs/master/ListMasterJobsReport.java
@@ -18,10 +18,8 @@
  */
 package org.apache.maven.dist.tools.jobs.master;
 
-import java.io.IOException;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Comparator;
@@ -30,13 +28,15 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.stream.Collectors;
 
-import org.apache.maven.dist.tools.JsoupRetry;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.maven.dist.tools.JsonRetry;
 import org.apache.maven.dist.tools.jobs.AbstractJobsReport;
 import org.apache.maven.doxia.sink.Sink;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.reporting.MavenReportException;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
 
 /**
  * Generate report with build status of the Jenkins job for the master branch 
of every Git repository in
@@ -76,46 +76,49 @@ public class ListMasterJobsReport extends 
AbstractJobsReport {
     protected void executeReport(Locale locale) throws MavenReportException {
         Collection<String> repositoryNames = repositoryNames();
 
-        List<Result> repoStatus = new ArrayList<>(repositoryNames.size());
-
-        for (String repository : repositoryNames) {
-            final String repositoryJobUrl = MAVENBOX_JOBS_BASE_URL + 
repository;
-
-            try {
-                Document doc = JsoupRetry.get(repositoryJobUrl);
-
-                Result result = new Result(repository, repositoryJobUrl);
-
-                Element masterRow = doc.getElementById("job_master");
-                if (masterRow == null) {
-                    getLog().warn(MAVENBOX_JOBS_BASE_URL + repository + " is 
missing id job_master");
-                    continue;
-                } else if (masterRow.hasClass("job-status-red") || 
masterRow.hasClass("job-status-red-anime")) {
-                    result.setStatus("FAILURE");
-                } else if (masterRow.hasClass("job-status-yellow") || 
masterRow.hasClass("job-status-yellow-anime")) {
-                    result.setStatus("UNSTABLE");
-                } else if (masterRow.hasClass("job-status-blue") || 
masterRow.hasClass("job-status-blue-anime")) {
-                    result.setStatus("SUCCESS");
-                } else {
-                    result.setStatus("UNKNOWN");
-                }
-                result.setIcon(masterRow
-                        .select("span.build-status-icon__wrapper")
-                        .first()
-                        .outerHtml());
-
-                result.setLastBuild(getLastBuild(
-                        masterRow.child(3).attr("data"), 
masterRow.child(4).attr("data")));
-
-                repoStatus.add(result);
-            } catch (IOException e) {
-                getLog().warn("Failed to read status for " + repository + " 
Jenkins job " + repositoryJobUrl);
-            }
-        }
+        List<Result> repoStatus = Flux.fromIterable(repositoryNames)
+                .flatMap(
+                        repo -> JsonRetry.getAsync(MAVENBOX_JOBS_BASE_URL + 
repo
+                                        + 
"/api/json?tree=jobs[name,url,color,lastBuild[result,number]]")
+                                .flatMap(jsonNode -> buildResult(repo, 
jsonNode))
+                                .onErrorResume(e -> {
+                                    getLog().warn("Failed to read status for " 
+ repo + " Jenkins job "
+                                            + MAVENBOX_JOBS_BASE_URL + repo);
+                                    return Mono.empty();
+                                }),
+                        concurrency)
+                .collectList()
+                .block();
 
         generateReport(repoStatus);
     }
 
+    private Mono<Result> buildResult(String repository, JsonNode jsonNode) {
+        if (!(jsonNode instanceof ObjectNode objectNode)) {
+            getLog().warn("Failed to read JSON for " + repository + " Jenkins 
job " + MAVENBOX_JOBS_BASE_URL
+                    + repository);
+            return Mono.empty();
+        }
+        // find the master node
+        return Mono.justOrEmpty(objectNode
+                .get("jobs")
+                .valueStream()
+                .filter(n -> n.get("name").asText().equals("master"))
+                .findFirst()
+                .map(n -> {
+                    JsonNode lastBuild = n.get("lastBuild");
+                    String status = lastBuild != null ? 
lastBuild.get("result").asText() : "UNKNOWN";
+                    String buildUrl = n.get("url").asText()
+                            + n.get("lastBuild").get("number").asText();
+                    Result result = new Result(repository, buildUrl);
+                    result.setStatus(status);
+                    // 
https://ci-maven.apache.org/static/67d6365a/images/24x24/blue.png
+                    result.setIcon("https://ci-maven.apache.org/static/48x48/";
+                            + n.get("color").asText().toLowerCase() + ".png");
+                    return result;
+                }));
+    }
+
     private void generateReport(List<Result> repoStatus) {
         Sink sink = getSink();
 
diff --git 
a/src/main/java/org/apache/maven/dist/tools/memorycheck/MemoryCheckReport.java 
b/src/main/java/org/apache/maven/dist/tools/memorycheck/MemoryCheckReport.java
index 145fbb6..b01f518 100644
--- 
a/src/main/java/org/apache/maven/dist/tools/memorycheck/MemoryCheckReport.java
+++ 
b/src/main/java/org/apache/maven/dist/tools/memorycheck/MemoryCheckReport.java
@@ -118,9 +118,8 @@ public class MemoryCheckReport extends 
AbstractDistCheckReport {
         status.forEach(s -> {
             sink.listItem();
             sink.link(s.getHtmlUrl().toString());
-            sink.text(DateTimeFormatter.ISO_LOCAL_DATE
-                    .withZone(ZoneId.of("UTC"))
-                    .format(s.getStartedAt().toInstant()));
+            sink.text(
+                    
DateTimeFormatter.ISO_LOCAL_DATE.withZone(ZoneId.of("UTC")).format(s.getStartedAt()));
             if (s.getConclusion() == GHWorkflowRun.Conclusion.SUCCESS) {
                 iconSuccess(sink);
             } else {
@@ -168,8 +167,9 @@ public class MemoryCheckReport extends 
AbstractDistCheckReport {
 
     private List<GHWorkflowJob> getLatestBuildStatus() {
         try {
+            // FIXME we should use some GH token
             return StreamSupport.stream(
-                            GitHub.connect()
+                            GitHub.connectAnonymously()
                                     .getRepository(GITHUB_REPOSITORY)
                                     .queryWorkflowRuns()
                                     .list()
diff --git 
a/src/test/java/org/apache/maven/dist/tools/committers/MavenCommittersRepositoryTest.java
 
b/src/test/java/org/apache/maven/dist/tools/committers/MavenCommittersRepositoryTest.java
index 0e81d28..65afa5f 100644
--- 
a/src/test/java/org/apache/maven/dist/tools/committers/MavenCommittersRepositoryTest.java
+++ 
b/src/test/java/org/apache/maven/dist/tools/committers/MavenCommittersRepositoryTest.java
@@ -18,19 +18,24 @@
  */
 package org.apache.maven.dist.tools.committers;
 
+import java.net.InetSocketAddress;
 import java.util.List;
 
-import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
-import com.github.tomakehurst.wiremock.junit5.WireMockTest;
 import 
org.apache.maven.dist.tools.committers.MavenCommittersRepository.Committer;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.io.Content;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.Callback;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
-import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.get;
-import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
 import static org.assertj.core.api.Assertions.assertThat;
 
-@WireMockTest
 class MavenCommittersRepositoryTest {
 
     private static final String GROUP = """
@@ -63,17 +68,45 @@ class MavenCommittersRepositoryTest {
             }
             """;
 
-    @Test
-    void dataLoad(WireMockRuntimeInfo wireMockRuntimeInfo) {
+    private Server server;
+    private String baseUrl;
+
+    @BeforeEach
+    void startServer() throws Exception {
+        server = new Server(new InetSocketAddress("localhost", 0));
+        server.setHandler(new Handler.Abstract() {
+            @Override
+            public boolean handle(Request request, Response response, Callback 
callback) throws Exception {
+                String path = request.getHttpURI().getPath();
+                String body;
+                if ("/json/foundation/groups.json".equals(path)) {
+                    body = GROUP;
+                } else if ("/json/foundation/people_name.json".equals(path)) {
+                    body = NAMES;
+                } else {
+                    Response.writeError(request, response, callback, 404);
+                    return true;
+                }
+                response.setStatus(200);
+                response.getHeaders().put(HttpHeader.CONTENT_TYPE, 
"application/json");
+                Content.Sink.write(response, true, body, callback);
+                return true;
+            }
+        });
+        server.start();
+        int port = ((ServerConnector) 
server.getConnectors()[0]).getLocalPort();
+        baseUrl = "http://localhost:"; + port;
+    }
 
-        stubFor(get("/json/foundation/groups.json")
-                .willReturn(aResponse().withStatus(200).withBody(GROUP)));
-        stubFor(get("/json/foundation/people_name.json")
-                .willReturn(aResponse().withStatus(200).withBody(NAMES)));
+    @AfterEach
+    void stopServer() throws Exception {
+        server.stop();
+    }
 
-        MavenCommittersRepository mavenCommittersRepository =
-                new 
MavenCommittersRepository(wireMockRuntimeInfo.getHttpBaseUrl());
-        assertThat(mavenCommittersRepository.getCommitters())
+    @Test
+    void dataLoad() {
+        MavenCommittersRepository repo = new 
MavenCommittersRepository(baseUrl);
+        assertThat(repo.getCommitters())
                 .containsExactly(
                         new Committer("cstamas", List.of("Tamas Cservenak", 
"Tamás Cservenák"), true),
                         new Committer("m1", List.of("M1 name"), false),


Reply via email to