#159 - Dependency upgrade automation.

Add commands to check for dependency upgrades and to perform an upgrade for Spring Data Build.
This commit is contained in:
Mark Paluch
2020-11-13 16:26:55 +01:00
parent 2a54dd3ea6
commit 9ee733d12c
21 changed files with 1720 additions and 28 deletions

3
.gitignore vendored
View File

@@ -7,4 +7,5 @@ target/
application-local.properties
spring-shell.log
.idea/
*.iml
*.iml
dependency-upgrade-*.properties

View File

@@ -83,3 +83,16 @@ $ tracker archive $trainIteration.previous
$ github update labels $project
```
#### Dependency Upgrade
`ProjectDependencies` contains a per-project configuration of dependencies.
Workflow:
* Check for dependency upgrades `$ dependency check $trainIteration`
Reports upgradable dependencies for Build and Modules and creates `dependency-upgrade-build.properties` file.
Edit `dependency-upgrade-build.properties` to specify the dependency version to upgrade. Removing a line will omit that dependency upgrade.
* Apply dependency upgrade with `$ dependency upgrade $trainIteration`. Applies dependency upgrades currently only to Spring Data Build.
* Report store-specific dependencies to Spring Boot's current upgrade ticket ([sample](https://github.com/spring-projects/spring-boot/issues/24036)) `$ dependency report $trainIteration`

View File

@@ -48,6 +48,9 @@ public interface Pom {
@XBWrite("/project/properties/{0}")
void setProperty(String property, @XBValue ArtifactVersion value);
@XBWrite("/project/properties/{0}")
void setProperty(String property, @XBValue String value);
@XBWrite("/project/repositories/repository[id=\"{0}\"]/id")
void setRepositoryId(String oldId, @XBValue String newId);
@@ -56,13 +59,16 @@ public interface Pom {
/**
* Sets the version of the dependency with the given artifact identifier to the given {@link ArtifactVersion}.
*
*
* @param artifactId
* @param version
*/
@XBWrite("/project/dependencies/dependency[artifactId=\"{0}\"]/version")
Pom setDependencyVersion(String artifactId, @XBValue ArtifactVersion version);
@XBRead("/project/dependencies/dependency[artifactId=\"{0}\"]/version")
String getDependencyVersion(String artifactId);
@XBWrite("/project/dependencyManagement/dependencies/dependency[artifactId=\"{0}\"]/version")
Pom setDependencyManagementVersion(String artifactId, @XBValue ArtifactVersion version);

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2020 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.dependency;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.ReflectionUtils;
/**
* @author Mark Paluch
*/
public class Dependencies {
static final List<Dependency> dependencies = new ArrayList<>();
public static final Dependency APT = Dependency.of("APT", "com.mysema.maven:apt-maven-plugin");
public static final Dependency ASPECTJ = Dependency.of("AspectJ", "org.aspectj:aspectjrt");
public static final Dependency ASSERTJ = Dependency.of("AssertJ", "org.assertj:assertj-core");
public static final Dependency JACKSON = Dependency.of("Jackson", "com.fasterxml.jackson:jackson-bom");
public static final Dependency JACOCO = Dependency.of("Jacoco", "org.jacoco:jacoco");
public static final Dependency JODA_TIME = Dependency.of("Joda Time", "joda-time:joda-time");
public static final Dependency JUNIT5 = Dependency.of("JUnit", "org.junit:junit-bom");
public static final Dependency JUNIT4 = Dependency.of("JUnit", "junit:junit");
public static final Dependency KOTLIN = Dependency.of("Kotlin", "org.jetbrains.kotlin:kotlin-bom");
public static final Dependency KOTLIN_COROUTINES = Dependency.of("Kotlin Coroutines",
"org.jetbrains.kotlinx:kotlinx-coroutines-bom");
public static final Dependency MOCKITO = Dependency.of("Mockito", "org.mockito:mockito-core");
public static final Dependency MOCKK = Dependency.of("Mockk", "io.mockk:mockk");
public static final Dependency QUERYDSL = Dependency.of("Querydsl", "com.querydsl:querydsl-jpa");
public static final Dependency PROJECT_REACTOR = Dependency.of("Project Reactor", "io.projectreactor:reactor-bom");
public static final Dependency RXJAVA1 = Dependency.of("RxJava", "io.reactivex:rxjava");
public static final Dependency RXJAVA2 = Dependency.of("RxJava", "io.reactivex.rxjava2:rxjava");
public static final Dependency RXJAVA3 = Dependency.of("RxJava", "io.reactivex.rxjava3:rxjava");
public static final Dependency RXJAVA_RS = Dependency.of("RxJava Reactive Streams",
"io.reactivex:rxjava-reactive-streams");
public static final Dependency SPRING_FRAMEWORK = Dependency.of("Spring Framework",
"org.springframework:spring-core");
public static final Dependency SPRING_HATEOAS = Dependency.of("Spring Hateoas",
"org.springframework.hateoas:spring-hateoas");
public static final Dependency SPRING_PLUGIN = Dependency.of("Spring Plugin",
"org.springframework.plugin:spring-plugin");
public static final Dependency TESTCONTAINERS = Dependency.of("Testcontainers", "org.testcontainers:testcontainers");
public static final Dependency THREE_TEN_BP = Dependency.of("ThreeTenBp", "org.threeten:threetenbp");
public static final Dependency OPEN_WEB_BEANS = Dependency.of("OpenWebBeans", "org.apache.openwebbeans:openwebbeans");
public static final Dependency VAVR = Dependency.of("Vavr", "io.vavr:vavr").excludeVersionStartingWith("1.0.0-alpha");
public static final Dependency XML_BEAM = Dependency.of("XMLBeam", "org.xmlbeam:xmlprojector");
public static final Dependency MONGODB_CORE = Dependency.of("MongoDB", "org.mongodb:mongodb-driver-core");
public static final Dependency MONGODB_LEGACY = Dependency.of("MongoDB", "org.mongodb:mongo-java-driver");
public static final Dependency MONGODB_SYNC = Dependency.of("MongoDB", "org.mongodb:mongodb-driver-sync");
public static final Dependency MONGODB_ASYNC = Dependency.of("MongoDB", "org.mongodb:mongodb-driver-async");
public static final Dependency MONGODB_RS = Dependency.of("MongoDB Reactive Streams",
"org.mongodb:mongodb-driver-reactivestreams");
public static final Dependency LETTUCE = Dependency.of("Lettuce", "io.lettuce:lettuce-core");
public static final Dependency JEDIS = Dependency.of("Jedis", "redis.clients:jedis");
public static final Dependency CASSANDRA_DRIVER3 = Dependency.of("Cassandra Driver",
"com.datastax.cassandra:cassandra-driver-core");
public static final Dependency CASSANDRA_DRIVER4 = Dependency.of("Cassandra Driver",
"com.datastax.oss:java-driver-core");
public static final Dependency NEO4J_OGM = Dependency.of("Neo4j OGM", "org.neo4j:neo4j-ogm-api");
public static final Dependency NEO4J_DRIVER = Dependency.of("Neo4j Driver", "org.neo4j.driver:neo4j-java-driver");
public static final Dependency COUCHBASE = Dependency.of("Couchbase Client", "com.couchbase.client:java-client");
public static final Dependency ELASTICSEARCH = Dependency.of("Elasticsearch",
"org.elasticsearch.client:elasticsearch-rest-high-level-client");
public static final Dependency SPRING_LDAP = Dependency.of("Spring LDAP",
"org.springframework.ldap:spring-ldap-core");
static {
ReflectionUtils.doWithFields(Dependencies.class, field -> {
// ignore dependencies constant
if (field.getName().toLowerCase().equals(field.getName())) {
return;
}
dependencies.add((Dependency) ReflectionUtils.getField(field, null));
});
}
public static Dependency getRequiredByName(String name) {
return dependencies.stream().filter(it -> it.getName().equals(name)).findFirst()
.orElseThrow(() -> new IllegalArgumentException(String.format("No such dependency: %s", name)));
}
public static Dependency getRequiredByArtifactId(String artifactId) {
return dependencies.stream().filter(it -> it.getArtifactId().equals(artifactId)).findFirst()
.orElseThrow(() -> new IllegalArgumentException(String.format("No such dependency: %s", artifactId)));
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2020 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.dependency;
import lombok.Value;
import java.util.function.Predicate;
import org.springframework.util.Assert;
/**
* @author Mark Paluch
*/
@Value
public class Dependency implements Comparable<Dependency> {
String name;
String groupId, artifactId;
Predicate<String> exclusions;
public static Dependency of(String name, String ga) {
Assert.hasText(name, "Name must not be empty");
Assert.hasText(ga, "GroupId/ArtifactId must not be empty");
Assert.isTrue(ga.indexOf(':') != -1, "GroupId/ArtifactId must be in the format of org.group:artifact-id");
String[] parts = ga.split(":");
return new Dependency(name, parts[0], parts[1], it -> false);
}
public Dependency excludeVersionStartingWith(String identifier) {
return new Dependency(name, groupId, artifactId, it -> it.startsWith(identifier) || exclusions.test(it));
}
public boolean shouldInclude(String identifier) {
return !exclusions.test(identifier);
}
@Override
public int compareTo(Dependency o) {
return name.compareTo(o.name);
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2020 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.dependency;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.Logger;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.shell.support.table.Table;
/**
* Shell commands for dependency management.
*
* @author Mark Paluch
*/
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class DependencyCommands extends TimedCommand {
public static final String BUILD_PROPERTIES = "dependency-upgrade-build.properties";
DependencyOperations operations;
GitOperations git;
Logger logger;
@CliCommand(value = "dependency check")
public void check(@CliOption(key = "", mandatory = true) TrainIteration iteration,
@CliOption(key = "all", mandatory = false) Boolean reportAll) throws IOException {
git.checkout(iteration.getTrain());
checkBuildDependencies(iteration, reportAll != null ? reportAll : false);
checkModuleDependencies(iteration, reportAll != null ? reportAll : false);
}
/**
* Retrieve a dependency report for all store modules to be used typically in Spring Boot upgrade tickets.
*
* @param iteration
* @return
*/
@CliCommand(value = "dependency report")
public String report(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
git.checkout(iteration.getTrain());
List<Project> projects = Projects.all().stream()
.filter(it -> it != Projects.BOM && it != Projects.BUILD && it != Projects.COMMONS)
.collect(Collectors.toList());
Map<Dependency, DependencyVersion> dependencies = new TreeMap<>();
for (Project project : projects) {
dependencies.putAll(operations.getCurrentDependencies(project));
}
StringBuilder report = new StringBuilder();
report.append(System.lineSeparator()).append("Project Dependencies Spring Data ")
.append(iteration.getReleaseTrainNameAndVersion()).append(System.lineSeparator())
.append(System.lineSeparator());
dependencies.forEach((dependency, dependencyVersion) -> {
report.append(String.format("* %s (%s:%s): %s", dependency.getName(), dependency.getGroupId(),
dependency.getArtifactId(), dependencyVersion.getIdentifier())).append(System.lineSeparator());
});
return report.toString();
}
@CliCommand(value = "dependency upgrade")
public void check(@CliOption(key = "", mandatory = true) TrainIteration iteration)
throws IOException, InterruptedException {
git.checkout(iteration.getTrain());
logger.log(iteration, "Applying dependency upgrades to Spring Data Build");
Properties properties = loadDependencyUpgrades(iteration);
Map<Dependency, DependencyVersion> dependencyUpgrades = DependencyUpgradeProposals.fromProperties(iteration,
properties);
operations.createUpgradeTickets(iteration, Projects.BUILD, dependencyUpgrades);
operations.upgradeDependencies(iteration, Projects.BUILD, dependencyUpgrades);
}
protected Properties loadDependencyUpgrades(@CliOption(key = "", mandatory = true) TrainIteration iteration)
throws IOException {
if (!Files.exists(Paths.get(BUILD_PROPERTIES))) {
logger.log(iteration, "Cannot upgrade dependencies: " + BUILD_PROPERTIES + " does not exist.");
}
Properties properties = new Properties();
try (FileInputStream fis = new FileInputStream(BUILD_PROPERTIES)) {
properties.load(fis);
}
return properties;
}
private void checkModuleDependencies(TrainIteration iteration, boolean reportAll) throws IOException {
String propertiesFile = "dependency-upgrade-modules.properties";
List<Project> projects = Projects.all().stream().filter(it -> it != Projects.BOM && it != Projects.BUILD)
.collect(Collectors.toList());
DependencyUpgradeProposals proposals = DependencyUpgradeProposals.empty();
for (Project project : projects) {
proposals = proposals.mergeWith(operations.getDependencyUpgradeProposals(project, iteration.getIteration()));
}
Files.write(Paths.get(propertiesFile), proposals.asProperties(iteration).getBytes());
Table summary = proposals.toTable(reportAll);
logger.log(iteration, "Upgrade summary:" + System.lineSeparator() + System.lineSeparator() + summary);
logger.log(iteration, "Upgrade proposals written to " + propertiesFile);
}
private void checkBuildDependencies(TrainIteration iteration, boolean reportAll) throws IOException {
String propertiesFile = BUILD_PROPERTIES;
DependencyUpgradeProposals proposals = operations.getDependencyUpgradeProposals(Projects.BUILD,
iteration.getIteration());
Files.write(Paths.get(propertiesFile), proposals.asProperties(iteration).getBytes());
Table summary = proposals.toTable(reportAll);
logger.log(Projects.BUILD, "Upgrade summary:" + System.lineSeparator() + System.lineSeparator() + summary);
logger.log(iteration, "Upgrade proposals written to " + propertiesFile);
}
}

View File

@@ -0,0 +1,412 @@
/*
* Copyright 2020 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.dependency;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.build.Pom;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.io.Workspace;
import org.springframework.data.release.issues.IssueTracker;
import org.springframework.data.release.issues.Ticket;
import org.springframework.data.release.issues.Tickets;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.data.release.utils.Logger;
import org.springframework.data.util.Streamable;
import org.springframework.http.ResponseEntity;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestOperations;
import org.xmlbeam.ProjectionFactory;
import org.xmlbeam.annotation.XBRead;
import org.xmlbeam.io.XBFileIO;
import org.xmlbeam.io.XBStreamInput;
/**
* Operations for dependency management.
*
* @author Mark Paluch
*/
@CliComponent
@RequiredArgsConstructor
public class DependencyOperations {
public static final Pattern REPO_MAVEN_ORG_DIR_LISTING = Pattern
.compile("<a (?>[^>]+)>([^\\/]+)\\/<\\/a>(?>\\s*)(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2})(?>\\s*)(?>-)");
public static final DateTimeFormatter DIR_LISTING_TIME_FORMAT = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm");
private final ProjectionFactory projectionFactory;
private final GitOperations gitOperations;
private final Workspace workspace;
private final PluginRegistry<IssueTracker, Project> tracker;
private final ExecutorService executor;
private final RestOperations restOperations;
private final Logger logger;
/**
* Obtain dependency upgrade proposals for {@link Project} and {@link Iteration}. Considers dependency upgrade rules
* according to minor/bugfix release increments. Also, SemVer with modifier are only allowed in milestone versions.
*
* @param project
* @param iteration
* @return
*/
public DependencyUpgradeProposals getDependencyUpgradeProposals(Project project, Iteration iteration) {
Map<Dependency, DependencyVersion> currentDependencies = getCurrentDependencies(project);
Map<Dependency, DependencyUpgradeProposal> proposals = Collections.synchronizedMap(new LinkedHashMap<>());
ExecutionUtils.run(executor, Streamable.of(currentDependencies.keySet()), dependency -> {
DependencyVersion currentVersion = currentDependencies.get(dependency);
List<DependencyVersion> versions = getAvailableVersions(dependency);
DependencyUpgradeProposal proposal = getDependencyUpgradeProposal(iteration, currentVersion, versions);
proposals.put(dependency, proposal);
});
return new DependencyUpgradeProposals(proposals);
}
/**
* Ensures there's a upgrade ticket for each dependency to upgrade.
*
* @param iteration
* @param project
* @param dependencyVersions
*/
public void createUpgradeTickets(TrainIteration iteration, Project project,
Map<Dependency, DependencyVersion> dependencyVersions) {
Map<Dependency, DependencyVersion> upgrades = getDependencyUpgradesToApply(project, dependencyVersions);
IssueTracker tracker = this.tracker.getRequiredPluginFor(project);
Tickets tickets = tracker.getTicketsFor(iteration);
upgrades.forEach((dependency, dependencyVersion) -> {
String upgradeTicketSummary = getUpgradeTicketSummary(dependency, dependencyVersion);
Optional<Ticket> upgradeTicket = getDependencyUpgradeTicket(tickets, upgradeTicketSummary);
if (upgradeTicket.isPresent()) {
logger.log(project, "Found upgrade ticket %s", upgradeTicket.get());
} else {
ModuleIteration module = iteration.getModule(project);
logger.log(module, "Creating upgrade ticket for %s", upgradeTicketSummary);
tracker.createTicket(module, upgradeTicketSummary);
}
});
}
/**
* Verifies dependencies to upgrade, applies the upgrade, creates a commit, pushes the repository and resolves the
* upgrade ticket.
*
* @param iteration
* @param project
* @param dependencyVersions
* @throws InterruptedException
*/
public void upgradeDependencies(TrainIteration iteration, Project project,
Map<Dependency, DependencyVersion> dependencyVersions) throws InterruptedException {
Map<Dependency, DependencyVersion> upgrades = getDependencyUpgradesToApply(project, dependencyVersions);
ProjectDependencies dependencies = ProjectDependencies.get(project);
ModuleIteration module = iteration.getModule(project);
if (upgrades.isEmpty()) {
logger.log(module, "No dependency upgrades to apply");
}
IssueTracker tracker = this.tracker.getRequiredPluginFor(project);
Tickets tickets = tracker.getTicketsFor(iteration);
List<Ticket> ticketsToClose = new ArrayList<>();
upgrades.forEach((dependency, dependencyVersion) -> {
String upgradeTicketSummary = getUpgradeTicketSummary(dependency, dependencyVersion);
Ticket upgradeTicket = getDependencyUpgradeTicket(tickets, upgradeTicketSummary).get();
String versionProperty = dependencies.getVersionPropertyFor(dependency);
File pom = getPomFile(project);
update(pom, Pom.class, it -> {
it.setProperty(versionProperty, dependencyVersion.getIdentifier());
});
gitOperations.commit(module, upgradeTicket, upgradeTicketSummary, Optional.empty(), pom);
ticketsToClose.add(upgradeTicket);
});
gitOperations.push(module);
// Allow GitHub to catch up with ticket notifications.
Thread.sleep(1500);
for (Ticket ticket : ticketsToClose) {
tracker.closeTicket(module, ticket);
}
}
private Map<Dependency, DependencyVersion> getDependencyUpgradesToApply(Project project,
Map<Dependency, DependencyVersion> dependencyVersions) {
Map<Dependency, DependencyVersion> currentDependencies = getCurrentDependencies(project);
Map<Dependency, DependencyVersion> upgrades = new LinkedHashMap<>();
currentDependencies.forEach((dependency, dependencyVersion) -> {
DependencyVersion upgradeVersion = dependencyVersions.get(dependency);
if (upgradeVersion == null) {
return;
}
if (upgradeVersion.equals(dependencyVersion)) {
logger.log(project, "Skipping upgrade of %s (%s)", dependency.getName(), dependencyVersion.getIdentifier());
return;
}
upgrades.put(dependency, upgradeVersion);
});
return upgrades;
}
private Optional<Ticket> getDependencyUpgradeTicket(Tickets tickets, String upgradeTicketSummary) {
List<Ticket> upgradeTickets = tickets.filter(it -> it.getSummary().equals(upgradeTicketSummary)).toList();
if (upgradeTickets.size() > 1) {
throw new IllegalStateException("Multiple upgrade tickets found: " + upgradeTickets);
}
return Optional.ofNullable(upgradeTickets.isEmpty() ? null : upgradeTickets.get(0));
}
protected static DependencyUpgradeProposal getDependencyUpgradeProposal(Iteration iteration,
DependencyVersion currentVersion, List<DependencyVersion> allVersions) {
DependencyVersion latestMinor = findLatestMinor(iteration, currentVersion, allVersions);
DependencyVersion latest = findLatest(iteration, allVersions);
List<DependencyVersion> newerVersions = allVersions.stream() //
.sorted() //
.filter(it -> it.compareTo(currentVersion) > 0) //
.collect(Collectors.toList());
return DependencyUpgradeProposal.of(iteration, currentVersion, latestMinor, latest, newerVersions);
}
private static DependencyVersion findLatest(Iteration iteration, List<DependencyVersion> availableVersions) {
return availableVersions.stream().filter(it -> {
if (!iteration.isMilestone() && StringUtils.hasText(it.getModifier())) {
return false;
}
return true;
}).max(DependencyVersion::compareTo).orElseThrow(
() -> new IllegalArgumentException("Cannot determine new latest version from " + availableVersions));
}
private static DependencyVersion findLatestMinor(Iteration iteration, DependencyVersion currentVersion,
List<DependencyVersion> availableVersions) {
return availableVersions.stream().filter(it -> {
if (!iteration.isMilestone() && StringUtils.hasText(it.getModifier())) {
return false;
}
if (it.getVersion() == null || currentVersion.getVersion() == null) {
return false;
}
if (it.getTrainName() != null && currentVersion.getTrainName() != null) {
return it.getTrainName().equals(currentVersion.getTrainName());
}
if (it.getVersion().getMajor() == currentVersion.getVersion().getMajor()
&& it.getVersion().getMinor() == currentVersion.getVersion().getMinor()) {
return true;
}
return false;
}) //
.max(DependencyVersion::compareTo) //
.orElseThrow(
() -> new IllegalArgumentException("Cannot determine new minor version from " + availableVersions));
}
Map<Dependency, DependencyVersion> getCurrentDependencies(Project project) {
if (!ProjectDependencies.containsProject(project)) {
return Collections.emptyMap();
}
File pom = getPomFile(project);
ProjectDependencies dependencies = ProjectDependencies.get(project);
return doWithPom(pom, Pom.class, it -> {
Map<Dependency, DependencyVersion> versions = new LinkedHashMap<>();
for (ProjectDependencies.ProjectDependency projectDependency : dependencies) {
Dependency dependency = projectDependency.getDependency();
if (!((project == Projects.MONGO_DB && projectDependency.getProperty().equals("mongo.reactivestreams"))
|| project == Projects.NEO4J)) {
if (it.getDependencyVersion(dependency.getArtifactId()) == null
&& it.getManagedDependency(dependency.getArtifactId()) == null) {
continue;
}
}
String value = it.getProperty(projectDependency.getProperty());
if (value != null && !value.contains("${")) {
versions.put(dependency, DependencyVersion.of(value));
}
}
return versions;
});
}
private File getPomFile(Project project) {
return workspace.getFile(project == Projects.BUILD ? "parent/pom.xml" : "pom.xml", project);
}
@SneakyThrows
List<DependencyVersion> getAvailableVersions(Dependency dependency) {
String baseUrl = String.format("https://repo1.maven.org/maven2/%s/%s/", dependency.getGroupId().replace('.', '/'),
dependency.getArtifactId());
ResponseEntity<byte[]> mavenMetadata = restOperations.getForEntity(baseUrl + "/maven-metadata.xml", byte[].class);
ResponseEntity<String> directoryListing = restOperations.getForEntity(baseUrl, String.class);
Map<String, LocalDateTime> creationDates = parseCreationDates(directoryListing.getBody());
XBStreamInput io = projectionFactory.io().stream(new ByteArrayInputStream(mavenMetadata.getBody()));
try {
MavenMetadata metadata = io.read(MavenMetadata.class);
return metadata.getVersions().stream().filter(dependency::shouldInclude).map(DependencyVersion::of).map(it -> {
if (creationDates.containsKey(it.getIdentifier())) {
return it.withCreatedAt(creationDates.get(it.getIdentifier()));
}
return it;
}).collect(Collectors.toList());
} catch (Exception o_O) {
throw new RuntimeException(o_O);
}
}
private Map<String, LocalDateTime> parseCreationDates(String body) {
Map<String, LocalDateTime> creationDates = new LinkedHashMap<>();
Matcher matcher = REPO_MAVEN_ORG_DIR_LISTING.matcher(body);
while (matcher.find()) {
String version = matcher.group(1);
LocalDateTime creationDate = LocalDateTime.from(DIR_LISTING_TIME_FORMAT.parse(matcher.group(2)));
creationDates.put(version, creationDate);
}
return creationDates;
}
private <T extends Pom, R> R doWithPom(File file, Class<T> type, Function<T, R> callback) {
XBFileIO io = projectionFactory.io().file(file);
try {
T pom = (T) io.read(type);
return callback.apply(pom);
} catch (Exception o_O) {
throw new RuntimeException(o_O);
}
}
private <T extends Pom> void update(File file, Class<T> type, Consumer<T> callback) {
XBFileIO io = projectionFactory.io().file(file);
try {
T pom = (T) io.read(type);
callback.accept(pom);
io.write(pom);
} catch (Exception o_O) {
throw new RuntimeException(o_O);
}
}
private static String getUpgradeTicketSummary(Dependency dependency, DependencyVersion dependencyVersion) {
return String.format("Upgrade to %s %s", dependency.getName(), dependencyVersion.getIdentifier());
}
public interface MavenMetadata {
@XBRead("/metadata/versioning/versions/version/text()")
List<String> getVersions();
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2020 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.dependency;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.release.model.Iteration;
/**
* @author Mark Paluch
*/
@Value
@RequiredArgsConstructor
class DependencyUpgradeProposal {
DependencyVersion current, latest, latestMinor, proposal;
List<DependencyVersion> newerVersions;
public static DependencyUpgradeProposal of(Iteration iteration, DependencyVersion currentVersion,
DependencyVersion latestMinor, DependencyVersion latest, List<DependencyVersion> newerVersions) {
if (iteration.isServiceIteration()) {
return new DependencyUpgradeProposal(currentVersion, latest, latestMinor, latestMinor, newerVersions);
}
return new DependencyUpgradeProposal(currentVersion, latest, latestMinor, latest, newerVersions);
}
public String getNewVersions(boolean includeAll, boolean includeDate) {
if (includeAll) {
return getNewerVersions().stream().map(dependencyVersion -> {
if (includeDate && dependencyVersion.getCreatedAt() != null) {
return String.format("%s (%s)", dependencyVersion.getIdentifier(),
dependencyVersion.getCreatedAt().toLocalDate());
}
return dependencyVersion.getIdentifier();
}).collect(Collectors.joining(", "));
}
if (latestMinor.toString().equals(latest.toString())) {
return latest.toString();
}
return String.format("%s, %s", latestMinor, latest);
}
public boolean isUpgradeAvailable() {
return !getCurrent().getIdentifier().equals(getProposal().getIdentifier());
}
@Override
public String toString() {
return getProposal().getIdentifier();
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2020 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.dependency;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.fusesource.jansi.Ansi;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.shell.support.table.Table;
import org.springframework.shell.support.table.TableHeader;
/**
* Value object capturing upgrade proposals for each {@link Dependency}.
*
* @author Mark Paluch
*/
public class DependencyUpgradeProposals {
private final Map<Dependency, DependencyUpgradeProposal> proposals;
public DependencyUpgradeProposals(Map<Dependency, DependencyUpgradeProposal> proposals) {
this.proposals = proposals;
}
/**
* Create an empty {@link DependencyUpgradeProposal} object.
*
* @return
*/
public static DependencyUpgradeProposals empty() {
return new DependencyUpgradeProposals(Collections.emptyMap());
}
/**
* Create a new {@link DependencyUpgradeProposal} by merging this and {@code other}.
*
* @param other
* @return
*/
public DependencyUpgradeProposals mergeWith(DependencyUpgradeProposals other) {
Map<Dependency, DependencyUpgradeProposal> proposals = new TreeMap<>(this.proposals);
proposals.putAll(other.proposals);
return new DependencyUpgradeProposals(proposals);
}
/**
* Create a tabular summary including {@link Ansi} escapes.
*
* @param includeAll
* @return
*/
public Table toTable(boolean includeAll) {
Table table = new Table();
table.addHeader(1, new TableHeader("Dependency"));
table.addHeader(2, new TableHeader("Current"));
table.addHeader(3, new TableHeader("Available"));
table.addHeader(4, new TableHeader("Proposed"));
proposals.forEach((dependency, proposal) -> {
boolean updateAvailable = proposal.isUpgradeAvailable();
String s = updateAvailable
? Ansi.ansi().fg(Ansi.Color.MAGENTA).a(proposal.getProposal()).fg(Ansi.Color.GREEN).toString()
: proposal.getProposal().toString();
if (includeAll || updateAvailable) {
table.addRow(dependency.getName(), proposal.getCurrent().toString(), proposal.getNewVersions(includeAll, false),
s);
}
});
return table;
}
/**
* Return the upgrade proposal as {@link java.util.Properties} representation.
*
* @param iteration
* @return
*/
public String asProperties(TrainIteration iteration) {
StringBuilder builder = new StringBuilder();
builder.append("dependency.train=").append(iteration.getTrain().getName()).append(System.lineSeparator());
builder.append("dependency.iteration=").append(iteration.getIteration().getName()).append(System.lineSeparator());
proposals.forEach((dependency, proposal) -> {
boolean updateAvailable = proposal.isUpgradeAvailable();
if (updateAvailable) {
builder.append(System.lineSeparator());
builder.append(String.format("# %s - Available versions: ", dependency.getName()))
.append(proposal.getNewVersions(true, true)).append(System.lineSeparator());
builder.append(
String.format("dependency[%s\\:%s]=%s", dependency.getGroupId(), dependency.getArtifactId(), proposal));
builder.append(System.lineSeparator());
}
});
return builder.toString();
}
/**
* Create a dependency upgrade map by parsing {@link Properties}.
*
* @param iteration
* @param properties
* @return
*/
public static Map<Dependency, DependencyVersion> fromProperties(TrainIteration iteration, Properties properties) {
Pattern keyPattern = Pattern.compile("dependency\\[([a-zA-Z0-9\\-\\.]+):([a-zA-Z0-9\\-\\.]+)\\]");
String verificationTrain = properties.getProperty("dependency.train", "");
String verificationIteration = properties.getProperty("dependency.iteration", "");
if (!verificationTrain.equals(iteration.getTrain().getName())
|| !verificationIteration.equals(iteration.getIteration().getName())) {
throw new IllegalArgumentException(
String.format("Verification failed: Dependency upgrade descriptor reports %s %s", verificationTrain,
verificationIteration));
}
Map<Dependency, DependencyVersion> result = new LinkedHashMap<>();
properties.forEach((k, v) -> {
if ("dependency.train".equals(k) || "dependency.iteration".equals(k)) {
return;
}
Matcher matcher = keyPattern.matcher(k.toString());
if (!matcher.matches()) {
throw new IllegalArgumentException(String.format("Unexpected key: %s", k));
}
String artifactId = matcher.group(2);
Dependency dependency = Dependencies.getRequiredByArtifactId(artifactId);
result.put(dependency, DependencyVersion.of(v.toString()));
});
return result;
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2020 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.dependency;
import lombok.Value;
import lombok.With;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.release.model.Version;
/**
* @author Mark Paluch
*/
@Value
class DependencyVersion implements Comparable<DependencyVersion> {
private static Pattern VERSION = Pattern.compile("((?>(?>\\d+)[\\.]?)+)(-[a-zA-Z]+)?(\\d+)?");
private static Pattern NAME_VERSION = Pattern.compile("([A-Za-z]+)-(RELEASE|SR(\\d+))");
private static Comparator<DependencyVersion> VERSION_COMPARATOR = Comparator.comparing(DependencyVersion::getVersion)
.thenComparing((o1, o2) -> {
// no modifier means release so it's higher order
if (o1.getModifier().isEmpty() && !o2.getModifier().isEmpty()) {
return 1;
}
if (!o1.getModifier().isEmpty() && o2.getModifier().isEmpty()) {
return -1;
}
return 0;
}).thenComparing(DependencyVersion::getModifier).thenComparing(DependencyVersion::getCounter)
.thenComparing(DependencyVersion::getIdentifier);
private static Comparator<DependencyVersion> TRAIN_VERSION_COMPARATOR = Comparator
.comparing(DependencyVersion::getTrainName).thenComparing(DependencyVersion::getVersion);
String identifier;
String trainName;
Version version;
String modifier;
int counter;
@With LocalDateTime createdAt;
public static DependencyVersion of(String identifier) {
Matcher bomMatcher = NAME_VERSION.matcher(identifier);
if (bomMatcher.find()) {
Version version = Version.of(0);
if (identifier.contains("-SR")) {
version = Version.of(Integer.parseInt(bomMatcher.group(3)));
}
return new DependencyVersion(identifier, bomMatcher.group(1), version, "", 0, null);
}
Matcher versionMatcher = VERSION.matcher(identifier);
if (versionMatcher.find()) {
Version version = null;
String modifier;
String counter;
try {
version = Version.parse(versionMatcher.group(1));
} catch (NumberFormatException e) {
}
modifier = versionMatcher.group(2);
counter = versionMatcher.group(3);
return new DependencyVersion(identifier, null, version, modifier == null ? "" : modifier.toUpperCase(Locale.ROOT),
counter != null ? Integer.parseInt(counter) : 0, null);
}
throw new IllegalArgumentException(String.format("Cannot parse version identifier%s", identifier));
}
@Override
public int compareTo(DependencyVersion o) {
if (trainName != null && o.trainName != null) {
return TRAIN_VERSION_COMPARATOR.compare(this, o);
}
if (trainName != null) {
return -1;
}
if (o.trainName != null) {
return 1;
}
if (version != null && o.version != null) {
return VERSION_COMPARATOR.compare(this, o);
}
return identifier.compareTo(o.identifier);
}
@Override
public String toString() {
return identifier;
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2020 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.dependency;
import lombok.Value;
import java.util.Iterator;
import java.util.List;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.util.Streamable;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Configuration of dependencies for a specific project.
*
* @author Mark Paluch
*/
public class ProjectDependencies implements Streamable<ProjectDependencies.ProjectDependency> {
private static final MultiValueMap<Project, ProjectDependency> config = new LinkedMultiValueMap<>();
static {
config.add(Projects.BUILD, ProjectDependency.ofProperty("apt", Dependencies.APT));
config.add(Projects.BUILD, ProjectDependency.ofProperty("aspectj", Dependencies.ASPECTJ));
config.add(Projects.BUILD, ProjectDependency.ofProperty("assertj", Dependencies.ASSERTJ));
config.add(Projects.BUILD, ProjectDependency.ofProperty("jackson", Dependencies.JACKSON));
config.add(Projects.BUILD, ProjectDependency.ofProperty("jacoco", Dependencies.JACOCO));
config.add(Projects.BUILD, ProjectDependency.ofProperty("jodatime", Dependencies.JODA_TIME));
config.add(Projects.BUILD, ProjectDependency.ofProperty("junit5", Dependencies.JUNIT5));
config.add(Projects.BUILD, ProjectDependency.ofProperty("junit", Dependencies.JUNIT4));
config.add(Projects.BUILD, ProjectDependency.ofProperty("kotlin", Dependencies.KOTLIN));
config.add(Projects.BUILD, ProjectDependency.ofProperty("kotlin-coroutines", Dependencies.KOTLIN_COROUTINES));
config.add(Projects.BUILD, ProjectDependency.ofProperty("mockito", Dependencies.MOCKITO));
config.add(Projects.BUILD, ProjectDependency.ofProperty("querydsl", Dependencies.QUERYDSL));
config.add(Projects.BUILD, ProjectDependency.ofProperty("reactor", Dependencies.PROJECT_REACTOR));
config.add(Projects.BUILD, ProjectDependency.ofProperty("rxjava", Dependencies.RXJAVA1));
config.add(Projects.BUILD, ProjectDependency.ofProperty("rxjava2", Dependencies.RXJAVA2));
config.add(Projects.BUILD, ProjectDependency.ofProperty("rxjava3", Dependencies.RXJAVA3));
config.add(Projects.BUILD, ProjectDependency.ofProperty("rxjava-reactive-streams", Dependencies.RXJAVA_RS));
config.add(Projects.BUILD, ProjectDependency.ofProperty("spring", Dependencies.SPRING_FRAMEWORK));
config.add(Projects.BUILD, ProjectDependency.ofProperty("spring-hateoas", Dependencies.SPRING_HATEOAS));
config.add(Projects.BUILD, ProjectDependency.ofProperty("spring-plugin", Dependencies.SPRING_PLUGIN));
config.add(Projects.BUILD, ProjectDependency.ofProperty("testcontainers", Dependencies.TESTCONTAINERS));
config.add(Projects.BUILD, ProjectDependency.ofProperty("threetenbp", Dependencies.THREE_TEN_BP));
config.add(Projects.BUILD, ProjectDependency.ofProperty("webbeans", Dependencies.OPEN_WEB_BEANS));
config.add(Projects.COMMONS, ProjectDependency.ofProperty("vavr", Dependencies.VAVR));
config.add(Projects.COMMONS, ProjectDependency.ofProperty("xmlbeam", Dependencies.XML_BEAM));
config.add(Projects.MONGO_DB, ProjectDependency.ofProperty("mongo.reactivestreams", Dependencies.MONGODB_RS));
config.add(Projects.MONGO_DB, ProjectDependency.ofProperty("mongo", Dependencies.MONGODB_LEGACY));
config.add(Projects.MONGO_DB, ProjectDependency.ofProperty("mongo", Dependencies.MONGODB_CORE));
config.add(Projects.MONGO_DB, ProjectDependency.ofProperty("mongo", Dependencies.MONGODB_SYNC));
config.add(Projects.MONGO_DB, ProjectDependency.ofProperty("mongo", Dependencies.MONGODB_ASYNC));
config.add(Projects.REDIS, ProjectDependency.ofProperty("lettuce", Dependencies.LETTUCE));
config.add(Projects.REDIS, ProjectDependency.ofProperty("jedis", Dependencies.JEDIS));
config.add(Projects.CASSANDRA,
ProjectDependency.ofProperty("cassandra-driver.version", Dependencies.CASSANDRA_DRIVER3));
config.add(Projects.CASSANDRA,
ProjectDependency.ofProperty("cassandra-driver.version", Dependencies.CASSANDRA_DRIVER4));
config.add(Projects.NEO4J, ProjectDependency.ofProperty("neo4j.ogm.version", Dependencies.NEO4J_OGM));
config.add(Projects.NEO4J, ProjectDependency.ofProperty("neo4j-java-driver.version", Dependencies.NEO4J_DRIVER));
config.add(Projects.COUCHBASE, ProjectDependency.ofProperty("couchbase", Dependencies.COUCHBASE));
config.add(Projects.ELASTICSEARCH, ProjectDependency.ofProperty("elasticsearch", Dependencies.ELASTICSEARCH));
config.add(Projects.LDAP, ProjectDependency.ofProperty("spring-ldap", Dependencies.SPRING_LDAP));
}
private final List<ProjectDependency> dependencies;
private ProjectDependencies(List<ProjectDependency> dependencies) {
this.dependencies = dependencies;
}
/**
* Retrieve upgradable dependencies for a {@link Project}.
*
* @param project
* @return
* @throws IllegalArgumentException if the project has no upgradable dependencies.
*/
public static ProjectDependencies get(Project project) {
if (!containsProject(project)) {
throw new IllegalArgumentException(String.format("No dependency configuration for %s!", project));
}
return new ProjectDependencies(config.get(project));
}
/**
* Check whether the {@link Project} has upgradable dependencies.
*
* @param project
* @return
*/
public static boolean containsProject(Project project) {
return config.containsKey(project);
}
public String getVersionPropertyFor(Dependency dependency) {
for (ProjectDependency projectDependency : dependencies) {
if (projectDependency.getDependency().equals(dependency)) {
return projectDependency.getProperty();
}
}
throw new IllegalArgumentException("Dependency " + dependency + " is not a dependency of this project!");
}
@Override
public Iterator<ProjectDependency> iterator() {
return dependencies.iterator();
}
@Value
public static class ProjectDependency {
String property;
Dependency dependency;
public static ProjectDependency ofProperty(String pomProperty, Dependency dependency) {
return new ProjectDependency(pomProperty, dependency);
}
}
}

View File

@@ -233,28 +233,29 @@ public class GitOperations {
}
public void push(TrainIteration iteration) {
ExecutionUtils.run(executor, iteration, this::push);
}
ExecutionUtils.run(executor, iteration, module -> {
public void push(ModuleIteration module) {
Branch branch = Branch.from(module);
logger.log(module, "git push origin %s", branch);
Branch branch = Branch.from(module);
logger.log(module, "git push origin %s", branch);
if (!branchExists(module.getProject(), branch)) {
if (!branchExists(module.getProject(), branch)) {
logger.log(module, "No branch %s in %s, skip push", branch, module.getProject().getName());
return;
}
logger.log(module, "No branch %s in %s, skip push", branch, module.getProject().getName());
return;
}
doWithGit(module.getProject(), git -> {
doWithGit(module.getProject(), git -> {
Ref ref = git.getRepository().findRef(branch.toString());
Ref ref = git.getRepository().findRef(branch.toString());
git.push()//
.setRemote("origin")//
.setRefSpecs(new RefSpec(ref.getName()))//
.setCredentialsProvider(gitProperties.getCredentials())//
.call();
});
git.push()//
.setRemote("origin")//
.setRefSpecs(new RefSpec(ref.getName()))//
.setCredentialsProvider(gitProperties.getCredentials())//
.call();
});
}
@@ -621,6 +622,26 @@ public class GitOperations {
() -> String.format("No issue tracker found for project %s!", project));
Ticket ticket = tracker.getReleaseTicketFor(module);
commit(module, ticket, summary, details, files);
}
/**
* Commits the given files for the given {@link ModuleIteration} using the given summary and details for the commit
* message. If no files are given, all pending changes are committed.
*
* @param module must not be {@literal null}.
* @param summary must not be {@literal null} or empty.
* @param details can be {@literal null} or empty.
* @param files can be empty.
* @throws Exception
*/
public void commit(ModuleIteration module, Ticket ticket, String summary, Optional<String> details, File... files) {
Assert.notNull(module, "Module iteration must not be null!");
Assert.hasText(summary, "Summary must not be null or empty!");
Project project = module.getProject();
Commit commit = new Commit(ticket, summary, details);
String author = gitProperties.getAuthor();
String email = gitProperties.getEmail();

View File

@@ -144,7 +144,7 @@ public interface IssueTracker extends Plugin<Project> {
*/
default Changelog getChangelogFor(ModuleIteration module, List<TicketReference> ticketReferences) {
Tickets tickets = resolve(module, ticketReferences);
Tickets tickets = findTickets(module, ticketReferences);
return Changelog.of(module, tickets);
}
@@ -155,6 +155,14 @@ public interface IssueTracker extends Plugin<Project> {
*/
void closeIteration(ModuleIteration module);
/**
* Resolve a {@link Ticket}.
*
* @param module must not be {@literal null}.
* @param ticket must not be {@literal null}.
*/
void closeTicket(ModuleIteration module, Ticket ticket);
/**
* Resolve a {@link List} of {@link TicketReference}s to {@link Tickets} for a given {@link ModuleIteration}. The
* implementation ensures to resolve only references that match the issue tracker scheme this issue tracker is
@@ -164,5 +172,5 @@ public interface IssueTracker extends Plugin<Project> {
* @param ticketReferences must not be {@literal null}.
* @return
*/
Tickets resolve(ModuleIteration moduleIteration, List<TicketReference> ticketReferences);
Tickets findTickets(ModuleIteration moduleIteration, List<TicketReference> ticketReferences);
}

View File

@@ -320,18 +320,23 @@ class GitHub extends GitHubSupport implements IssueTracker {
Assert.notNull(module, "ModuleIteration must not be null.");
Ticket releaseTicketFor = getReleaseTicketFor(module);
GitHubIssue response = close(module, releaseTicketFor);
return toTicket(response);
}
private GitHubIssue close(ModuleIteration module, Ticket ticket) {
String repositoryName = GitProject.of(module.getProject()).getRepositoryName();
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("id", stripHash(releaseTicketFor));
parameters.put("id", stripHash(ticket));
GitHubIssue edit = GitHubIssue.assignedTo(properties.getUsername()).close();
GitHubIssue response = operations.exchange(ISSUE_BY_ID_URI_TEMPLATE, HttpMethod.PATCH,
return operations.exchange(ISSUE_BY_ID_URI_TEMPLATE, HttpMethod.PATCH,
new HttpEntity<>(edit, new HttpHeaders()), ISSUE_TYPE, parameters).getBody();
return toTicket(response);
}
private String stripHash(Ticket ticket) {
@@ -424,7 +429,12 @@ class GitHub extends GitHubSupport implements IssueTracker {
}
@Override
public Tickets resolve(ModuleIteration moduleIteration, List<TicketReference> ticketReferences) {
public void closeTicket(ModuleIteration module, Ticket ticket) {
close(module, ticket);
}
@Override
public Tickets findTickets(ModuleIteration moduleIteration, List<TicketReference> ticketReferences) {
logger.log(moduleIteration, "Looking up GitHub issues from milestone …");

View File

@@ -543,6 +543,11 @@ class Jira implements JiraConnector {
// - if no next version exists, create
}
@Override
public void closeTicket(ModuleIteration module, Ticket ticket) {
resolve(ticket);
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.jira.JiraConnector#getChangelogFor(org.springframework.data.release.model.Module, org.springframework.data.release.model.Iteration)
@@ -575,7 +580,7 @@ class Jira implements JiraConnector {
}
@Override
public Tickets resolve(ModuleIteration moduleIteration, List<TicketReference> ticketReferences) {
public Tickets findTickets(ModuleIteration moduleIteration, List<TicketReference> ticketReferences) {
List<String> ids = ticketReferences.stream()
.filter(it -> it.getId().startsWith(moduleIteration.getProjectKey().getKey())).map(TicketReference::getId)

View File

@@ -131,7 +131,7 @@ public class ReleaseOperations {
List<Ticket> tickets = new ArrayList<>();
for (IssueTracker tracker : trackers) {
tickets.addAll(tracker.resolve(module, ticketReferences).getTickets());
tickets.addAll(tracker.findTickets(module, ticketReferences).getTickets());
}
changelog = Changelog.of(module, new Tickets(tickets));

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2020 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.dependency;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assume.*;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.release.AbstractIntegrationTests;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.Projects;
/**
* Integration tests for {@link DependencyOperations}.
*
* @author Mark Paluch
*/
@Disabled
class DependencyOperationsIntegrationTests extends AbstractIntegrationTests {
@Autowired GitOperations git;
@Autowired DependencyOperations operations;
@BeforeAll
static void beforeAll() {
try {
URL url = new URL("https://repo1.maven.org");
URLConnection urlConnection = url.openConnection();
urlConnection.connect();
urlConnection.getInputStream().close();
} catch (IOException e) {
assumeTrue("Test requires connectivity to Maven: " + e.toString(), false);
}
}
@Test
void shouldDiscoverDependencyVersions() {
assertThat(operations.getAvailableVersions(Dependencies.PROJECT_REACTOR)).isNotEmpty();
}
@Test
void shouldReportExistingDependencyVersions() {
assertThat(operations.getCurrentDependencies(Projects.BUILD)).isNotEmpty();
}
@Test
void shouldReportExistingOptionalDependencies() {
// git.checkout(ReleaseTrains.MOORE);
assertThat(operations.getCurrentDependencies(Projects.CASSANDRA)).hasSize(1);
assertThat(operations.getCurrentDependencies(Projects.MONGO_DB)).hasSize(1);
assertThat(operations.getCurrentDependencies(Projects.NEO4J)).hasSize(1);
}
@Test
void getUpgradeProposals() {
System.out.println(operations.getDependencyUpgradeProposals(Projects.BUILD, Iteration.M1));
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2020 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.dependency;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.data.release.model.Iteration;
/**
* Unit tests for {@link DependencyOperations}.
*
* @author Mark Paluch
*/
class DependencyOperationsUnitTests {
@Test
void shouldRetainCurrentVersion() {
List<DependencyVersion> availableVersions = Stream.of("5.7.0", "5.7.0-M1") //
.map(DependencyVersion::of) //
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.SR1,
DependencyVersion.of("5.7.0"), availableVersions);
assertThat(proposal.getCurrent()).isEqualTo(DependencyVersion.of("5.7.0"));
assertThat(proposal.getLatestMinor()).isEqualTo(DependencyVersion.of("5.7.0"));
assertThat(proposal.getProposal()).isEqualTo(DependencyVersion.of("5.7.0"));
assertThat(proposal.getLatest()).isEqualTo(DependencyVersion.of("5.7.0"));
}
@Test
void shouldSelectNextMinorVersion() {
List<DependencyVersion> availableVersions = Stream.of("5.7.0", "5.7.1", "5.8.0") //
.map(DependencyVersion::of) //
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.SR1,
DependencyVersion.of("5.7.0"), availableVersions);
assertThat(proposal.getCurrent()).isEqualTo(DependencyVersion.of("5.7.0"));
assertThat(proposal.getLatestMinor()).isEqualTo(DependencyVersion.of("5.7.1"));
assertThat(proposal.getProposal()).isEqualTo(DependencyVersion.of("5.7.1"));
assertThat(proposal.getLatest()).isEqualTo(DependencyVersion.of("5.8.0"));
}
@Test
void shouldSelectLatestVersion() {
List<DependencyVersion> availableVersions = Stream.of("5.7.0", "5.7.1", "5.8.0") //
.map(DependencyVersion::of) //
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.M1,
DependencyVersion.of("5.7.0"), availableVersions);
assertThat(proposal.getCurrent()).isEqualTo(DependencyVersion.of("5.7.0"));
assertThat(proposal.getLatestMinor()).isEqualTo(DependencyVersion.of("5.7.1"));
assertThat(proposal.getProposal()).isEqualTo(DependencyVersion.of("5.8.0"));
assertThat(proposal.getLatest()).isEqualTo(DependencyVersion.of("5.8.0"));
}
@Test
void shouldReportNewerVersions() {
List<DependencyVersion> availableVersions = Stream.of("5.7.0", "5.7.1", "5.7.2-M2", "5.7.2", "5.8.0") //
.map(DependencyVersion::of) //
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.SR1,
DependencyVersion.of("5.7.1"), availableVersions);
assertThat(proposal.getNewerVersions()).extracting(DependencyVersion::getIdentifier).containsExactly("5.7.2-M2",
"5.7.2", "5.8.0");
}
@Test
void shouldReportMilestoneVersionForMilestoneIteration() {
List<DependencyVersion> availableVersions = Stream.of("5.7.0", "5.7.1", "5.7.2-M2") //
.map(DependencyVersion::of) //
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.M1,
DependencyVersion.of("5.7.1"), availableVersions);
assertThat(proposal.getLatest()).extracting(DependencyVersion::getIdentifier).isEqualTo("5.7.2-M2");
assertThat(proposal.getProposal()).extracting(DependencyVersion::getIdentifier).isEqualTo("5.7.2-M2");
assertThat(proposal.getNewerVersions()).extracting(DependencyVersion::getIdentifier).containsExactly("5.7.2-M2");
}
@Test
void shouldSkipMilestoneVersionForNonMilestoneIteration() {
List<DependencyVersion> availableVersions = Stream.of("5.7.0", "5.7.1", "5.7.2-M2") //
.map(DependencyVersion::of) //
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.RC1,
DependencyVersion.of("5.7.1"), availableVersions);
assertThat(proposal.getLatest()).extracting(DependencyVersion::getIdentifier).isEqualTo("5.7.1");
assertThat(proposal.getProposal()).extracting(DependencyVersion::getIdentifier).isEqualTo("5.7.1");
assertThat(proposal.getNewerVersions()).extracting(DependencyVersion::getIdentifier).containsExactly("5.7.2-M2");
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2020 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.dependency;
import static org.assertj.core.api.Assertions.*;
import java.util.Map;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ReleaseTrains;
/**
* Unit tests for {@link DependencyUpgradeProposals}.
*
* @author Mark Paluch
*/
class DependencyUpgradeProposalsUnitTests {
@Test
void shouldParseDependencies() {
Properties properties = new Properties();
properties.put("dependency.train", "Pascal");
properties.put("dependency.iteration", "M1");
properties.put("dependency[org.assertj:assertj-core]", "3.18.1");
Map<Dependency, DependencyVersion> dependencies = DependencyUpgradeProposals
.fromProperties(ReleaseTrains.PASCAL.getIteration(Iteration.M1), properties);
assertThat(dependencies).hasSize(1).containsEntry(Dependencies.ASSERTJ, DependencyVersion.of("3.18.1"));
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2020 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.dependency;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link DependencyVersion}.
*
* @author Mark Paluch
*/
class DependencyVersionUnitTests {
@Test
void shouldConsiderSemVerSortOrder() {
List<String> sorted = Stream.of("1.0.0", "1.0.0-m1", "1.0.0-rc1", "1.0.0-m2") //
.map(DependencyVersion::of) //
.sorted() //
.map(DependencyVersion::getIdentifier) //
.collect(Collectors.toList());
assertThat(sorted).containsExactly("1.0.0-m1", "1.0.0-m2", "1.0.0-rc1", "1.0.0");
}
@Test
void shouldConsiderReleaseTrainSortOrder() {
List<String> sorted = Stream.of("Bismuth-SR1", "Aluminium-SR1", "Aluminium-RELEASE", "Aluminium-SR2") //
.map(DependencyVersion::of) //
.sorted() //
.map(DependencyVersion::getIdentifier) //
.collect(Collectors.toList());
assertThat(sorted).containsExactly("Aluminium-RELEASE", "Aluminium-SR1", "Aluminium-SR2", "Bismuth-SR1");
}
}

View File

@@ -56,7 +56,7 @@ class ReleaseOperationsIntegrationTests extends AbstractIntegrationTests {
List<TicketReference> ticketReferences = gitOperations.getTicketReferencesBetween(Projects.MONGO_DB, from, to);
IssueTracker tracker = trackers.getRequiredPluginFor(Projects.MONGO_DB);
Tickets tickets = tracker.resolve(to.getModule(Projects.MONGO_DB), ticketReferences);
Tickets tickets = tracker.findTickets(to.getModule(Projects.MONGO_DB), ticketReferences);
assertThat(tickets).hasSize(15);
}
@@ -70,7 +70,7 @@ class ReleaseOperationsIntegrationTests extends AbstractIntegrationTests {
List<TicketReference> ticketReferences = gitOperations.getTicketReferencesBetween(Projects.R2DBC, from, to);
IssueTracker tracker = trackers.getRequiredPluginFor(Projects.R2DBC);
Tickets tickets = tracker.resolve(to.getModule(Projects.R2DBC), ticketReferences);
Tickets tickets = tracker.findTickets(to.getModule(Projects.R2DBC), ticketReferences);
assertThat(tickets).hasSize(22);
}