#7 - Added support to update project information on spring.io.

This commit introduces a command "sagan update …" to take a comma-separated list of release train names that are supported. The implementation will find the latest releases by inspecting the Git tags and the creating the necessary project metadata information. The code will then wipe the existing metadata and PUT the current information to the server.
This commit is contained in:
Oliver Gierke
2017-07-26 17:25:00 +02:00
parent 4f90f1ec12
commit 4cd460fa98
22 changed files with 1387 additions and 65 deletions

View File

@@ -22,6 +22,7 @@ import lombok.Getter;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.util.Assert;
/**
@@ -34,8 +35,8 @@ public class MavenArtifact {
private static final GroupId GROUP_ID = GroupId.of("org.springframework.data");
private final ModuleIteration module;
private final Repository repository;
private final Project project;
private final @Getter Repository repository;
private final @Getter ArtifactVersion version;
/**
@@ -47,11 +48,22 @@ public class MavenArtifact {
Assert.notNull(module, "Module iteration must not be null!");
this.module = module;
this.project = module.getModule().getProject();
this.repository = new Repository(module.getIteration());
this.version = ArtifactVersion.of(module);
}
public MavenArtifact(Project project, ArtifactVersion version) {
this.project = project;
this.repository = new Repository(version);
this.version = version;
}
public String getGroupId() {
return GROUP_ID.getValue();
}
/**
* Returns the Maven artifact identifier.
*
@@ -59,8 +71,9 @@ public class MavenArtifact {
*/
public String getArtifactId() {
String artifactId = String.format("spring-data-%s", module.getProject().getName().toLowerCase());
return REST.equals(module.getProject()) ? artifactId.concat("-webmvc") : artifactId;
String artifactId = String.format("spring-data-%s", project.getName().toLowerCase());
return REST.equals(project) ? artifactId.concat("-webmvc") : artifactId;
}
public ArtifactVersion getNextDevelopmentVersion() {

View File

@@ -17,6 +17,7 @@ package org.springframework.data.release.build;
import lombok.Value;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Iteration;
import org.springframework.util.Assert;
@@ -40,6 +41,14 @@ public class Repository {
this.url = BASE.concat(iteration.isPublic() ? "release" : "milestone");
}
public Repository(ArtifactVersion version) {
String suffix = getSuffixFor(version);
this.id = ID_BASE.concat(suffix);
this.url = BASE.concat(suffix);
}
public String getSnapshotId() {
return ID_BASE.concat("snapshot");
}
@@ -47,4 +56,21 @@ public class Repository {
public String getSnapshotUrl() {
return BASE.concat("snapshot");
}
private static String getSuffixFor(ArtifactVersion version) {
if (version.isSnapshotVersion()) {
return "snapshot";
}
if (version.isMilestoneVersion()) {
return "milestone";
}
if (version.isReleaseVersion()) {
return "release";
}
throw new IllegalArgumentException(String.format("Unsupported ArtifactVersion %s!", version));
}
}

View File

@@ -18,22 +18,17 @@ package org.springframework.data.release.issues;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;
import org.springframework.data.release.Streamable;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ListWrapperCollector;
import org.springframework.util.StringUtils;
/**
@@ -115,56 +110,6 @@ public class Tickets implements Streamable<Ticket> {
* @return
*/
public static Collector<? super Ticket, ?, Tickets> toTicketsCollector() {
return new Collector<Ticket, List<Ticket>, Tickets>() {
/*
* (non-Javadoc)
* @see java.util.stream.Collector#supplier()
*/
@Override
public Supplier<List<Ticket>> supplier() {
return ArrayList::new;
}
/*
* (non-Javadoc)
* @see java.util.stream.Collector#accumulator()
*/
@Override
public BiConsumer<List<Ticket>, Ticket> accumulator() {
return List::add;
}
/*
* (non-Javadoc)
* @see java.util.stream.Collector#combiner()
*/
@Override
public BinaryOperator<List<Ticket>> combiner() {
return (left, right) -> {
left.addAll(right);
return left;
};
}
/*
* (non-Javadoc)
* @see java.util.stream.Collector#finisher()
*/
@Override
public Function<List<Ticket>, Tickets> finisher() {
return tickets -> new Tickets(tickets);
}
/*
* (non-Javadoc)
* @see java.util.stream.Collector#characteristics()
*/
@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
};
return ListWrapperCollector.collectInto(Tickets::new);
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.release.model;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.util.Assert;
@@ -35,7 +36,7 @@ public class ArtifactVersion implements Comparable<ArtifactVersion> {
SNAPSHOT_SUFFIX);
private final Version version;
private final String suffix;
private final @Getter String suffix;
/**
* Creates a new {@link ArtifactVersion} from the given logical {@link Version}.
@@ -102,6 +103,10 @@ public class ArtifactVersion implements Comparable<ArtifactVersion> {
return new ArtifactVersion(version, iteration.getName());
}
public boolean isVersionWithin(Version version) {
return this.version.toMajorMinorBugfix().startsWith(version.toString());
}
/**
* Returns the release version for the current one.
*
@@ -138,6 +143,14 @@ public class ArtifactVersion implements Comparable<ArtifactVersion> {
return suffix.matches(MILESTONE_SUFFIX);
}
public boolean isSnapshotVersion() {
return suffix.matches(SNAPSHOT_SUFFIX);
}
public boolean isBugFixVersion() {
return isReleaseVersion() && !version.toMajorMinorBugfix().endsWith("0");
}
/**
* Returns the next development version to be used for the current release version, which means next minor for GA
* versions and next bug fix for service releases. Will return the current version as snapshot otherwise.
@@ -172,6 +185,19 @@ public class ArtifactVersion implements Comparable<ArtifactVersion> {
return suffix.equals(SNAPSHOT_SUFFIX) ? this : new ArtifactVersion(version, SNAPSHOT_SUFFIX);
}
public String getReleaseTrainSuffix() {
if (isSnapshotVersion() || isMilestoneVersion()) {
return suffix;
}
if (isBugFixVersion()) {
return "SR" + version.getBugfix();
}
return "GA";
}
/*
* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)

View File

@@ -31,6 +31,8 @@ import org.springframework.util.Assert;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class Password implements Masked {
public static Password NONE = new Password("");
@Getter(AccessLevel.NONE) String value;
/**

View File

@@ -1,5 +1,7 @@
package org.springframework.data.release.model;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@@ -11,6 +13,7 @@ import org.springframework.util.StringUtils;
*
* @author Oliver Gierke
*/
@Getter
public class Version implements Comparable<Version> {
private final int major;

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import net.minidev.json.JSONArray;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.utils.Logger;
import org.springframework.web.client.RestOperations;
import com.jayway.jsonpath.JsonPath;
/**
* Sagan client to interact with the Sagan instance defined through {@link SaganProperties}.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
class DefaultSaganClient implements SaganClient {
RestOperations operations;
SaganProperties properties;
Logger logger;
/*
* (non-Javadoc)
* @see org.springframework.data.release.sagan.SaganClient#getProjectMetadata(org.springframework.data.release.sagan.MaintainedVersion)
*/
@Override
public String getProjectMetadata(MaintainedVersion version) {
URI resource = properties.getProjectMetadataResource(version);
logger.log(version.getProject(), "Getting project metadata for version %s from %s…", version.getVersion(),
resource);
return operations.getForObject(resource, String.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.sagan.SaganClient#getProjectMetadata(org.springframework.data.release.model.Project)
*/
@Override
public String getProjectMetadata(Project project) {
URI resource = properties.getProjectMetadataResource(project);
logger.log(project, "Getting project metadata from %s…", resource);
return operations.getForObject(resource, String.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.sagan.SaganClient#updateProjectMetadata(org.springframework.data.release.model.Project, java.util.List)
*/
@Override
public void updateProjectMetadata(Project project, MaintainedVersions versions) {
URI resource = properties.getProjectMetadataResource(project);
String versionsString = versions.stream()//
.map(MaintainedVersion::getVersion)//
.map(Object::toString) //
.collect(Collectors.joining(", "));
logger.log(project, "Updating project metadata to %s via %s…", versionsString, resource);
// Delete all existing versions first
Arrays.stream(JsonPath.compile("$..version").<JSONArray> read(getProjectMetadata(project)).toArray())//
.map(version -> properties.getProjectMetadataResource(project, version.toString()))//
.peek(uri -> logger.log(project, "Deleting existing project metadata at %s…", uri)) //
.forEach(uri -> operations.delete(uri));
logger.log(project, "Writing project metadata for versions %s!", versionsString);
// Write new ones
List<ProjectMetadata> payload = versions.stream() //
.map(version -> new ProjectMetadata(version, versions)) //
.collect(Collectors.toList());
operations.put(resource, payload);
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.utils.Logger;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectWriter;
/**
* Dummy implementation of {@link SaganClient} to just dump the request payloads into {@link System#out} instead of
* communicating with a real server.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
class DummySaganClient implements SaganClient {
Logger logger;
ObjectWriter mapper;
/*
* (non-Javadoc)
* @see org.springframework.data.release.sagan.SaganClient#updateProjectMetadata(org.springframework.data.release.model.Project, org.springframework.data.release.sagan.MaintainedVersions)
*/
@Override
public void updateProjectMetadata(Project project, MaintainedVersions versions) {
logger.log(project, "Updating released version on Sagan to %s!", versions);
List<ProjectMetadata> payload = versions.stream() //
.map(it -> new ProjectMetadata(it, versions)) //
.collect(Collectors.toList());
try {
System.out.println(mapper.writeValueAsString(payload));
} catch (JsonProcessingException o_O) {
throw new RuntimeException(o_O);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.sagan.SaganClient#getProjectMetadata(org.springframework.data.release.sagan.MaintainedVersion)
*/
@Override
public String getProjectMetadata(MaintainedVersion version) {
try {
return mapper.writeValueAsString(new ProjectMetadata(version, MaintainedVersions.of(version)));
} catch (JsonProcessingException o_O) {
throw new RuntimeException(o_O);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.sagan.SaganClient#getProjectMetadata(org.springframework.data.release.model.Project)
*/
@Override
public String getProjectMetadata(Project project) {
return null;
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import lombok.Value;
import java.util.Comparator;
import java.util.stream.Stream;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Module;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Train;
import org.springframework.util.Assert;
/**
* Represents a maintained project version to be represented in Sagan.
*
* @author Oliver Gierke
*/
@Value(staticConstructor = "of")
class MaintainedVersion implements Comparable<MaintainedVersion> {
Project project;
ArtifactVersion version;
Train train;
/**
* Creates a new {@link MaintainedVersion} representing the snaphot version of the given {@link Module} and
* {@link Train}.
*
* @param module must not be {@literal null}.
* @param train must not be {@literal null}.
* @return
*/
public static MaintainedVersion snapshot(Module module, Train train) {
Assert.notNull(module, "Module must not be null!");
Assert.notNull(train, "Train must not be null!");
ArtifactVersion snapshotVersion = ArtifactVersion.of(module.getVersion()).getSnapshotVersion();
return MaintainedVersion.of(module.getProject(), snapshotVersion, train);
}
/**
* Returns a {@link Stream} of {@link MaintainedVersion} that are related to the current one. In case the current one
* is not a snapshot version in the first place, the next development version is added, too.
*
* @return
*/
Stream<MaintainedVersion> all() {
return version.isSnapshotVersion() ? Stream.of(this) : Stream.of(nextDevelopmentVersion(), this);
}
/**
* Creates the {@link MaintainedVersion} for the next development version of the current one.
*
* @return
*/
MaintainedVersion nextDevelopmentVersion() {
return MaintainedVersion.of(project, version.getNextDevelopmentVersion(), train);
}
/*
* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(MaintainedVersion o) {
return Comparator.comparing(MaintainedVersion::getVersion).compare(this, o);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return String.format("%s - %s - %s", project.getName(), train.getName(), version);
}
}

View File

@@ -0,0 +1,93 @@
/*
* Licensed 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.springframework.data.release.sagan;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.release.Streamable;
import org.springframework.data.release.model.ArtifactVersion;
/**
* Wrapper type for a {@link List} of {@link MaintainedVersion}.
*
* @author Oliver Gierke
*/
@ToString
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class MaintainedVersions implements Streamable<MaintainedVersion> {
private final List<MaintainedVersion> versions;
/**
* Creates a new {@link MaintainedVersions} with the given {@link MaintainedVersion}s in descending order (more recent
* versions first).
*
* @param versions must not be {@literal null}.
* @return
*/
static MaintainedVersions of(List<MaintainedVersion> versions) {
return new MaintainedVersions(versions.stream()//
.sorted(Comparator.reverseOrder())//
.collect(Collectors.toList()));
}
/**
* Creates a new {@link MaintainedVersions} with the given {@link MaintainedVersion}s in descending order (more recent
* versions first).
*
* @param versions must not be {@literal null}.
* @return
*/
static MaintainedVersions of(MaintainedVersion... versions) {
return MaintainedVersions.of(Arrays.asList(versions));
}
/**
* Returns whether the given {@link MaintainedVersion} is the main version of the current set.
*
* @param version must not be {@literal null}.
* @return
*/
boolean isMainVersion(MaintainedVersion version) {
if (!version.getVersion().isReleaseVersion()) {
return false;
}
return versions.stream() //
.map(it -> it.getVersion()) //
.filter(ArtifactVersion::isReleaseVersion) //
.findFirst() //
.map(it -> it.equals(version.getVersion())) //
.orElse(false);
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<MaintainedVersion> iterator() {
return versions.iterator();
}
}

View File

@@ -0,0 +1,193 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import java.util.Locale;
import org.springframework.data.release.build.MavenArtifact;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Projects;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
/**
* Simple value object to create payloads to update project metadata in Sagan.
*
* @author Oliver Gierke
*/
@JsonInclude(Include.NON_NULL)
class ProjectMetadata {
private static String DOCS_BASE = "https://docs.spring.io/spring-data/%s/docs/{version}";
private static String DOCS = DOCS_BASE.concat("/reference/html/");
private static String JAVADOC = DOCS_BASE.concat("/api");
private final MaintainedVersions versions;
private final MaintainedVersion version;
private final MavenArtifact artifact;
/**
* Creates a new {@link ProjectMetadata} instace from the given {@link MaintainedVersion} in the context of the
* {@link MaintainedVersions}.
*
* @param version must not be {@literal null}.
* @param versions must not be {@literal null}.
*/
public ProjectMetadata(MaintainedVersion version, MaintainedVersions versions) {
Assert.notNull(version, "MaintainedVersion must not be null!");
Assert.notNull(versions, "MaintainedVersions must not be null!");
this.version = version;
this.versions = versions;
this.artifact = new MavenArtifact(version.getProject(), version.getVersion());
}
/**
* Returns the release status used on Sagan.
*
* @return
*/
public String getReleaseStatus() {
ArtifactVersion artifactVersion = version.getVersion();
if (artifactVersion.isReleaseVersion()) {
return "GENERAL_AVAILABILITY";
}
if (artifactVersion.isMilestoneVersion()) {
return "PRERELEASE";
}
if (artifactVersion.isSnapshotVersion()) {
return "SNAPSHOT";
}
throw new IllegalStateException();
}
/**
* Returns the group identifier of the release.
*
* @return
*/
public String getGroupId() {
return artifact.getGroupId();
}
/**
* Returns the artifact identifier.
*
* @return
*/
public String getArtifactId() {
if (Projects.BUILD.equals(version.getProject())) {
return "spring-data-releasetrain";
}
return artifact.getArtifactId();
}
/**
* Returns whether the version is the most current one.
*
* @return
*/
public Boolean getCurrent() {
return versions.isMainVersion(version) ? true : null;
}
/**
* Returns the reference documentation URL for non-snapshot versions and not the build project.
*
* @return
*/
public String getRefDocUrl() {
return version.getVersion().isSnapshotVersion() || Projects.BUILD.equals(version.getProject()) //
? "" //
: String.format(DOCS, version.getProject().getName().toLowerCase(Locale.US));
}
/**
* Returns the JavaDoc URL for non-snapshot versions and not the build project.
*
* @return
*/
public String getApiDocUrl() {
return version.getVersion().isSnapshotVersion() || Projects.BUILD.equals(version.getProject()) //
? "" //
: String.format(JAVADOC, version.getProject().getName().toLowerCase(Locale.US));
}
public Repository getRepository() {
return new Repository();
}
/**
* Return the version to use. For the build project thats the release train name, for everything else the artifact
* version.
*
* @return
*/
public String getVersion() {
if (Projects.BUILD.equals(version.getProject())) {
return String.format("%s-%s", version.getTrain().getName(), version.getVersion().getReleaseTrainSuffix());
}
return version.getVersion().toString();
}
public class Repository {
public String getId() {
return artifact.getRepository().getId();
}
public boolean isSnapshotsEnabled() {
return version.getVersion().isSnapshotVersion();
}
public String getUrl() {
return artifact.getRepository().getUrl();
}
public String getName() {
String result = "Spring ";
if (version.getVersion().isMilestoneVersion()) {
return result.concat("Milestones");
}
if (version.getVersion().isReleaseVersion()) {
return result.concat("Releases");
}
if (isSnapshotsEnabled()) {
return result.concat("Snapshots");
}
throw new IllegalStateException();
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import org.springframework.data.release.model.Project;
/**
* Interface to abstract the actual interaction with the Sagan server.
*
* @author Oliver Gierke
*/
interface SaganClient {
/**
* Returns the project metadata for the given {@link MaintainedVersion}.
*
* @param version must not be {@literal null}.
* @return
*/
String getProjectMetadata(MaintainedVersion version);
/**
* Returns all project release metadata for the given {@link Project}.
*
* @param project must not be {@literal null}.
* @return
*/
String getProjectMetadata(Project project);
/**
* Updates the project metadata for the given {@link Project} to the given {@link MaintainedVersions}.
*
* @param project must not be {@literal null}.
* @param versions must not be {@literal null}.
*/
void updateProjectMetadata(Project project, MaintainedVersions versions);
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.stereotype.Component;
/**
* Operations for Sagan interaction.
*
* @author Oliver Gierke
*/
@Component
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
class SaganCommands extends TimedCommand {
SaganOperations sagan;
@CliCommand("sagan update")
public void updateProjectInformation(@CliOption(key = "", mandatory = true) String trains) {
sagan.updateProjectMetadata(Stream.of(trains.split(","))//
.map(train -> ReleaseTrains.getTrainByName(train)) //
.collect(Collectors.toList()));
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.utils.Logger;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.RestTemplate;
/**
* Configuration for the Sagan interaction subsystem.
*
* @author Oliver Gierke
*/
@Configuration
class SaganConfiguration {
@Autowired SaganProperties properties;
@Autowired Logger logger;
@Bean
public SaganOperations saganOperations(GitOperations operations) {
return new SaganOperations(operations, saganClient(), logger);
}
@Bean
SaganClient saganClient() {
return new DefaultSaganClient(saganRestTemplate(), properties, logger);
// return new DummySaganClient(logger, new ObjectMapper().writerWithDefaultPrettyPrinter());
}
@Bean
RestTemplate saganRestTemplate() {
RestTemplate template = new RestTemplate();
template.setInterceptors(Arrays.asList(new AuthenticatingClientHttpRequestInterceptor(properties)));
return template;
}
@RequiredArgsConstructor
private static class AuthenticatingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private final @NonNull SaganProperties properties;
/*
* (non-Javadoc)
* @see org.springframework.http.client.ClientHttpRequestInterceptor#intercept(org.springframework.http.HttpRequest, byte[], org.springframework.http.client.ClientHttpRequestExecution)
*/
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
request.getHeaders().add(HttpHeaders.AUTHORIZATION, properties.getCredentials().toString());
return execution.execute(request, body);
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.git.Tag;
import org.springframework.data.release.model.Module;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.Version;
import org.springframework.data.release.utils.ListWrapperCollector;
import org.springframework.data.release.utils.Logger;
import org.springframework.util.Assert;
/**
* @author Oliver Gierke
*/
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
class SaganOperations {
private static final List<Project> TO_FILTER = Arrays.asList(Projects.COMMONS, //
Projects.GEODE, //
Projects.KEY_VALUE);
GitOperations git;
SaganClient client;
Logger logger;
/**
* Updates the project metadata for the modules in the given release {@link Train}s.
*
* @param trains must not be {@literal null}.
*/
void updateProjectMetadata(Train... trains) {
Assert.notNull(trains, "Trains must not be null!");
updateProjectMetadata(Arrays.asList(trains));
}
void updateProjectMetadata(List<Train> trains) {
Assert.notNull(trains, "Trains must not be null!");
findVersions(trains).forEach(client::updateProjectMetadata);
}
/**
* Returns all {@link MaintainedVersions} grouped by {@link Project} for the given release {@link Train}.
*
* @param trains must not be {@literal null}.
* @return will never be {@literal null}.
*/
Map<Project, MaintainedVersions> findVersions(Train... trains) {
Assert.notNull(trains, "Trains must not be null!");
return findVersions(Arrays.asList(trains));
}
Map<Project, MaintainedVersions> findVersions(List<Train> trains) {
Assert.notNull(trains, "Trains must not be null!");
return trains.stream() //
.flatMap(train -> train.stream()//
.filter(module -> !TO_FILTER.contains(module.getProject())) //
.map(module -> getLatestVersion(module, train)) //
.flatMap(MaintainedVersion::all)) //
.collect(
Collectors.groupingBy(it -> it.getProject(), ListWrapperCollector.collectInto(MaintainedVersions::of)));
}
private MaintainedVersion getLatestVersion(Module module, Train train) {
Project project = module.getProject();
MaintainedVersion version = git.getTags(project).stream()//
.filter(tag -> matches(tag, module.getVersion())) //
.sorted(Comparator.reverseOrder()) //
.findFirst() //
.flatMap(tag -> tag.toArtifactVersion()) //
.map(it -> MaintainedVersion.of(module.getProject(), it, train)) //
.orElseGet(() -> MaintainedVersion.snapshot(module, train));
logger.log(project, "Found version %s for train %s!", version.getVersion(), train.getName());
return version;
}
/**
* Returns whether the given {@link Tag} is one that logically belongs to the given version.
*
* @param tag must not be {@literal null}.
* @param version must not be {@literal null}.
* @return
*/
private static boolean matches(Tag tag, Version version) {
return tag.toArtifactVersion()//
.map(it -> it.isVersionWithin(version))//
.orElse(false);
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import lombok.Setter;
import java.net.URI;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.release.model.Password;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.utils.HttpBasicCredentials;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.util.UriTemplate;
/**
* Configuration properties for the Sagan instance to talk to.
*
* @author Oliver Gierke
*/
@Component
@ConfigurationProperties(prefix = "sagan")
class SaganProperties {
private static String SAGAN_PROJECT_METADATA = "https://spring.io/project_metadata/{project}/releases";
private static String SAGAN_PROJECT_VERSION_METADATA = SAGAN_PROJECT_METADATA.concat("/{version}");
@Setter String key;
/**
* Returns the {@link HttpBasicCredentials} to be used when talking to the server.
*
* @return
*/
HttpBasicCredentials getCredentials() {
return new HttpBasicCredentials(key, Password.NONE);
}
/**
* Returns the URI to the resource exposing the project metadata for the given {@link Project}.
*
* @param project must not be {@literal null}.
* @return
*/
URI getProjectMetadataResource(Project project) {
Assert.notNull(project, "Project must not be null!");
return new UriTemplate(SAGAN_PROJECT_METADATA).expand(getProjectPathSegment(project));
}
/**
* Returns the URI to the resource exposing the project metadata for the given {@link Project} and version
* {@link String}.
*
* @param project must not be {@literal null}.
* @param version must not be {@literal null}.
* @return
*/
URI getProjectMetadataResource(Project project, String version) {
Assert.notNull(project, "Project must not be null!");
Assert.hasText(version, "Version must not be null!");
return new UriTemplate(SAGAN_PROJECT_VERSION_METADATA).expand(getProjectPathSegment(project), version);
}
/**
* Returns the {@link URI} to the resource exposing the project metadata for the given {@link MaintainedVersion}.
*
* @param version must not be {@literal null}.
* @return
*/
URI getProjectMetadataResource(MaintainedVersion version) {
return getProjectMetadataResource(version.getProject(), version.getVersion().toString());
}
private static String getProjectPathSegment(Project project) {
return Projects.BUILD.equals(project) ? "spring-data" : project.getFolderName();
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.release.utils;
import lombok.NonNull;
import lombok.Value;
import java.nio.charset.StandardCharsets;
@@ -28,8 +29,8 @@ import org.springframework.data.release.model.Password;
@Value
public class HttpBasicCredentials {
private final String username;
private final Password password;
private final @NonNull String username;
private final @NonNull Password password;
/*
* (non-Javadoc)

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import org.springframework.util.Assert;
/**
* Base class to easily create a {@link Collector} to create a collection wrapper type.
*
* @author Oliver Gierke
*/
public abstract class ListWrapperCollector<T, S> implements Collector<T, List<T>, S> {
/**
* Creates a new {@link Collector} to eventually apply the given function to turn the {@link List} of elements into a
* wrapper type.
*
* @param function must not be {@literal null}.
* @return
*/
public static <T, S> ListWrapperCollector<T, S> collectInto(Function<List<T>, S> function) {
Assert.notNull(function, "Finisher function must not be null!");
return new ListWrapperCollector<T, S>() {
/*
* (non-Javadoc)
* @see java.util.stream.Collector#finisher()
*/
@Override
public Function<List<T>, S> finisher() {
return function;
}
};
}
/*
* (non-Javadoc)
* @see java.util.stream.Collector#supplier()
*/
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
/*
* (non-Javadoc)
* @see java.util.stream.Collector#accumulator()
*/
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
/*
* (non-Javadoc)
* @see java.util.stream.Collector#combiner()
*/
@Override
public BinaryOperator<List<T>> combiner() {
return (left, right) -> {
left.addAll(right);
return left;
};
}
/*
* (non-Javadoc)
* @see java.util.stream.Collector#characteristics()
*/
@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import static org.assertj.core.api.Assertions.*;
import org.junit.Test;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
/**
* Unit tests for {@link MaintainedVersion}.
*
* @author Oliver Gierke
*/
public class MaintainedVersionUnitTests {
@Test
public void newerVersionIsOrderedFirst() {
MaintainedVersion ingalls = MaintainedVersion.of(Projects.COMMONS, ArtifactVersion.of("1.13.0.RELEASE"),
ReleaseTrains.INGALLS);
MaintainedVersion hopper = MaintainedVersion.of(Projects.COMMONS, ArtifactVersion.of("1.12.0.RELEASE"),
ReleaseTrains.HOPPER);
assertThat(ingalls.compareTo(hopper)).isGreaterThan(0);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import static org.assertj.core.api.Assertions.*;
import org.junit.Test;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
/**
* Unit tests for {@link MaintainedVersions}.
*
* @author Oliver Gierke
*/
public class MaintainedVersionsUnitTests {
@Test
public void considersMostRecentReleaseVersionTheMainOne() {
MaintainedVersion ingalls = MaintainedVersion.of(Projects.COMMONS, ArtifactVersion.of("1.13.0.RELEASE"),
ReleaseTrains.INGALLS);
MaintainedVersion hopper = MaintainedVersion.of(Projects.COMMONS, ArtifactVersion.of("1.12.0.RELEASE"),
ReleaseTrains.HOPPER);
MaintainedVersions versions = MaintainedVersions.of(ingalls, hopper);
assertThat(versions.isMainVersion(ingalls)).isTrue();
assertThat(versions.isMainVersion(hopper)).isFalse();
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import org.junit.Test;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
/**
* Tests for serialization of {@link ProjectMetadata}.
*
* @author Oliver Gierke
*/
public class ProjectMetadataSerializationTests {
@Test
public void serializesMaintainedVersionsIntoProjectMetadata() throws Exception {
ObjectWriter mapper = new ObjectMapper().writerWithDefaultPrettyPrinter();
MaintainedVersion kay = MaintainedVersion.of(Projects.COMMONS, ArtifactVersion.of("2.0.0.RC1"), ReleaseTrains.KAY);
MaintainedVersion ingalls = MaintainedVersion.of(Projects.COMMONS, ArtifactVersion.of("1.13.5.RELEASE"),
ReleaseTrains.INGALLS);
MaintainedVersion ingallsSnapshot = ingalls.nextDevelopmentVersion();
MaintainedVersion hopper = MaintainedVersion.of(Projects.COMMONS, ArtifactVersion.of("1.12.8.RELEASE"),
ReleaseTrains.HOPPER);
MaintainedVersions versions = MaintainedVersions.of(kay, ingalls, ingallsSnapshot, hopper);
System.out.println(mapper.writeValueAsString(new ProjectMetadata(kay, versions)));
System.out.println(mapper.writeValueAsString(new ProjectMetadata(ingallsSnapshot, versions)));
System.out.println(mapper.writeValueAsString(new ProjectMetadata(ingalls, versions)));
System.out.println(mapper.writeValueAsString(new ProjectMetadata(hopper, versions)));
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed 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.springframework.data.release.sagan;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.release.AbstractIntegrationTests;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
/**
* Integration tests for {@link SaganOperations}.
*
* @author Oliver Gierke
*/
public class SaganOperationsIntegrationTests extends AbstractIntegrationTests {
@Autowired SaganOperations sagan;
@Autowired SaganClient client;
@Test
public void detectVersionsToUpdate() {
sagan.findVersions(ReleaseTrains.LOVELACE, ReleaseTrains.KAY, ReleaseTrains.INGALLS, ReleaseTrains.HOPPER)
.forEach((project, versions) -> {
System.out.println(project.getName());
System.out.println("-----");
versions.forEach(version -> {
String output = version.toString();
System.out.println(versions.isMainVersion(version) ? output.concat(" (main release)") : output);
});
System.out.println();
});
}
@Test
public void updateVersions() {
sagan.updateProjectMetadata(ReleaseTrains.KAY, ReleaseTrains.INGALLS, ReleaseTrains.HOPPER);
}
@Test
public void updateJpa() {
MaintainedVersions versions = sagan.findVersions(ReleaseTrains.KAY, ReleaseTrains.INGALLS, ReleaseTrains.HOPPER)
.get(Projects.JPA);
System.out.println(versions);
client.updateProjectMetadata(Projects.JPA, versions);
}
@Test
public void getJpa() {
System.out.println(client.getProjectMetadata(Projects.JPA));
}
@Test
public void updateBuild() {
MaintainedVersions versions = sagan.findVersions(ReleaseTrains.KAY, ReleaseTrains.INGALLS, ReleaseTrains.HOPPER)
.get(Projects.BUILD);
client.updateProjectMetadata(Projects.BUILD, versions);
}
}