Add support to upgrade Maven wrapper across projects.

Closes #192
This commit is contained in:
Mark Paluch
2021-09-17 08:10:42 +02:00
parent bdafbce523
commit eb6fa44f89
7 changed files with 408 additions and 35 deletions

View File

@@ -117,6 +117,8 @@ public class Dependencies {
public static final Dependency SPRING_LDAP = Dependency.of("Spring LDAP",
"org.springframework.ldap:spring-ldap-core");
public static final Dependency MAVEN = Dependency.of("Maven Wrapper", "org.apache.maven:apache-maven");
static {
ReflectionUtils.doWithFields(Dependencies.class, field -> {

View File

@@ -115,7 +115,15 @@ public class DependencyCommands extends TimedCommand {
git.checkout(iteration.getTrain(), false);
Tickets tickets = operations.createUpgradeTickets(module, dependencyVersions);
DependencyVersions upgradesToApply = operations.getDependencyUpgradesToApply(module.getProject(),
dependencyVersions);
if (upgradesToApply.isEmpty()) {
logger.log(module, "No dependency upgrades to apply");
return;
}
Tickets tickets = operations.getOrCreateUpgradeTickets(module, upgradesToApply);
operations.upgradeDependencies(tickets, module, dependencyVersions);
git.push(module);

View File

@@ -20,6 +20,10 @@ import lombok.SneakyThrows;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
@@ -28,7 +32,9 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
@@ -47,6 +53,7 @@ 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;
@@ -99,7 +106,8 @@ public class DependencyOperations {
DependencyVersion currentVersion = currentDependencies.get(dependency);
List<DependencyVersion> versions = getAvailableVersions(dependency);
DependencyUpgradeProposal proposal = getDependencyUpgradeProposal(iteration, currentVersion, versions);
DependencyUpgradeProposal proposal = getDependencyUpgradeProposal(DependencyUpgradePolicy.from(iteration),
currentVersion, versions);
proposals.put(dependency, proposal);
});
@@ -107,6 +115,140 @@ public class DependencyOperations {
return new DependencyUpgradeProposals(proposals);
}
/**
* Obtain dependency upgrade proposals for Maven Wrapper.
*
* @param project
* @param iteration
* @return
*/
public DependencyUpgradeProposals getMavenWrapperDependencyUpgradeProposals(TrainIteration iteration) {
for (ModuleIteration moduleIteration : iteration) {
// ensure we have Maven Wrapper for each project.
getMavenWrapperVersion(moduleIteration.getProject());
}
return getDependencyUpgradeProposals(Projects.BUILD, DependencyUpgradePolicy.LATEST_STABLE, Dependencies.MAVEN,
this::getMavenWrapperVersion);
}
/**
* Applies the upgrade and creates a commit.
*
* @param tickets
* @param module
* @param dependencyVersions
*/
public Tickets upgradeMavenWrapperVersion(Tickets tickets, ModuleIteration module,
DependencyVersions dependencyVersions) {
if (dependencyVersions.isEmpty()) {
logger.log(module, "No dependency upgrades to apply");
}
return doWithDependencyVersionsAndCommit(tickets, module, dependencyVersions, (dependency, version) -> {
upgradeMavenWrapperVersion(module.getProject(), version);
});
}
public List<Project> getProjectsToUpgradeMavenWrapper(DependencyVersion targetVersion, TrainIteration iteration) {
List<Project> projectsToUpgrade = new ArrayList<>();
for (ModuleIteration moduleIteration : iteration) {
DependencyVersion currentVersion = getMavenWrapperVersion(moduleIteration.getProject());
if (targetVersion.isNewer(currentVersion)) {
projectsToUpgrade.add(moduleIteration.getProject());
}
}
return projectsToUpgrade;
}
private DependencyVersion getMavenWrapperVersion(Project project) {
try {
File file = getMavenWrapperProperties(project);
String distributionUrl;
try (FileInputStream fileInputStream = new FileInputStream(file)) {
Properties properties = new Properties();
properties.load(fileInputStream);
distributionUrl = properties.getProperty("distributionUrl");
}
Pattern versionPattern = Pattern.compile(".*/maven2/org/apache/maven/apache-maven/([\\d\\.]+)/.*");
Matcher matcher = versionPattern.matcher(distributionUrl);
if (!matcher.find()) {
throw new IllegalStateException(
String.format("Invalid distribution URL in %s: %s", project.getName(), distributionUrl));
}
return DependencyVersion.of(matcher.group(1));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private void upgradeMavenWrapperVersion(Project project, DependencyVersion dependencyVersion) {
String distributionUrlTemplate = "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/%s/apache-maven-%s-bin.zip";
try {
File file = getMavenWrapperProperties(project);
Properties properties = new Properties();
try (FileInputStream is = new FileInputStream(file)) {
properties.load(is);
}
properties.setProperty("distributionUrl",
String.format(distributionUrlTemplate, dependencyVersion.getIdentifier(), dependencyVersion.getIdentifier()));
try (FileOutputStream os = new FileOutputStream(file)) {
properties.store(os, null);
}
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private File getMavenWrapperProperties(Project project) throws FileNotFoundException {
File file = workspace.getFile(".mvn/wrapper/maven-wrapper.properties", project);
if (!file.exists()) {
throw new FileNotFoundException(file.toString());
}
return file;
}
public DependencyUpgradeProposals getDependencyUpgradeProposals(Project project, DependencyUpgradePolicy policy,
Dependency dependency, Function<Project, DependencyVersion> currentVersionExtractor) {
DependencyVersions currentDependencies = new DependencyVersions(
Collections.singletonMap(dependency, currentVersionExtractor.apply(project)));
Map<Dependency, DependencyUpgradeProposal> proposals = Collections.synchronizedMap(new LinkedHashMap<>());
DependencyVersion currentVersion = currentDependencies.get(dependency);
List<DependencyVersion> versions = getAvailableVersions(dependency);
DependencyUpgradeProposal proposal = getDependencyUpgradeProposal(policy, currentVersion, versions);
proposals.put(dependency, proposal);
return new DependencyUpgradeProposals(proposals);
}
/**
* Ensures there's a upgrade ticket for each dependency to upgrade.
*
@@ -114,17 +256,16 @@ public class DependencyOperations {
* @param dependencyVersions
* @return
*/
public Tickets createUpgradeTickets(ModuleIteration module, DependencyVersions dependencyVersions) {
public Tickets getOrCreateUpgradeTickets(ModuleIteration module, DependencyVersions dependencyVersions) {
Project project = module.getProject();
DependencyVersions upgrades = getDependencyUpgradesToApply(project, dependencyVersions);
IssueTracker tracker = this.tracker.getRequiredPluginFor(project);
Tickets tickets = tracker.getTicketsFor(module);
List<Ticket> upgradeTickets = new ArrayList<>();
upgrades.forEach((dependency, dependencyVersion) -> {
dependencyVersions.forEach((dependency, dependencyVersion) -> {
String upgradeTicketSummary = getUpgradeTicketSummary(dependency, dependencyVersion);
Optional<Ticket> upgradeTicket = getDependencyUpgradeTicket(tickets, upgradeTicketSummary);
@@ -151,7 +292,7 @@ public class DependencyOperations {
}
/**
* Verifies dependencies to upgrade, applies the upgrade, creates a commit.
* Applies the upgrade, creates a commit.
*
* @param tickets
* @param module
@@ -160,25 +301,45 @@ public class DependencyOperations {
public Tickets upgradeDependencies(Tickets tickets, ModuleIteration module, DependencyVersions dependencyVersions) {
Project project = module.getProject();
DependencyVersions upgrades = getDependencyUpgradesToApply(project, dependencyVersions);
ProjectDependencies dependencies = ProjectDependencies.get(project);
if (upgrades.isEmpty()) {
if (dependencyVersions.isEmpty()) {
logger.log(module, "No dependency upgrades to apply");
}
return doWithDependencyVersionsAndCommit(tickets, module, dependencyVersions, (dependency, version) -> {
String versionProperty = dependencies.getVersionPropertyFor(dependency);
File pom = getPomFile(project);
update(pom, Pom.class, it -> {
it.setProperty(versionProperty, version.getIdentifier());
});
});
}
/**
* Verifies dependencies to upgrade, applies the upgrade, creates a commit.
*
* @param tickets
* @param module
* @param dependencyVersions
*/
private Tickets doWithDependencyVersionsAndCommit(Tickets tickets, ModuleIteration module,
DependencyVersions dependencyVersions, BiConsumer<Dependency, DependencyVersion> action) {
if (dependencyVersions.isEmpty()) {
logger.log(module, "No dependency upgrades to apply");
}
List<Ticket> ticketsToClose = new ArrayList<>();
upgrades.forEach((dependency, dependencyVersion) -> {
dependencyVersions.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());
});
action.accept(dependency, dependencyVersion);
gitOperations.commit(module, upgradeTicket, upgradeTicketSummary, Optional.empty());
@@ -197,7 +358,7 @@ public class DependencyOperations {
}
}
private DependencyVersions getDependencyUpgradesToApply(Project project, DependencyVersions dependencyVersions) {
public DependencyVersions getDependencyUpgradesToApply(Project project, DependencyVersions dependencyVersions) {
DependencyVersions currentDependencies = getCurrentDependencies(project);
Map<Dependency, DependencyVersion> upgrades = new LinkedHashMap<>();
@@ -232,11 +393,11 @@ public class DependencyOperations {
return Optional.ofNullable(upgradeTickets.isEmpty() ? null : upgradeTickets.get(0));
}
protected static DependencyUpgradeProposal getDependencyUpgradeProposal(Iteration iteration,
protected static DependencyUpgradeProposal getDependencyUpgradeProposal(DependencyUpgradePolicy policy,
DependencyVersion currentVersion, List<DependencyVersion> allVersions) {
Optional<DependencyVersion> latestMinor = findLatestMinor(iteration, currentVersion, allVersions);
Optional<DependencyVersion> latest = findLatest(iteration, allVersions);
Optional<DependencyVersion> latestMinor = findLatestMinor(policy, currentVersion, allVersions);
Optional<DependencyVersion> latest = findLatest(policy, allVersions);
List<DependencyVersion> newerVersions = allVersions.stream() //
.sorted() //
.filter(it -> it.compareTo(currentVersion) > 0) //
@@ -244,16 +405,16 @@ public class DependencyOperations {
DependencyVersion latestToUse = latest.filter(it -> it.isNewer(currentVersion)).orElse(currentVersion);
return DependencyUpgradeProposal.of(iteration, currentVersion, latestMinor.orElse(latestToUse), latestToUse,
return DependencyUpgradeProposal.of(policy, currentVersion, latestMinor.orElse(latestToUse), latestToUse,
newerVersions);
}
private static Optional<DependencyVersion> findLatest(Iteration iteration,
private static Optional<DependencyVersion> findLatest(DependencyUpgradePolicy policy,
List<DependencyVersion> availableVersions) {
return availableVersions.stream().filter(it -> {
if (iteration.isPublic() && StringUtils.hasText(it.getModifier())) {
if (!policy.milestoneAllowed() && StringUtils.hasText(it.getModifier())) {
return false;
}
@@ -262,14 +423,14 @@ public class DependencyOperations {
}).max(DependencyVersion::compareTo);
}
private static Optional<DependencyVersion> findLatestMinor(Iteration iteration,
private static Optional<DependencyVersion> findLatestMinor(DependencyUpgradePolicy policy,
DependencyVersion currentVersion,
List<DependencyVersion> availableVersions) {
return availableVersions.stream().filter(it -> {
if (iteration.isPublic() && StringUtils.hasText(it.getModifier())) {
return false;
if (policy.milestoneAllowed() && StringUtils.hasText(it.getModifier())) {
return true;
}
if (it.getVersion() == null || currentVersion.getVersion() == null) {

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2021 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
*
* https://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 org.springframework.data.release.model.Iteration;
/**
* Upgrade policy expressing rules how dependency upgrades should happen.
*
* @author Mark Paluch
*/
interface DependencyUpgradePolicy {
DependencyUpgradePolicy LATEST_STABLE = new DependencyUpgradePolicy() {
@Override
public boolean milestoneAllowed() {
return false;
}
@Override
public boolean restrictToMinorVersion() {
return false;
}
};
/**
* @return {@code true} if the use of pre-release dependency versions is allowed.
*/
boolean milestoneAllowed();
/**
* @return {@code true} if upgrades only within the minor version (e.g. bugfixes/patch releases) are allowed.
*/
boolean restrictToMinorVersion();
/**
* Create a upgrade policy from a {@link Iteration}.
*
* @param iteration
* @return
*/
static DependencyUpgradePolicy from(Iteration iteration) {
return new DependencyUpgradePolicy() {
@Override
public boolean milestoneAllowed() {
return iteration.isMilestone() || iteration.isReleaseCandidate();
}
@Override
public boolean restrictToMinorVersion() {
return iteration.isPublic();
}
};
}
}

View File

@@ -21,8 +21,6 @@ import lombok.Value;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.release.model.Iteration;
/**
* @author Mark Paluch
*/
@@ -33,10 +31,10 @@ class DependencyUpgradeProposal {
DependencyVersion current, latest, latestMinor, proposal;
List<DependencyVersion> newerVersions;
public static DependencyUpgradeProposal of(Iteration iteration, DependencyVersion currentVersion,
public static DependencyUpgradeProposal of(DependencyUpgradePolicy policy, DependencyVersion currentVersion,
DependencyVersion latestMinor, DependencyVersion latest, List<DependencyVersion> newerVersions) {
if (iteration.isServiceIteration()) {
if (policy.restrictToMinorVersion()) {
return new DependencyUpgradeProposal(currentVersion, latest, latestMinor, latestMinor, newerVersions);
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2021 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
*
* https://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.Properties;
import java.util.concurrent.ExecutorService;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.issues.Tickets;
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.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 InfrastructureCommands extends TimedCommand {
public static final String MAVEN_PROPERTIES = "dependency-upgrade-maven.properties";
DependencyOperations operations;
ExecutorService executor;
GitOperations git;
Logger logger;
@CliCommand(value = "infra maven check")
public void check(@CliOption(key = "", mandatory = true) TrainIteration iteration,
@CliOption(key = "all", mandatory = false) Boolean reportAll) throws IOException {
git.checkout(iteration.getTrain());
DependencyUpgradeProposals proposals = operations.getMavenWrapperDependencyUpgradeProposals(iteration);
Files.write(Paths.get(MAVEN_PROPERTIES), proposals.asProperties(iteration).getBytes());
Table summary = proposals.toTable(reportAll == null ? false : reportAll);
logger.log(Projects.BUILD, "Upgrade summary:" + System.lineSeparator() + System.lineSeparator() + summary);
logger.log(iteration, "Upgrade proposals written to " + MAVEN_PROPERTIES);
}
@CliCommand(value = "infra maven upgrade")
public void upgrade(@CliOption(key = "", mandatory = true) TrainIteration iteration)
throws IOException, InterruptedException {
logger.log(iteration, "Applying Maven wrapper upgrades to Spring Data…");
DependencyVersions dependencyVersions = loadDependencyUpgrades(iteration);
if (dependencyVersions.isEmpty()) {
throw new IllegalStateException("No version to upgrade found!");
}
git.checkout(iteration.getTrain(), false);
List<Project> projectsToUpgrade = operations
.getProjectsToUpgradeMavenWrapper(dependencyVersions.get(Dependencies.MAVEN), iteration);
ExecutionUtils.run(executor, Streamable.of(projectsToUpgrade), project -> {
ModuleIteration module = iteration.getModule(project);
Tickets tickets = operations.getOrCreateUpgradeTickets(module, dependencyVersions);
operations.upgradeMavenWrapperVersion(tickets, module, dependencyVersions);
git.push(module);
// Allow GitHub to catch up with ticket notifications.
Thread.sleep(1500);
operations.closeUpgradeTickets(module, tickets);
});
}
private DependencyVersions loadDependencyUpgrades(TrainIteration iteration) throws IOException {
if (!Files.exists(Paths.get(MAVEN_PROPERTIES))) {
logger.log(iteration, "Cannot upgrade dependencies: " + MAVEN_PROPERTIES + " does not exist.");
}
Properties properties = new Properties();
try (FileInputStream fis = new FileInputStream(MAVEN_PROPERTIES)) {
properties.load(fis);
}
return DependencyUpgradeProposals.fromProperties(iteration, properties);
}
}

View File

@@ -40,7 +40,8 @@ class DependencyOperationsUnitTests {
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.SR1,
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(
DependencyUpgradePolicy.from(Iteration.SR1),
DependencyVersion.of("5.7.0"), availableVersions);
assertThat(proposal.getCurrent()).isEqualTo(DependencyVersion.of("5.7.0"));
@@ -57,7 +58,8 @@ class DependencyOperationsUnitTests {
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.SR1,
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(
DependencyUpgradePolicy.from(Iteration.SR1),
DependencyVersion.of("5.7.0"), availableVersions);
assertThat(proposal.getCurrent()).isEqualTo(DependencyVersion.of("5.7.0"));
@@ -74,7 +76,8 @@ class DependencyOperationsUnitTests {
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.M1,
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(
DependencyUpgradePolicy.from(Iteration.M1),
DependencyVersion.of("5.7.0"), availableVersions);
assertThat(proposal.getCurrent()).isEqualTo(DependencyVersion.of("5.7.0"));
@@ -91,11 +94,13 @@ class DependencyOperationsUnitTests {
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.SR1,
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(
DependencyUpgradePolicy.from(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");
assertThat(proposal.getProposal()).extracting(DependencyVersion::getIdentifier).isEqualTo("5.7.2");
}
@Test
@@ -106,7 +111,8 @@ class DependencyOperationsUnitTests {
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.M1,
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(
DependencyUpgradePolicy.from(Iteration.M1),
DependencyVersion.of("5.7.1"), availableVersions);
assertThat(proposal.getLatest()).extracting(DependencyVersion::getIdentifier).isEqualTo("5.7.2-M2");
@@ -122,7 +128,8 @@ class DependencyOperationsUnitTests {
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.RC1,
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(
DependencyUpgradePolicy.from(Iteration.RC1),
DependencyVersion.of("5.7.1"), availableVersions);
assertThat(proposal.getLatest()).extracting(DependencyVersion::getIdentifier).isEqualTo("5.7.2-M2");
@@ -138,7 +145,8 @@ class DependencyOperationsUnitTests {
.sorted() //
.collect(Collectors.toList());
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(Iteration.GA,
DependencyUpgradeProposal proposal = DependencyOperations.getDependencyUpgradeProposal(
DependencyUpgradePolicy.from(Iteration.GA),
DependencyVersion.of("5.7.1"), availableVersions);
assertThat(proposal.getLatest()).extracting(DependencyVersion::getIdentifier).isEqualTo("5.7.1");