Merge branch '2.7.x' into 3.0.x

Closes gh-37223
This commit is contained in:
Andy Wilkinson
2023-09-07 10:02:06 +01:00
19 changed files with 752 additions and 75 deletions

View File

@@ -113,8 +113,8 @@ public class BomExtension {
LibraryHandler libraryHandler = objects.newInstance(LibraryHandler.class, (version != null) ? version : "");
action.execute(libraryHandler);
LibraryVersion libraryVersion = new LibraryVersion(DependencyVersion.parse(libraryHandler.version));
addLibrary(new Library(name, libraryVersion, libraryHandler.groups, libraryHandler.prohibitedVersions,
libraryHandler.considerSnapshots));
addLibrary(new Library(name, libraryHandler.calendarName, libraryVersion, libraryHandler.groups,
libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots));
}
public void effectiveBomArtifact() {
@@ -218,6 +218,8 @@ public class BomExtension {
private String version;
private String calendarName;
@Inject
public LibraryHandler(String version) {
this.version = version;
@@ -231,6 +233,10 @@ public class BomExtension {
this.considerSnapshots = true;
}
public void setCalendarName(String calendarName) {
this.calendarName = calendarName;
}
public void group(String id, Action<GroupHandler> action) {
GroupHandler groupHandler = new GroupHandler(id);
action.execute(groupHandler);

View File

@@ -34,6 +34,8 @@ public class Library {
private final String name;
private final String calendarName;
private final LibraryVersion version;
private final List<Group> groups;
@@ -48,14 +50,17 @@ public class Library {
* Create a new {@code Library} with the given {@code name}, {@code version}, and
* {@code groups}.
* @param name name of the library
* @param calendarName name of the library as it appears in the Spring Calendar. May
* be {@code null} in which case the {@code name} is used.
* @param version version of the library
* @param groups groups in the library
* @param prohibitedVersions version of the library that are prohibited
* @param considerSnapshots whether to consider snapshots
*/
public Library(String name, LibraryVersion version, List<Group> groups, List<ProhibitedVersion> prohibitedVersions,
boolean considerSnapshots) {
public Library(String name, String calendarName, LibraryVersion version, List<Group> groups,
List<ProhibitedVersion> prohibitedVersions, boolean considerSnapshots) {
this.name = name;
this.calendarName = (calendarName != null) ? calendarName : name;
this.version = version;
this.groups = groups;
this.versionProperty = "Spring Boot".equals(name) ? null
@@ -68,6 +73,10 @@ public class Library {
return this.name;
}
public String getCalendarName() {
return this.calendarName;
}
public LibraryVersion getVersion() {
return this.version;
}

View File

@@ -17,13 +17,23 @@
package org.springframework.boot.build.bom.bomr;
import java.net.URI;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.BiPredicate;
import javax.inject.Inject;
import org.gradle.api.Task;
import org.gradle.api.tasks.TaskAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.build.bom.BomExtension;
import org.springframework.boot.build.bom.Library;
import org.springframework.boot.build.bom.bomr.ReleaseSchedule.Release;
import org.springframework.boot.build.bom.bomr.github.Milestone;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
/**
* A {@link Task} to move to snapshot dependencies.
@@ -32,6 +42,8 @@ import org.springframework.boot.build.bom.Library;
*/
public abstract class MoveToSnapshots extends UpgradeDependencies {
private static final Logger log = LoggerFactory.getLogger(MoveToSnapshots.class);
private final URI REPOSITORY_URI = URI.create("https://repo.spring.io/snapshot/");
@Inject
@@ -40,6 +52,12 @@ public abstract class MoveToSnapshots extends UpgradeDependencies {
getRepositoryUris().add(this.REPOSITORY_URI);
}
@Override
@TaskAction
void upgradeDependencies() {
super.upgradeDependencies();
}
@Override
protected String issueTitle(Upgrade upgrade) {
String snapshotVersion = upgrade.getVersion().toString();
@@ -63,4 +81,28 @@ public abstract class MoveToSnapshots extends UpgradeDependencies {
return library.isConsiderSnapshots() && super.eligible(library);
}
@Override
protected List<BiPredicate<Library, DependencyVersion>> determineUpdatePredicates(Milestone milestone) {
ReleaseSchedule releaseSchedule = new ReleaseSchedule();
Map<String, List<Release>> releases = releaseSchedule.releasesBetween(OffsetDateTime.now(),
milestone.getDueOn());
List<BiPredicate<Library, DependencyVersion>> predicates = super.determineUpdatePredicates(milestone);
predicates.add((library, candidate) -> {
List<Release> releasesForLibrary = releases.get(library.getCalendarName());
if (releasesForLibrary != null) {
for (Release release : releasesForLibrary) {
if (candidate.isSnapshotFor(release.getVersion())) {
return true;
}
}
}
if (log.isInfoEnabled()) {
log.info("Ignoring " + candidate + ". No release of " + library.getName() + " scheduled before "
+ milestone.getDueOn());
}
return false;
});
return predicates;
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2023 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.boot.build.bom.bomr;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* Release schedule for Spring projects, retrieved from
* <a href="https://calendar.spring.io">https://calendar.spring.io</a>.
*
* @author Andy Wilkinson
*/
class ReleaseSchedule {
private static final Pattern LIBRARY_AND_VERSION = Pattern.compile("([A-Za-z0-9 ]+) ([0-9A-Za-z.-]+)");
private final RestOperations rest;
ReleaseSchedule() {
this(new RestTemplate());
}
ReleaseSchedule(RestOperations rest) {
this.rest = rest;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
Map<String, List<Release>> releasesBetween(OffsetDateTime start, OffsetDateTime end) {
ResponseEntity<List> response = this.rest
.getForEntity("https://calendar.spring.io/releases?start=" + start + "&end=" + end, List.class);
List<Map<String, String>> body = response.getBody();
Map<String, List<Release>> releasesByLibrary = new LinkedCaseInsensitiveMap<>();
body.stream()
.map(this::asRelease)
.filter(Objects::nonNull)
.forEach((release) -> releasesByLibrary.computeIfAbsent(release.getLibraryName(), (l) -> new ArrayList<>())
.add(release));
return releasesByLibrary;
}
private Release asRelease(Map<String, String> entry) {
LocalDate due = LocalDate.parse(entry.get("start"));
String title = entry.get("title");
Matcher matcher = LIBRARY_AND_VERSION.matcher(title);
if (!matcher.matches()) {
return null;
}
String library = matcher.group(1);
String version = matcher.group(2);
return new Release(library, DependencyVersion.parse(version), due);
}
static class Release {
private final String libraryName;
private final DependencyVersion version;
private final LocalDate dueOn;
Release(String libraryName, DependencyVersion version, LocalDate dueOn) {
this.libraryName = libraryName;
this.version = version;
this.dueOn = dueOn;
}
String getLibraryName() {
return this.libraryName;
}
DependencyVersion getVersion() {
return this.version;
}
LocalDate getDueOn() {
return this.dueOn;
}
}
}

View File

@@ -24,17 +24,15 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.build.bom.Library;
import org.springframework.boot.build.bom.Library.Group;
import org.springframework.boot.build.bom.Library.Module;
import org.springframework.boot.build.bom.Library.ProhibitedVersion;
import org.springframework.boot.build.bom.UpgradePolicy;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
/**
@@ -48,15 +46,21 @@ class StandardLibraryUpdateResolver implements LibraryUpdateResolver {
private final VersionResolver versionResolver;
private final UpgradePolicy upgradePolicy;
private final BiPredicate<Library, DependencyVersion> predicate;
private final boolean movingToSnapshots;
StandardLibraryUpdateResolver(VersionResolver versionResolver, UpgradePolicy upgradePolicy,
boolean movingToSnapshots) {
StandardLibraryUpdateResolver(VersionResolver versionResolver,
List<BiPredicate<Library, DependencyVersion>> predicates) {
this.versionResolver = versionResolver;
this.upgradePolicy = upgradePolicy;
this.movingToSnapshots = movingToSnapshots;
BiPredicate<Library, DependencyVersion> predicate = null;
for (BiPredicate<Library, DependencyVersion> p : predicates) {
if (predicate == null) {
predicate = p;
}
else {
predicate = predicate.and(p);
}
}
this.predicate = predicate;
}
@Override
@@ -87,26 +91,24 @@ class StandardLibraryUpdateResolver implements LibraryUpdateResolver {
private List<VersionOption> determineResolvedVersionOptions(Library library) {
Map<String, SortedSet<DependencyVersion>> moduleVersions = new LinkedHashMap<>();
DependencyVersion libraryVersion = library.getVersion().getVersion();
for (Group group : library.getGroups()) {
for (Module module : group.getModules()) {
moduleVersions.put(group.getId() + ":" + module.getName(),
getLaterVersionsForModule(group.getId(), module.getName(), libraryVersion));
getLaterVersionsForModule(group.getId(), module.getName(), library));
}
for (String bom : group.getBoms()) {
moduleVersions.put(group.getId() + ":" + bom,
getLaterVersionsForModule(group.getId(), bom, libraryVersion));
moduleVersions.put(group.getId() + ":" + bom, getLaterVersionsForModule(group.getId(), bom, library));
}
for (String plugin : group.getPlugins()) {
moduleVersions.put(group.getId() + ":" + plugin,
getLaterVersionsForModule(group.getId(), plugin, libraryVersion));
getLaterVersionsForModule(group.getId(), plugin, library));
}
}
List<DependencyVersion> allVersions = moduleVersions.values()
.stream()
.flatMap(SortedSet::stream)
.distinct()
.filter((dependencyVersion) -> isPermitted(dependencyVersion, library.getProhibitedVersions()))
.filter((dependencyVersion) -> this.predicate.test(library, dependencyVersion))
.toList();
if (allVersions.isEmpty()) {
return Collections.emptyList();
@@ -117,32 +119,6 @@ class StandardLibraryUpdateResolver implements LibraryUpdateResolver {
.collect(Collectors.toList());
}
private boolean isPermitted(DependencyVersion dependencyVersion, List<ProhibitedVersion> prohibitedVersions) {
for (ProhibitedVersion prohibitedVersion : prohibitedVersions) {
String dependencyVersionToString = dependencyVersion.toString();
if (prohibitedVersion.getRange() != null && prohibitedVersion.getRange()
.containsVersion(new DefaultArtifactVersion(dependencyVersionToString))) {
return false;
}
for (String startsWith : prohibitedVersion.getStartsWith()) {
if (dependencyVersionToString.startsWith(startsWith)) {
return false;
}
}
for (String endsWith : prohibitedVersion.getEndsWith()) {
if (dependencyVersionToString.endsWith(endsWith)) {
return false;
}
}
for (String contains : prohibitedVersion.getContains()) {
if (dependencyVersionToString.contains(contains)) {
return false;
}
}
}
return true;
}
private List<String> getMissingModules(Map<String, SortedSet<DependencyVersion>> moduleVersions,
DependencyVersion version) {
List<String> missingModules = new ArrayList<>();
@@ -154,12 +130,8 @@ class StandardLibraryUpdateResolver implements LibraryUpdateResolver {
return missingModules;
}
private SortedSet<DependencyVersion> getLaterVersionsForModule(String groupId, String artifactId,
DependencyVersion currentVersion) {
SortedSet<DependencyVersion> versions = this.versionResolver.resolveVersions(groupId, artifactId);
versions.removeIf((candidate) -> !this.upgradePolicy.test(candidate, currentVersion));
versions.removeIf((candidate) -> !currentVersion.isUpgrade(candidate, this.movingToSnapshots));
return versions;
private SortedSet<DependencyVersion> getLaterVersionsForModule(String groupId, String artifactId, Library library) {
return this.versionResolver.resolveVersions(groupId, artifactId);
}
}

View File

@@ -27,11 +27,13 @@ import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import javax.inject.Inject;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.gradle.api.DefaultTask;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.internal.tasks.userinput.UserInputHandler;
@@ -45,10 +47,12 @@ import org.gradle.api.tasks.options.Option;
import org.springframework.boot.build.bom.BomExtension;
import org.springframework.boot.build.bom.Library;
import org.springframework.boot.build.bom.Library.ProhibitedVersion;
import org.springframework.boot.build.bom.bomr.github.GitHub;
import org.springframework.boot.build.bom.bomr.github.GitHubRepository;
import org.springframework.boot.build.bom.bomr.github.Issue;
import org.springframework.boot.build.bom.bomr.github.Milestone;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
import org.springframework.util.StringUtils;
/**
@@ -70,8 +74,8 @@ public abstract class UpgradeDependencies extends DefaultTask {
protected UpgradeDependencies(BomExtension bom, boolean movingToSnapshots) {
this.bom = bom;
this.movingToSnapshots = movingToSnapshots;
getThreads().convention(2);
this.movingToSnapshots = movingToSnapshots;
}
@Input
@@ -97,7 +101,7 @@ public abstract class UpgradeDependencies extends DefaultTask {
this.bom.getUpgrade().getGitHub().getRepository());
List<String> issueLabels = verifyLabels(repository);
Milestone milestone = determineMilestone(repository);
List<Upgrade> upgrades = resolveUpgrades();
List<Upgrade> upgrades = resolveUpgrades(milestone);
applyUpgrades(repository, issueLabels, milestone, upgrades);
}
@@ -213,15 +217,52 @@ public abstract class UpgradeDependencies extends DefaultTask {
}
@SuppressWarnings("deprecation")
private List<Upgrade> resolveUpgrades() {
private List<Upgrade> resolveUpgrades(Milestone milestone) {
List<Upgrade> upgrades = new InteractiveUpgradeResolver(getServices().get(UserInputHandler.class),
new MultithreadedLibraryUpdateResolver(getThreads().get(),
new StandardLibraryUpdateResolver(new MavenMetadataVersionResolver(getRepositoryUris().get()),
this.bom.getUpgrade().getPolicy(), this.movingToSnapshots)))
determineUpdatePredicates(milestone))))
.resolveUpgrades(matchingLibraries(), this.bom.getLibraries());
return upgrades;
}
protected List<BiPredicate<Library, DependencyVersion>> determineUpdatePredicates(Milestone milestone) {
BiPredicate<Library, DependencyVersion> compilesWithUpgradePolicy = (library,
candidate) -> this.bom.getUpgrade().getPolicy().test(candidate, library.getVersion().getVersion());
BiPredicate<Library, DependencyVersion> isAnUpgrade = (library,
candidate) -> library.getVersion().getVersion().isUpgrade(candidate, this.movingToSnapshots);
BiPredicate<Library, DependencyVersion> isPermitted = (library, candidate) -> {
for (ProhibitedVersion prohibitedVersion : library.getProhibitedVersions()) {
String candidateString = candidate.toString();
if (prohibitedVersion.getRange() != null
&& prohibitedVersion.getRange().containsVersion(new DefaultArtifactVersion(candidateString))) {
return false;
}
for (String startsWith : prohibitedVersion.getStartsWith()) {
if (candidateString.startsWith(startsWith)) {
return false;
}
}
for (String endsWith : prohibitedVersion.getEndsWith()) {
if (candidateString.endsWith(endsWith)) {
return false;
}
}
for (String contains : prohibitedVersion.getContains()) {
if (candidateString.contains(contains)) {
return false;
}
}
}
return true;
};
List<BiPredicate<Library, DependencyVersion>> updatePredicates = new ArrayList<>();
updatePredicates.add(compilesWithUpgradePolicy);
updatePredicates.add(isAnUpgrade);
updatePredicates.add(isPermitted);
return updatePredicates;
}
private List<Library> matchingLibraries() {
List<Library> matchingLibraries = this.bom.getLibraries().stream().filter(this::eligible).toList();
if (matchingLibraries.isEmpty()) {

View File

@@ -16,6 +16,8 @@
package org.springframework.boot.build.bom.bomr.github;
import java.time.OffsetDateTime;
/**
* A milestone in a {@link GitHubRepository GitHub repository}.
*
@@ -27,9 +29,12 @@ public class Milestone {
private final int number;
Milestone(String name, int number) {
private final OffsetDateTime dueOn;
Milestone(String name, int number, OffsetDateTime dueOn) {
this.name = name;
this.number = number;
this.dueOn = dueOn;
}
/**
@@ -48,6 +53,10 @@ public class Milestone {
return this.number;
}
public OffsetDateTime getDueOn() {
return this.dueOn;
}
@Override
public String toString() {
return this.name + " (" + this.number + ")";

View File

@@ -17,6 +17,7 @@
package org.springframework.boot.build.bom.bomr.github;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -75,8 +76,9 @@ final class StandardGitHubRepository implements GitHubRepository {
@Override
public List<Milestone> getMilestones() {
return get("milestones?per_page=100",
(milestone) -> new Milestone((String) milestone.get("title"), (Integer) milestone.get("number")));
return get("milestones?per_page=100", (milestone) -> new Milestone((String) milestone.get("title"),
(Integer) milestone.get("number"),
(milestone.get("due_on") != null) ? OffsetDateTime.parse((String) milestone.get("due_on")) : null));
}
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2023 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.
@@ -87,24 +87,40 @@ class ArtifactVersionDependencyVersion extends AbstractDependencyVersion {
if (this.artifactVersion.equals(other)) {
return false;
}
if (this.artifactVersion.getMajorVersion() == other.getMajorVersion()
&& this.artifactVersion.getMinorVersion() == other.getMinorVersion()
&& this.artifactVersion.getIncrementalVersion() == other.getIncrementalVersion()) {
if (sameMajorMinorIncremental(other)) {
if (!StringUtils.hasLength(this.artifactVersion.getQualifier())
|| "RELEASE".equals(this.artifactVersion.getQualifier())) {
return false;
}
if ("SNAPSHOT".equals(this.artifactVersion.getQualifier())
|| "BUILD".equals(this.artifactVersion.getQualifier())) {
if (isSnapshot()) {
return true;
}
else if ("SNAPSHOT".equals(other.getQualifier()) || "BUILD".equals(other.getQualifier())) {
else if (((ArtifactVersionDependencyVersion) candidate).isSnapshot()) {
return movingToSnapshots;
}
}
return super.isUpgrade(candidate, movingToSnapshots);
}
private boolean sameMajorMinorIncremental(ArtifactVersion other) {
return this.artifactVersion.getMajorVersion() == other.getMajorVersion()
&& this.artifactVersion.getMinorVersion() == other.getMinorVersion()
&& this.artifactVersion.getIncrementalVersion() == other.getIncrementalVersion();
}
private boolean isSnapshot() {
return "SNAPSHOT".equals(this.artifactVersion.getQualifier())
|| "BUILD".equals(this.artifactVersion.getQualifier());
}
@Override
public boolean isSnapshotFor(DependencyVersion candidate) {
if (!isSnapshot() || !(candidate instanceof ArtifactVersionDependencyVersion)) {
return false;
}
return sameMajorMinorIncremental(((ArtifactVersionDependencyVersion) candidate).artifactVersion);
}
@Override
public String toString() {
return this.artifactVersion.toString();

View File

@@ -46,13 +46,21 @@ public interface DependencyVersion extends Comparable<DependencyVersion> {
/**
* Returns whether the given {@code candidate} is an upgrade of this version.
* @param candidate the version the consider
* @param candidate the version to consider
* @param movingToSnapshots whether the upgrade is to be considered as part of moving
* to snaphots
* @return {@code true} if the candidate is an upgrade, otherwise false
*/
boolean isUpgrade(DependencyVersion candidate, boolean movingToSnapshots);
/**
* Returns whether this version is a snapshot for the given {@code candidate}.
* @param candidate the version to consider
* @return {@code true} if this version is a snapshot for the candidate, otherwise
* false
*/
boolean isSnapshotFor(DependencyVersion candidate);
static DependencyVersion parse(String version) {
List<Function<String, DependencyVersion>> parsers = Arrays.asList(CalendarVersionDependencyVersion::parse,
ArtifactVersionDependencyVersion::parse, ReleaseTrainDependencyVersion::parse,

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2023 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.
@@ -72,8 +72,7 @@ final class ReleaseTrainDependencyVersion implements DependencyVersion {
if (comparison != 0) {
return comparison < 0;
}
if (movingToSnapshots && !"BUILD-SNAPSHOT".equals(this.type)
&& "BUILD-SNAPSHOT".equals(candidateReleaseTrain.type)) {
if (movingToSnapshots && !isSnapshot() && candidateReleaseTrain.isSnapshot()) {
return true;
}
comparison = this.type.compareTo(candidateReleaseTrain.type);
@@ -83,6 +82,19 @@ final class ReleaseTrainDependencyVersion implements DependencyVersion {
return Integer.compare(this.version, candidateReleaseTrain.version) < 0;
}
private boolean isSnapshot() {
return "BUILD-SNAPSHOT".equals(this.type);
}
@Override
public boolean isSnapshotFor(DependencyVersion candidate) {
if (!isSnapshot() || !(candidate instanceof ReleaseTrainDependencyVersion)) {
return false;
}
ReleaseTrainDependencyVersion candidateReleaseTrain = (ReleaseTrainDependencyVersion) candidate;
return this.releaseTrain.equals(candidateReleaseTrain.releaseTrain);
}
@Override
public boolean isSameMajor(DependencyVersion other) {
return isSameReleaseTrain(other);

View File

@@ -48,6 +48,11 @@ final class UnstructuredDependencyVersion extends AbstractDependencyVersion impl
return this.version;
}
@Override
public boolean isSnapshotFor(DependencyVersion candidate) {
return false;
}
static UnstructuredDependencyVersion parse(String version) {
return new UnstructuredDependencyVersion(version);
}