From b29a447e0f98026568c45ba3375c22e9a9fca3c1 Mon Sep 17 00:00:00 2001 From: Tobias Nett Date: Sat, 23 Jan 2021 22:58:04 +0100 Subject: [PATCH] feat(repositories): add adapter for new jenkins.terasology.io (#621) * refactor(repositories): mark adapter for old Jenkins as "legacy" * feat(repositories): add adapter for new jenkins.io builds --- .../JenkinsRepositoryAdapter.java | 105 +++++++++++++++--- .../LegacyJenkinsRepositoryAdapter.java | 99 +++++++++++++++++ .../repositories/RepositoryManager.java | 18 ++- 3 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/terasology/launcher/repositories/LegacyJenkinsRepositoryAdapter.java diff --git a/src/main/java/org/terasology/launcher/repositories/JenkinsRepositoryAdapter.java b/src/main/java/org/terasology/launcher/repositories/JenkinsRepositoryAdapter.java index dc72a9368..93f5fd8e1 100644 --- a/src/main/java/org/terasology/launcher/repositories/JenkinsRepositoryAdapter.java +++ b/src/main/java/org/terasology/launcher/repositories/JenkinsRepositoryAdapter.java @@ -4,6 +4,7 @@ package org.terasology.launcher.repositories; import com.google.gson.Gson; +import com.vdurmont.semver4j.Semver; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,52 +15,91 @@ import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.LinkedList; import java.util.List; +import java.util.Optional; +import java.util.Properties; import java.util.stream.Collectors; +/** + * Repository adapter for http://jenkins.terasology.io, replacing the {@link LegacyJenkinsRepositoryAdapter}. + *

+ * On the new Jenkins we can make use of the {@code versionInfo.properties} file to get the display name for the release + * along other metadata (for instance, the corresponding engine version). + *

+ * However, this means that we are doing {@code n + 1} API calls for fetching {@code n} release packages on each + * launcher start. + */ class JenkinsRepositoryAdapter implements ReleaseRepository { private static final Logger logger = LoggerFactory.getLogger(JenkinsRepositoryAdapter.class); + private static final String BASE_URL = "http://jenkins.terasology.io/teraorg/job/Terasology/"; + private static final String API_FILTER = "api/json?tree=" + "builds[" - + "actions[causes[upstreamBuild]]{0}," + "number," + "timestamp," + "result," + "artifacts[fileName,relativePath]," - + "url," - + "changeSet[items[msg]]]," - + "upstreamProjects[name]"; + + "url]"; private static final String TERASOLOGY_ZIP_PATTERN = "Terasology.*zip"; + private static final String ARTIFACT = "artifact/"; private final Gson gson = new Gson(); - private final String baseUrl; - private final String jobName; private final Build buildProfile; private final Profile profile; - JenkinsRepositoryAdapter(String baseUrl, String jobName, Build buildProfile, Profile profile) { - this.baseUrl = baseUrl; - this.jobName = jobName; + private final String jobSelector; + + JenkinsRepositoryAdapter(Profile profile, Build buildProfile) { this.buildProfile = buildProfile; this.profile = profile; + this.jobSelector = job(profileToJobName(profile)) + job(buildProfileToJobName(buildProfile)); } private boolean isSuccess(Jenkins.Build build) { return build.result == Jenkins.Build.Result.SUCCESS || build.result == Jenkins.Build.Result.UNSTABLE; } + private static String profileToJobName(Profile profile) { + switch (profile) { + case OMEGA: + return "Omega/"; + case ENGINE: + return "Terasology/"; + default: + throw new IllegalStateException("Unexpected value: " + profile); + } + } + + private static String buildProfileToJobName(Build buildProfile) { + switch (buildProfile) { + case STABLE: + return "master/"; + case NIGHTLY: + return "develop/"; + default: + throw new IllegalStateException("Unexpected value: " + buildProfile); + } + } + + private String job(String job) { + return "job/" + job; + } + public List fetchReleases() { final List pkgList = new LinkedList<>(); - final String apiUrl = baseUrl + "job/" + jobName + "/" + API_FILTER; + + final String apiUrl = BASE_URL + jobSelector + API_FILTER; logger.debug("fetching releases from '{}'", apiUrl); @@ -70,13 +110,27 @@ public List fetchReleases() { final Jenkins.ApiResult result = gson.fromJson(reader, Jenkins.ApiResult.class); for (Jenkins.Build build : result.builds) { if (isSuccess(build)) { - final List changelog = Arrays.stream(build.changeSet.items) - .map(change -> change.msg) - .collect(Collectors.toList()); final String url = getArtifactUrl(build, TERASOLOGY_ZIP_PATTERN); if (url != null) { - final GameIdentifier id = new GameIdentifier(build.number, buildProfile, profile); + + Properties versionInfo = fetchProperties(getArtifactUrl(build, "versionInfo.properties")); + final Date timestamp = new Date(build.timestamp); + + String displayName = versionInfo.getProperty("displayVersion"); + + final GameIdentifier id = new GameIdentifier(displayName, buildProfile, profile); + + Semver semver = deriveSemver(versionInfo); + logger.debug("Derived SemVer for {}: \t{}", id, semver); + + List changelog = Optional.ofNullable(build.changeSet) + .map(changeSet -> + Arrays.stream(changeSet.items) + .map(change -> change.msg) + .collect(Collectors.toList())).orElse(new ArrayList<>()); + + final GameRelease release = new GameRelease(id, new URL(url), changelog, timestamp); pkgList.add(release); } @@ -88,12 +142,33 @@ public List fetchReleases() { return pkgList; } + private Properties fetchProperties(final String artifactUrl) { + if (artifactUrl != null) { + try (InputStream inputStream = new URL(artifactUrl).openStream()) { + final Properties properties = new Properties(); + properties.load(inputStream); + return properties; + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; + } + + private Semver deriveSemver(final Properties versionInfo) { + if (versionInfo != null) { + final Semver engineVersion = new Semver(versionInfo.getProperty("engineVersion")); + return engineVersion.withBuild(versionInfo.getProperty("buildId")); + } + return null; + } + @Nullable private String getArtifactUrl(Jenkins.Build build, String regex) { return Arrays.stream(build.artifacts) .filter(artifact -> artifact.fileName.matches(regex)) .findFirst() - .map(artifact -> build.url + "artifact/" + artifact.relativePath) + .map(artifact -> build.url + ARTIFACT + artifact.relativePath) .orElse(null); } } diff --git a/src/main/java/org/terasology/launcher/repositories/LegacyJenkinsRepositoryAdapter.java b/src/main/java/org/terasology/launcher/repositories/LegacyJenkinsRepositoryAdapter.java new file mode 100644 index 000000000..0862a339a --- /dev/null +++ b/src/main/java/org/terasology/launcher/repositories/LegacyJenkinsRepositoryAdapter.java @@ -0,0 +1,99 @@ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.launcher.repositories; + +import com.google.gson.Gson; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.launcher.model.Build; +import org.terasology.launcher.model.GameIdentifier; +import org.terasology.launcher.model.GameRelease; +import org.terasology.launcher.model.Profile; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +class LegacyJenkinsRepositoryAdapter implements ReleaseRepository { + + private static final Logger logger = LoggerFactory.getLogger(LegacyJenkinsRepositoryAdapter.class); + + private static final String API_FILTER = "api/json?tree=" + + "builds[" + + "actions[causes[upstreamBuild]]{0}," + + "number," + + "timestamp," + + "result," + + "artifacts[fileName,relativePath]," + + "url," + + "changeSet[items[msg]]]," + + "upstreamProjects[name]"; + + private static final String TERASOLOGY_ZIP_PATTERN = "Terasology.*zip"; + + private final Gson gson = new Gson(); + + private final String baseUrl; + private final String jobName; + private final Build buildProfile; + private final Profile profile; + + LegacyJenkinsRepositoryAdapter(String baseUrl, String jobName, Build buildProfile, Profile profile) { + this.baseUrl = baseUrl; + this.jobName = jobName; + this.buildProfile = buildProfile; + this.profile = profile; + } + + private boolean isSuccess(Jenkins.Build build) { + return build.result == Jenkins.Build.Result.SUCCESS || build.result == Jenkins.Build.Result.UNSTABLE; + } + + public List fetchReleases() { + final List pkgList = new LinkedList<>(); + final String apiUrl = baseUrl + "job/" + jobName + "/" + API_FILTER; + + logger.debug("fetching releases from '{}'", apiUrl); + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader( + new URL(apiUrl).openStream()) + )) { + final Jenkins.ApiResult result = gson.fromJson(reader, Jenkins.ApiResult.class); + for (Jenkins.Build build : result.builds) { + if (isSuccess(build)) { + final List changelog = Arrays.stream(build.changeSet.items) + .map(change -> change.msg) + .collect(Collectors.toList()); + final String url = getArtifactUrl(build, TERASOLOGY_ZIP_PATTERN); + if (url != null) { + final GameIdentifier id = new GameIdentifier(build.number, buildProfile, profile); + final Date timestamp = new Date(build.timestamp); + final GameRelease release = new GameRelease(id, new URL(url), changelog, timestamp); + pkgList.add(release); + } + } + } + } catch (IOException e) { + logger.warn("Failed to fetch packages from: {}", apiUrl, e); + } + return pkgList; + } + + @Nullable + private String getArtifactUrl(Jenkins.Build build, String regex) { + return Arrays.stream(build.artifacts) + .filter(artifact -> artifact.fileName.matches(regex)) + .findFirst() + .map(artifact -> build.url + "artifact/" + artifact.relativePath) + .orElse(null); + } +} diff --git a/src/main/java/org/terasology/launcher/repositories/RepositoryManager.java b/src/main/java/org/terasology/launcher/repositories/RepositoryManager.java index be30c9724..1aad4fa91 100644 --- a/src/main/java/org/terasology/launcher/repositories/RepositoryManager.java +++ b/src/main/java/org/terasology/launcher/repositories/RepositoryManager.java @@ -18,12 +18,18 @@ public class RepositoryManager { private final Set releases; public RepositoryManager() { - ReleaseRepository terasologyNightly = new JenkinsRepositoryAdapter(JENKINS_BASE_URL, "Terasology", Build.NIGHTLY, Profile.ENGINE); - ReleaseRepository terasologyStable = new JenkinsRepositoryAdapter(JENKINS_BASE_URL, "TerasologyStable", Build.STABLE, Profile.ENGINE); - ReleaseRepository omegaNightly = new JenkinsRepositoryAdapter(JENKINS_BASE_URL, "DistroOmega", Build.NIGHTLY, Profile.OMEGA); - ReleaseRepository omegaStable = new JenkinsRepositoryAdapter(JENKINS_BASE_URL, "DistroOmegaRelease", Build.STABLE, Profile.OMEGA); - - Set all = Sets.newHashSet(terasologyNightly, terasologyStable, omegaNightly, omegaStable); + ReleaseRepository legacyEngineNightly = new LegacyJenkinsRepositoryAdapter(JENKINS_BASE_URL, "Terasology", Build.NIGHTLY, Profile.ENGINE); + ReleaseRepository legacyEngineStable = new LegacyJenkinsRepositoryAdapter(JENKINS_BASE_URL, "TerasologyStable", Build.STABLE, Profile.ENGINE); + ReleaseRepository legacyOmegaNightly = new LegacyJenkinsRepositoryAdapter(JENKINS_BASE_URL, "DistroOmega", Build.NIGHTLY, Profile.OMEGA); + ReleaseRepository legacyOmegaStable = new LegacyJenkinsRepositoryAdapter(JENKINS_BASE_URL, "DistroOmegaRelease", Build.STABLE, Profile.OMEGA); + + ReleaseRepository omegaNightly = new JenkinsRepositoryAdapter(Profile.OMEGA, Build.NIGHTLY); + ReleaseRepository omegaStable = new JenkinsRepositoryAdapter(Profile.OMEGA, Build.STABLE); + + Set all = Sets.newHashSet( + legacyEngineNightly, legacyEngineStable, + legacyOmegaNightly, legacyOmegaStable, + omegaNightly, omegaStable); releases = fetchReleases(all); }