From 361e928e7326c3633e41c90aaec7fd0c456737ea Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 2 Oct 2025 15:22:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BB=8E=20WorkflowMultiBran?= =?UTF-8?q?chProject=20=E4=B8=AD=E6=A3=80=E6=9F=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=AC=20(#4576)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 4 +- .../jackhuang/hmcl/gradle/ci/CheckUpdate.java | 178 +++++++++++++----- 2 files changed, 129 insertions(+), 53 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e0333963c..8bdd11d0b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -78,10 +78,10 @@ tasks.register("updateDocuments") { tasks.register("checkUpdateDev") { tagPrefix.set("v") - api.set("https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json") + uri.set("https://ci.huangyuhui.net/job/HMCL") } tasks.register("checkUpdateStable") { tagPrefix.set("release-") - api.set("https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/api/json") + uri.set("https://ci.huangyuhui.net/job/HMCL-stable") } diff --git a/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/ci/CheckUpdate.java b/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/ci/CheckUpdate.java index 4b52b6ba5..316d104ef 100644 --- a/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/ci/CheckUpdate.java +++ b/buildSrc/src/main/java/org/jackhuang/hmcl/gradle/ci/CheckUpdate.java @@ -18,6 +18,8 @@ package org.jackhuang.hmcl.gradle.ci; import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.logging.Logger; @@ -35,6 +37,7 @@ import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.function.BiConsumer; @@ -45,8 +48,12 @@ import java.util.regex.Pattern; public abstract class CheckUpdate extends DefaultTask { private static final Logger LOGGER = Logging.getLogger(CheckUpdate.class); + private static final String WORKFLOW_JOB = "org.jenkinsci.plugins.workflow.job.WorkflowJob"; + private static final String WORKFLOW_MULTI_BRANCH_PROJECT = "org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject"; + private static final String FREE_STYLE_PROJECT = "hudson.model.FreeStyleProject"; + @Input - public abstract Property getApi(); + public abstract Property getUri(); @Input public abstract Property getTagPrefix(); @@ -55,81 +62,150 @@ public abstract class CheckUpdate extends DefaultTask { getOutputs().upToDateWhen(task -> false); } - private static T fetch(URI uri, Class type) throws IOException, InterruptedException { - // // HttpClient implements Closeable since Java 21 - //noinspection resource - var client = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.ALWAYS) - .build(); - - HttpResponse response = client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString()); - if (response.statusCode() / 100 != 2) { - throw new IOException("Bad status code: " + response.statusCode()); - } - - return new Gson().fromJson(response.body(), type); + private static URI toURI(String baseUri, String suffix) { + return URI.create(baseUri.endsWith("/") + ? baseUri + suffix + : baseUri + "/" + suffix + ); } @TaskAction - public void run() throws IOException, InterruptedException { + public void run() throws Exception { + String uri = getUri().get(); + URI apiUri = toURI(uri, "api/json"); + LOGGER.quiet("Fetching metadata from {}", apiUri); + + BuildMetadata buildMetadata; + + try (var helper = new Helper()) { + JsonObject body = Objects.requireNonNull(helper.fetch(apiUri, JsonObject.class)); + String jobType = Objects.requireNonNull(body.getAsJsonPrimitive("_class"), "Missing _class property") + .getAsString(); + + if (WORKFLOW_MULTI_BRANCH_PROJECT.equals(jobType)) { + Pattern namePattern = Pattern.compile("release%2F3\\.\\d+"); + + List metadatas = Objects.requireNonNull(helper.gson.fromJson(body.get("jobs"), new TypeToken>() { + }), "jobs") + .stream() + .filter(it -> WORKFLOW_JOB.equals(it._class())) + .filter(it -> namePattern.matcher(it.name()).matches()) + .filter(it -> !it.color().equals("disabled")) + .map(it -> { + try { + return fetchBuildInfo(helper, toURI(it.url, "lastSuccessfulBuild/api/json")); + } catch (Throwable e) { + throw new GradleException("Failed to retrieve build info from " + it.url(), e); + } + }).sorted(Comparator.comparing(BuildMetadata::timestamp)) + .toList(); + + if (metadatas.isEmpty()) + throw new GradleException("Failed to retrieve build metadata from " + apiUri); + + buildMetadata = metadatas.get(metadatas.size() - 1); + } else if (WORKFLOW_JOB.equals(jobType) || FREE_STYLE_PROJECT.equals(jobType)) { + buildMetadata = fetchBuildInfo(helper, toURI(uri, "lastSuccessfulBuild/api/json")); + } else { + throw new GradleException("Unsupported job type: " + jobType); + } + } + + LOGGER.quiet("Build metadata found: {}", buildMetadata); + String githubEnv = Objects.requireNonNullElse(System.getenv("GITHUB_ENV"), ""); if (githubEnv.isBlank()) LOGGER.warn("GITHUB_ENV is not set"); - String uri = getApi().get(); - LOGGER.info("Fetching metadata from {}", uri); - BuildInfo buildInfo = Objects.requireNonNull(fetch(URI.create(uri), BuildInfo.class), - "Could not fetch build info"); - try (PrintWriter writer = githubEnv.isBlank() ? null : new PrintWriter(Files.newBufferedWriter(Path.of(githubEnv), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND))) { BiConsumer addEnv = (name, value) -> { String item = name + "=" + value; - LOGGER.info(item); + LOGGER.quiet(item); if (writer != null) writer.println(item); }; - String revision = Objects.requireNonNullElse(buildInfo.actions(), List.of()) - .stream() - .filter(action -> "hudson.plugins.git.util.BuildData".equals(action._class)) - .map(BuildInfo.ActionInfo::lastBuiltRevision) - .map(BuildInfo.ActionInfo.BuiltRevision::SHA1) - .findFirst() - .orElseThrow(() -> new GradleException("Could not find revision")); - if (revision.matches("[0-9a-z]{40}")) - addEnv.accept("HMCL_COMMIT_SHA", revision); - else - throw new GradleException("Invalid revision: " + revision); - - Pattern fileNamePattern = Pattern.compile("HMCL-(?\\d+(?:\\.\\d+)+)\\.jar"); - String version = Objects.requireNonNullElse(buildInfo.artifacts(), List.of()) - .stream() - .map(BuildInfo.ArtifactInfo::fileName) - .map(fileNamePattern::matcher) - .filter(Matcher::matches) - .map(matcher -> matcher.group("version")) - .findFirst() - .orElseThrow(() -> new GradleException("Could not find .jar artifact")); - addEnv.accept("HMCL_VERSION", version); - addEnv.accept("HMCL_TAG_NAME", getTagPrefix().get() + version); + addEnv.accept("HMCL_COMMIT_SHA", buildMetadata.revision()); + addEnv.accept("HMCL_VERSION", buildMetadata.version()); + addEnv.accept("HMCL_TAG_NAME", getTagPrefix().get() + buildMetadata.version()); } } - private record BuildInfo(long number, - List artifacts, - List actions - ) { + private record BuildMetadata(String version, String revision, long timestamp) { + } - record ArtifactInfo(String fileName) { + private BuildMetadata fetchBuildInfo(Helper helper, URI uri) throws IOException, InterruptedException { + LOGGER.quiet("Fetching build info from {}", uri); + + BuildInfo buildInfo = Objects.requireNonNull(helper.fetch(uri, BuildInfo.class), "build info"); + + String revision = Objects.requireNonNullElse(buildInfo.actions(), List.of()) + .stream() + .filter(action -> "hudson.plugins.git.util.BuildData".equals(action._class())) + .map(ActionInfo::lastBuiltRevision) + .map(BuiltRevision::SHA1) + .findFirst() + .orElseThrow(() -> new GradleException("Could not find revision")); + if (!revision.matches("[0-9a-z]{40}")) + throw new GradleException("Invalid revision: " + revision); + + Pattern fileNamePattern = Pattern.compile("HMCL-(?\\d+(?:\\.\\d+)+)\\.jar"); + String version = Objects.requireNonNullElse(buildInfo.artifacts(), List.of()) + .stream() + .map(ArtifactInfo::fileName) + .map(fileNamePattern::matcher) + .filter(Matcher::matches) + .map(matcher -> matcher.group("version")) + .findFirst() + .orElseThrow(() -> new GradleException("Could not find .jar artifact")); + + return new BuildMetadata(version, revision, buildInfo.timestamp()); + } + + private static final class Helper implements AutoCloseable { + private final HttpClient client = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .build(); + private final Gson gson = new Gson(); + + private T fetch(URI uri, Class type) throws IOException, InterruptedException { + HttpResponse response = client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() / 100 != 2) { + throw new IOException("Bad status code " + response.statusCode() + " for " + uri); + } + + return gson.fromJson(response.body(), type); } - record ActionInfo(String _class, BuiltRevision lastBuiltRevision) { - record BuiltRevision(String SHA1) { + @Override + public void close() throws Exception { + // HttpClient implements AutoCloseable since Java 21 + if (((Object) client) instanceof AutoCloseable closeable) { + closeable.close(); } } } + private record SubJobInfo(String _class, String name, String url, String color) { + + } + + private record BuildInfo(long number, + long timestamp, + List artifacts, + List actions + ) { + } + + record ArtifactInfo(String fileName) { + } + + record ActionInfo(String _class, BuiltRevision lastBuiltRevision) { + } + + record BuiltRevision(String SHA1) { + } }