Skip to content

Commit

Permalink
feat(repositories): add adapter for new jenkins.terasology.io (#621)
Browse files Browse the repository at this point in the history
* refactor(repositories): mark adapter for old Jenkins as "legacy"
* feat(repositories): add adapter for new jenkins.io builds
  • Loading branch information
skaldarnar authored Jan 23, 2021
1 parent 59f8a31 commit b29a447
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}.
* <p>
* 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).
* <p>
* 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<GameRelease> fetchReleases() {
final List<GameRelease> 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);

Expand All @@ -70,13 +110,27 @@ public List<GameRelease> fetchReleases() {
final Jenkins.ApiResult result = gson.fromJson(reader, Jenkins.ApiResult.class);
for (Jenkins.Build build : result.builds) {
if (isSuccess(build)) {
final List<String> 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<String> 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);
}
Expand All @@ -88,12 +142,33 @@ public List<GameRelease> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<GameRelease> fetchReleases() {
final List<GameRelease> 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<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@ public class RepositoryManager {
private final Set<GameRelease> 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<ReleaseRepository> 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<ReleaseRepository> all = Sets.newHashSet(
legacyEngineNightly, legacyEngineStable,
legacyOmegaNightly, legacyOmegaStable,
omegaNightly, omegaStable);

releases = fetchReleases(all);
}
Expand Down

0 comments on commit b29a447

Please sign in to comment.