From 9ee733d12cf6696117f64324ecfbae3896d69b57 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 13 Nov 2020 16:26:55 +0100 Subject: [PATCH] #159 - Dependency upgrade automation. Add commands to check for dependency upgrades and to perform an upgrade for Spring Data Build. --- .gitignore | 3 +- release-tools/readme.md | 13 + .../data/release/build/Pom.java | 8 +- .../data/release/dependency/Dependencies.java | 145 ++++++ .../data/release/dependency/Dependency.java | 57 +++ .../dependency/DependencyCommands.java | 170 ++++++++ .../dependency/DependencyOperations.java | 412 ++++++++++++++++++ .../dependency/DependencyUpgradeProposal.java | 75 ++++ .../DependencyUpgradeProposals.java | 175 ++++++++ .../release/dependency/DependencyVersion.java | 125 ++++++ .../dependency/ProjectDependencies.java | 151 +++++++ .../data/release/git/GitOperations.java | 51 ++- .../data/release/issues/IssueTracker.java | 12 +- .../data/release/issues/github/GitHub.java | 20 +- .../data/release/issues/jira/Jira.java | 7 +- .../data/release/misc/ReleaseOperations.java | 2 +- .../DependencyOperationsIntegrationTests.java | 82 ++++ .../DependencyOperationsUnitTests.java | 132 ++++++ .../DependencyUpgradeProposalsUnitTests.java | 48 ++ .../DependencyVersionUnitTests.java | 56 +++ .../ReleaseOperationsIntegrationTests.java | 4 +- 21 files changed, 1720 insertions(+), 28 deletions(-) create mode 100644 release-tools/src/main/java/org/springframework/data/release/dependency/Dependencies.java create mode 100644 release-tools/src/main/java/org/springframework/data/release/dependency/Dependency.java create mode 100644 release-tools/src/main/java/org/springframework/data/release/dependency/DependencyCommands.java create mode 100644 release-tools/src/main/java/org/springframework/data/release/dependency/DependencyOperations.java create mode 100644 release-tools/src/main/java/org/springframework/data/release/dependency/DependencyUpgradeProposal.java create mode 100644 release-tools/src/main/java/org/springframework/data/release/dependency/DependencyUpgradeProposals.java create mode 100644 release-tools/src/main/java/org/springframework/data/release/dependency/DependencyVersion.java create mode 100644 release-tools/src/main/java/org/springframework/data/release/dependency/ProjectDependencies.java create mode 100644 release-tools/src/test/java/org/springframework/data/release/dependency/DependencyOperationsIntegrationTests.java create mode 100644 release-tools/src/test/java/org/springframework/data/release/dependency/DependencyOperationsUnitTests.java create mode 100644 release-tools/src/test/java/org/springframework/data/release/dependency/DependencyUpgradeProposalsUnitTests.java create mode 100644 release-tools/src/test/java/org/springframework/data/release/dependency/DependencyVersionUnitTests.java diff --git a/.gitignore b/.gitignore index 1b29e90..c63862d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ target/ application-local.properties spring-shell.log .idea/ -*.iml \ No newline at end of file +*.iml +dependency-upgrade-*.properties diff --git a/release-tools/readme.md b/release-tools/readme.md index 8b3c7c1..b9dba2a 100644 --- a/release-tools/readme.md +++ b/release-tools/readme.md @@ -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` diff --git a/release-tools/src/main/java/org/springframework/data/release/build/Pom.java b/release-tools/src/main/java/org/springframework/data/release/build/Pom.java index 1c2eeb1..b101666 100644 --- a/release-tools/src/main/java/org/springframework/data/release/build/Pom.java +++ b/release-tools/src/main/java/org/springframework/data/release/build/Pom.java @@ -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); diff --git a/release-tools/src/main/java/org/springframework/data/release/dependency/Dependencies.java b/release-tools/src/main/java/org/springframework/data/release/dependency/Dependencies.java new file mode 100644 index 0000000..c395c20 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/dependency/Dependencies.java @@ -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 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))); + } + +} diff --git a/release-tools/src/main/java/org/springframework/data/release/dependency/Dependency.java b/release-tools/src/main/java/org/springframework/data/release/dependency/Dependency.java new file mode 100644 index 0000000..df27715 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/dependency/Dependency.java @@ -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 { + + String name; + String groupId, artifactId; + Predicate 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); + } +} diff --git a/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyCommands.java b/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyCommands.java new file mode 100644 index 0000000..00ef9a8 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyCommands.java @@ -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 projects = Projects.all().stream() + .filter(it -> it != Projects.BOM && it != Projects.BUILD && it != Projects.COMMONS) + .collect(Collectors.toList()); + + Map 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 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 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); + } + +} diff --git a/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyOperations.java b/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyOperations.java new file mode 100644 index 0000000..30c16c4 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyOperations.java @@ -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>(?>\\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 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 currentDependencies = getCurrentDependencies(project); + Map proposals = Collections.synchronizedMap(new LinkedHashMap<>()); + + ExecutionUtils.run(executor, Streamable.of(currentDependencies.keySet()), dependency -> { + + DependencyVersion currentVersion = currentDependencies.get(dependency); + List 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 dependencyVersions) { + + Map 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 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 dependencyVersions) throws InterruptedException { + + Map 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 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 getDependencyUpgradesToApply(Project project, + Map dependencyVersions) { + + Map currentDependencies = getCurrentDependencies(project); + Map 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 getDependencyUpgradeTicket(Tickets tickets, String upgradeTicketSummary) { + + List 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 allVersions) { + + DependencyVersion latestMinor = findLatestMinor(iteration, currentVersion, allVersions); + DependencyVersion latest = findLatest(iteration, allVersions); + List 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 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 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 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 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 getAvailableVersions(Dependency dependency) { + + String baseUrl = String.format("https://repo1.maven.org/maven2/%s/%s/", dependency.getGroupId().replace('.', '/'), + dependency.getArtifactId()); + + ResponseEntity mavenMetadata = restOperations.getForEntity(baseUrl + "/maven-metadata.xml", byte[].class); + ResponseEntity directoryListing = restOperations.getForEntity(baseUrl, String.class); + + Map 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 parseCreationDates(String body) { + + Map 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 R doWithPom(File file, Class type, Function 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 void update(File file, Class type, Consumer 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 getVersions(); + + } + +} diff --git a/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyUpgradeProposal.java b/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyUpgradeProposal.java new file mode 100644 index 0000000..a74cd15 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyUpgradeProposal.java @@ -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 newerVersions; + + public static DependencyUpgradeProposal of(Iteration iteration, DependencyVersion currentVersion, + DependencyVersion latestMinor, DependencyVersion latest, List 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(); + } +} diff --git a/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyUpgradeProposals.java b/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyUpgradeProposals.java new file mode 100644 index 0000000..9924969 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyUpgradeProposals.java @@ -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 proposals; + + public DependencyUpgradeProposals(Map 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 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 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 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; + } + +} diff --git a/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyVersion.java b/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyVersion.java new file mode 100644 index 0000000..0009475 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/dependency/DependencyVersion.java @@ -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 { + + 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 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 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; + } +} diff --git a/release-tools/src/main/java/org/springframework/data/release/dependency/ProjectDependencies.java b/release-tools/src/main/java/org/springframework/data/release/dependency/ProjectDependencies.java new file mode 100644 index 0000000..59406ff --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/dependency/ProjectDependencies.java @@ -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 { + + private static final MultiValueMap 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 dependencies; + + private ProjectDependencies(List 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 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); + } + } +} diff --git a/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java b/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java index 547c384..e2e0f35 100644 --- a/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java +++ b/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java @@ -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 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(); diff --git a/release-tools/src/main/java/org/springframework/data/release/issues/IssueTracker.java b/release-tools/src/main/java/org/springframework/data/release/issues/IssueTracker.java index 5964a18..58c7ff1 100644 --- a/release-tools/src/main/java/org/springframework/data/release/issues/IssueTracker.java +++ b/release-tools/src/main/java/org/springframework/data/release/issues/IssueTracker.java @@ -144,7 +144,7 @@ public interface IssueTracker extends Plugin { */ default Changelog getChangelogFor(ModuleIteration module, List 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 { */ 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 { * @param ticketReferences must not be {@literal null}. * @return */ - Tickets resolve(ModuleIteration moduleIteration, List ticketReferences); + Tickets findTickets(ModuleIteration moduleIteration, List ticketReferences); } diff --git a/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHub.java b/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHub.java index 2d291ec..a65df24 100644 --- a/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHub.java +++ b/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHub.java @@ -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 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 ticketReferences) { + public void closeTicket(ModuleIteration module, Ticket ticket) { + close(module, ticket); + } + + @Override + public Tickets findTickets(ModuleIteration moduleIteration, List ticketReferences) { logger.log(moduleIteration, "Looking up GitHub issues from milestone …"); diff --git a/release-tools/src/main/java/org/springframework/data/release/issues/jira/Jira.java b/release-tools/src/main/java/org/springframework/data/release/issues/jira/Jira.java index 56bc52b..f7028b8 100644 --- a/release-tools/src/main/java/org/springframework/data/release/issues/jira/Jira.java +++ b/release-tools/src/main/java/org/springframework/data/release/issues/jira/Jira.java @@ -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 ticketReferences) { + public Tickets findTickets(ModuleIteration moduleIteration, List ticketReferences) { List ids = ticketReferences.stream() .filter(it -> it.getId().startsWith(moduleIteration.getProjectKey().getKey())).map(TicketReference::getId) diff --git a/release-tools/src/main/java/org/springframework/data/release/misc/ReleaseOperations.java b/release-tools/src/main/java/org/springframework/data/release/misc/ReleaseOperations.java index c35e5b3..aeac94e 100644 --- a/release-tools/src/main/java/org/springframework/data/release/misc/ReleaseOperations.java +++ b/release-tools/src/main/java/org/springframework/data/release/misc/ReleaseOperations.java @@ -131,7 +131,7 @@ public class ReleaseOperations { List 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)); diff --git a/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyOperationsIntegrationTests.java b/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyOperationsIntegrationTests.java new file mode 100644 index 0000000..ec5735d --- /dev/null +++ b/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyOperationsIntegrationTests.java @@ -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)); + } +} diff --git a/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyOperationsUnitTests.java b/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyOperationsUnitTests.java new file mode 100644 index 0000000..c3629b7 --- /dev/null +++ b/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyOperationsUnitTests.java @@ -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 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 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 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 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 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 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"); + } +} diff --git a/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyUpgradeProposalsUnitTests.java b/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyUpgradeProposalsUnitTests.java new file mode 100644 index 0000000..f934a4b --- /dev/null +++ b/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyUpgradeProposalsUnitTests.java @@ -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 dependencies = DependencyUpgradeProposals + .fromProperties(ReleaseTrains.PASCAL.getIteration(Iteration.M1), properties); + + assertThat(dependencies).hasSize(1).containsEntry(Dependencies.ASSERTJ, DependencyVersion.of("3.18.1")); + } +} diff --git a/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyVersionUnitTests.java b/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyVersionUnitTests.java new file mode 100644 index 0000000..c2dbebd --- /dev/null +++ b/release-tools/src/test/java/org/springframework/data/release/dependency/DependencyVersionUnitTests.java @@ -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 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 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"); + } +} diff --git a/release-tools/src/test/java/org/springframework/data/release/misc/ReleaseOperationsIntegrationTests.java b/release-tools/src/test/java/org/springframework/data/release/misc/ReleaseOperationsIntegrationTests.java index 39aeb60..0b4a323 100644 --- a/release-tools/src/test/java/org/springframework/data/release/misc/ReleaseOperationsIntegrationTests.java +++ b/release-tools/src/test/java/org/springframework/data/release/misc/ReleaseOperationsIntegrationTests.java @@ -56,7 +56,7 @@ class ReleaseOperationsIntegrationTests extends AbstractIntegrationTests { List 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 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); }