Add support to Bomr for moving to snapshots

Closes gh-27002
pull/31258/merge
Andy Wilkinson 2 years ago
parent bb6dfc10ab
commit 994a7a0795

@ -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.
@ -37,6 +37,7 @@ import org.springframework.boot.build.DeployedPlugin;
import org.springframework.boot.build.MavenRepositoryPlugin;
import org.springframework.boot.build.bom.Library.Group;
import org.springframework.boot.build.bom.Library.Module;
import org.springframework.boot.build.bom.bomr.MoveToSnapshots;
import org.springframework.boot.build.bom.bomr.UpgradeBom;
/**
@ -63,6 +64,7 @@ public class BomPlugin implements Plugin<Project> {
project);
project.getTasks().create("bomrCheck", CheckBom.class, bom);
project.getTasks().create("bomrUpgrade", UpgradeBom.class, bom);
project.getTasks().create("moveToSnapshots", MoveToSnapshots.class, bom);
new PublishingCustomizer(project, bom).customize();
}

@ -17,6 +17,7 @@
package org.springframework.boot.build.bom.bomr;
import java.io.StringReader;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@ -49,13 +50,13 @@ final class MavenMetadataVersionResolver implements VersionResolver {
private final RestTemplate rest;
private final Collection<String> repositoryUrls;
private final Collection<URI> repositoryUrls;
MavenMetadataVersionResolver(Collection<String> repositoryUrls) {
MavenMetadataVersionResolver(Collection<URI> repositoryUrls) {
this(new RestTemplate(Collections.singletonList(new StringHttpMessageConverter())), repositoryUrls);
}
MavenMetadataVersionResolver(RestTemplate restTemplate, Collection<String> repositoryUrls) {
MavenMetadataVersionResolver(RestTemplate restTemplate, Collection<URI> repositoryUrls) {
this.rest = restTemplate;
this.repositoryUrls = repositoryUrls;
}
@ -63,15 +64,15 @@ final class MavenMetadataVersionResolver implements VersionResolver {
@Override
public SortedSet<DependencyVersion> resolveVersions(String groupId, String artifactId) {
Set<String> versions = new HashSet<>();
for (String repositoryUrl : this.repositoryUrls) {
for (URI repositoryUrl : this.repositoryUrls) {
versions.addAll(resolveVersions(groupId, artifactId, repositoryUrl));
}
return versions.stream().map(DependencyVersion::parse).collect(Collectors.toCollection(TreeSet::new));
}
private Set<String> resolveVersions(String groupId, String artifactId, String repositoryUrl) {
private Set<String> resolveVersions(String groupId, String artifactId, URI repositoryUrl) {
Set<String> versions = new HashSet<>();
String url = repositoryUrl + "/" + groupId.replace('.', '/') + "/" + artifactId + "/maven-metadata.xml";
URI url = repositoryUrl.resolve(groupId.replace('.', '/') + "/" + artifactId + "/maven-metadata.xml");
try {
String metadata = this.rest.getForObject(url, String.class);
Document metadataDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder()

@ -0,0 +1,60 @@
/*
* Copyright 2021-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.net.URI;
import javax.inject.Inject;
import org.gradle.api.Task;
import org.springframework.boot.build.bom.BomExtension;
/**
* A {@link Task} to move to snapshot dependencies.
*
* @author Andy Wilkinson
*/
public abstract class MoveToSnapshots extends UpgradeDependencies {
private final URI REPOSITORY_URI = URI.create("https://repo.spring.io/snapshot/");
@Inject
public MoveToSnapshots(BomExtension bom) {
super(bom);
getRepositoryUris().add(this.REPOSITORY_URI);
}
@Override
protected String issueTitle(Upgrade upgrade) {
String snapshotVersion = upgrade.getVersion().toString();
String releaseVersion = snapshotVersion.substring(0, snapshotVersion.length() - "-SNAPSHOT".length());
return "Upgrade to " + upgrade.getLibrary().getName() + " " + releaseVersion;
}
@Override
protected String commitMessage(Upgrade upgrade, int issueNumber) {
return "Start building against " + upgrade.getLibrary().getName() + " " + releaseVersion(upgrade) + " snapshots"
+ "\n\nSee gh-" + issueNumber;
}
private String releaseVersion(Upgrade upgrade) {
String snapshotVersion = upgrade.getVersion().toString();
return snapshotVersion.substring(0, snapshotVersion.length() - "-SNAPSHOT".length());
}
}

@ -16,41 +16,14 @@
package org.springframework.boot.build.bom.bomr;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.net.URI;
import javax.inject.Inject;
import org.gradle.api.DefaultTask;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.Task;
import org.gradle.api.artifacts.repositories.MavenArtifactRepository;
import org.gradle.api.internal.tasks.userinput.UserInputHandler;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskExecutionException;
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.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.util.StringUtils;
/**
* {@link Task} to upgrade the libraries managed by a bom.
@ -58,161 +31,27 @@ import org.springframework.util.StringUtils;
* @author Andy Wilkinson
* @author Moritz Halbritter
*/
public class UpgradeBom extends DefaultTask {
private final Set<String> repositoryUrls = new LinkedHashSet<>();
private final BomExtension bom;
private String milestone;
private String libraries;
private int threads = 2;
public abstract class UpgradeBom extends UpgradeDependencies {
@Inject
public UpgradeBom(BomExtension bom) {
this.bom = bom;
super(bom);
getProject().getRepositories().withType(MavenArtifactRepository.class, (repository) -> {
String repositoryUrl = repository.getUrl().toString();
if (!repositoryUrl.endsWith("snapshot")) {
this.repositoryUrls.add(repositoryUrl);
URI repositoryUrl = repository.getUrl();
if (!repositoryUrl.toString().endsWith("snapshot")) {
getRepositoryUris().add(repositoryUrl);
}
});
}
@Option(option = "milestone", description = "Milestone to which dependency upgrade issues should be assigned")
public void setMilestone(String milestone) {
this.milestone = milestone;
}
@Option(option = "threads", description = "Number of Threads to use for update resolution")
public void setThreads(String threads) {
this.threads = Integer.parseInt(threads);
}
@Input
public String getMilestone() {
return this.milestone;
}
@Option(option = "libraries", description = "Regular expression that identifies the libraries to upgrade")
public void setLibraries(String libraries) {
this.libraries = libraries;
}
@Input
@org.gradle.api.tasks.Optional
public String getLibraries() {
return this.libraries;
}
@TaskAction
@SuppressWarnings("deprecation")
void upgradeDependencies() {
GitHubRepository repository = createGitHub().getRepository(this.bom.getUpgrade().getGitHub().getOrganization(),
this.bom.getUpgrade().getGitHub().getRepository());
Set<String> availableLabels = repository.getLabels();
List<String> issueLabels = this.bom.getUpgrade().getGitHub().getIssueLabels();
if (!availableLabels.containsAll(issueLabels)) {
List<String> unknownLabels = new ArrayList<>(issueLabels);
unknownLabels.removeAll(availableLabels);
throw new InvalidUserDataException(
"Unknown label(s): " + StringUtils.collectionToCommaDelimitedString(unknownLabels));
}
Milestone milestone = determineMilestone(repository);
List<Issue> existingUpgradeIssues = repository.findIssues(issueLabels, milestone);
List<Upgrade> upgrades = new InteractiveUpgradeResolver(getServices().get(UserInputHandler.class),
new MultithreadedLibraryUpdateResolver(new MavenMetadataVersionResolver(this.repositoryUrls),
this.bom.getUpgrade().getPolicy(), this.threads))
.resolveUpgrades(matchingLibraries(this.libraries), this.bom.getLibraries());
Path buildFile = getProject().getBuildFile().toPath();
Path gradleProperties = new File(getProject().getRootProject().getProjectDir(), "gradle.properties").toPath();
UpgradeApplicator upgradeApplicator = new UpgradeApplicator(buildFile, gradleProperties);
for (Upgrade upgrade : upgrades) {
String title = "Upgrade to " + upgrade.getLibrary().getName() + " " + upgrade.getVersion();
Issue existingUpgradeIssue = findExistingUpgradeIssue(existingUpgradeIssues, upgrade);
if (existingUpgradeIssue != null) {
System.out.println(title + " (supersedes #" + existingUpgradeIssue.getNumber() + " "
+ existingUpgradeIssue.getTitle() + ")");
}
else {
System.out.println(title);
}
try {
Path modified = upgradeApplicator.apply(upgrade);
int issueNumber = repository.openIssue(title,
(existingUpgradeIssue != null) ? "Supersedes #" + existingUpgradeIssue.getNumber() : "",
issueLabels, milestone);
if (existingUpgradeIssue != null) {
existingUpgradeIssue.label(Arrays.asList("type: task", "status: superseded"));
}
if (new ProcessBuilder().command("git", "add", modified.toFile().getAbsolutePath()).start()
.waitFor() != 0) {
throw new IllegalStateException("git add failed");
}
if (new ProcessBuilder().command("git", "commit", "-m", title + "\n\nCloses gh-" + issueNumber).start()
.waitFor() != 0) {
throw new IllegalStateException("git commit failed");
}
}
catch (IOException ex) {
throw new TaskExecutionException(this, ex);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
private List<Library> matchingLibraries(String pattern) {
if (pattern == null) {
return this.bom.getLibraries();
}
Predicate<String> libraryPredicate = Pattern.compile(pattern).asPredicate();
List<Library> matchingLibraries = this.bom.getLibraries().stream()
.filter((library) -> libraryPredicate.test(library.getName())).collect(Collectors.toList());
if (matchingLibraries.isEmpty()) {
throw new InvalidUserDataException("No libraries matched '" + pattern + "'");
}
return matchingLibraries;
}
private Issue findExistingUpgradeIssue(List<Issue> existingUpgradeIssues, Upgrade upgrade) {
String toMatch = "Upgrade to " + upgrade.getLibrary().getName();
for (Issue existingUpgradeIssue : existingUpgradeIssues) {
if (existingUpgradeIssue.getTitle().substring(0, existingUpgradeIssue.getTitle().lastIndexOf(' '))
.equals(toMatch)) {
return existingUpgradeIssue;
}
}
return null;
}
private GitHub createGitHub() {
Properties bomrProperties = new Properties();
try (Reader reader = new FileReader(new File(System.getProperty("user.home"), ".bomr.properties"))) {
bomrProperties.load(reader);
String username = bomrProperties.getProperty("bomr.github.username");
String password = bomrProperties.getProperty("bomr.github.password");
return GitHub.withCredentials(username, password);
}
catch (IOException ex) {
throw new InvalidUserDataException("Failed to load .bomr.properties from user home", ex);
}
@Override
protected String issueTitle(Upgrade upgrade) {
return "Upgrade to " + upgrade.getLibrary().getName() + " " + upgrade.getVersion();
}
private Milestone determineMilestone(GitHubRepository repository) {
if (this.milestone == null) {
return null;
}
List<Milestone> milestones = repository.getMilestones();
Optional<Milestone> matchingMilestone = milestones.stream()
.filter((milestone) -> milestone.getName().equals(this.milestone)).findFirst();
if (!matchingMilestone.isPresent()) {
throw new InvalidUserDataException("Unknown milestone: " + this.milestone);
}
return matchingMilestone.get();
@Override
protected String commitMessage(Upgrade upgrade, int issueNumber) {
return issueTitle(upgrade) + "\n\nCloses gh-" + issueNumber;
}
}

@ -0,0 +1,223 @@
/*
* 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.
* 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.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.gradle.api.DefaultTask;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.internal.tasks.userinput.UserInputHandler;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskExecutionException;
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.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.util.StringUtils;
/**
* Base class for tasks that upgrade dependencies in a bom.
*
* @author Andy Wilkinson
* @author Moritz Halbritter
*/
public abstract class UpgradeDependencies extends DefaultTask {
private final BomExtension bom;
@Inject
public UpgradeDependencies(BomExtension bom) {
this.bom = bom;
getThreads().convention(2);
}
@Input
@Option(option = "milestone", description = "Milestone to which dependency upgrade issues should be assigned")
public abstract Property<String> getMilestone();
@Input
@Optional
@Option(option = "threads", description = "Number of Threads to use for update resolution")
public abstract Property<Integer> getThreads();
@Input
@Optional
@Option(option = "libraries", description = "Regular expression that identifies the libraries to upgrade")
public abstract Property<String> getLibraries();
@Input
abstract ListProperty<URI> getRepositoryUris();
@TaskAction
void upgradeDependencies() {
GitHubRepository repository = createGitHub().getRepository(this.bom.getUpgrade().getGitHub().getOrganization(),
this.bom.getUpgrade().getGitHub().getRepository());
List<String> issueLabels = verifyLabels(repository);
Milestone milestone = determineMilestone(repository);
List<Upgrade> upgrades = resolveUpgrades();
applyUpgrades(repository, issueLabels, milestone, upgrades);
}
private void applyUpgrades(GitHubRepository repository, List<String> issueLabels, Milestone milestone,
List<Upgrade> upgrades) {
Path buildFile = getProject().getBuildFile().toPath();
Path gradleProperties = new File(getProject().getRootProject().getProjectDir(), "gradle.properties").toPath();
UpgradeApplicator upgradeApplicator = new UpgradeApplicator(buildFile, gradleProperties);
List<Issue> existingUpgradeIssues = repository.findIssues(issueLabels, milestone);
for (Upgrade upgrade : upgrades) {
String title = issueTitle(upgrade);
Issue existingUpgradeIssue = findExistingUpgradeIssue(existingUpgradeIssues, upgrade);
if (existingUpgradeIssue != null) {
if (existingUpgradeIssue.getState() == Issue.State.CLOSED) {
System.out.println(title + " (supersedes #" + existingUpgradeIssue.getNumber() + " "
+ existingUpgradeIssue.getTitle() + ")");
}
else {
System.out.println(title + " (completes existing upgrade)");
}
}
else {
System.out.println(title);
}
try {
Path modified = upgradeApplicator.apply(upgrade);
int issueNumber;
if (existingUpgradeIssue != null && existingUpgradeIssue.getState() == Issue.State.OPEN) {
issueNumber = existingUpgradeIssue.getNumber();
}
else {
issueNumber = repository.openIssue(title,
(existingUpgradeIssue != null) ? "Supersedes #" + existingUpgradeIssue.getNumber() : "",
issueLabels, milestone);
if (existingUpgradeIssue != null && existingUpgradeIssue.getState() == Issue.State.CLOSED) {
existingUpgradeIssue.label(Arrays.asList("type: task", "status: superseded"));
}
}
if (new ProcessBuilder().command("git", "add", modified.toFile().getAbsolutePath()).start()
.waitFor() != 0) {
throw new IllegalStateException("git add failed");
}
if (new ProcessBuilder().command("git", "commit", "-m", commitMessage(upgrade, issueNumber)).start()
.waitFor() != 0) {
throw new IllegalStateException("git commit failed");
}
}
catch (IOException ex) {
throw new TaskExecutionException(this, ex);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
private List<String> verifyLabels(GitHubRepository repository) {
Set<String> availableLabels = repository.getLabels();
List<String> issueLabels = this.bom.getUpgrade().getGitHub().getIssueLabels();
if (!availableLabels.containsAll(issueLabels)) {
List<String> unknownLabels = new ArrayList<>(issueLabels);
unknownLabels.removeAll(availableLabels);
throw new InvalidUserDataException(
"Unknown label(s): " + StringUtils.collectionToCommaDelimitedString(unknownLabels));
}
return issueLabels;
}
private GitHub createGitHub() {
Properties bomrProperties = new Properties();
try (Reader reader = new FileReader(new File(System.getProperty("user.home"), ".bomr.properties"))) {
bomrProperties.load(reader);
String username = bomrProperties.getProperty("bomr.github.username");
String password = bomrProperties.getProperty("bomr.github.password");
return GitHub.withCredentials(username, password);
}
catch (IOException ex) {
throw new InvalidUserDataException("Failed to load .bomr.properties from user home", ex);
}
}
private Milestone determineMilestone(GitHubRepository repository) {
List<Milestone> milestones = repository.getMilestones();
java.util.Optional<Milestone> matchingMilestone = milestones.stream()
.filter((milestone) -> milestone.getName().equals(getMilestone().get())).findFirst();
if (!matchingMilestone.isPresent()) {
throw new InvalidUserDataException("Unknown milestone: " + getMilestone().get());
}
return matchingMilestone.get();
}
private Issue findExistingUpgradeIssue(List<Issue> existingUpgradeIssues, Upgrade upgrade) {
String toMatch = "Upgrade to " + upgrade.getLibrary().getName();
for (Issue existingUpgradeIssue : existingUpgradeIssues) {
if (existingUpgradeIssue.getTitle().substring(0, existingUpgradeIssue.getTitle().lastIndexOf(' '))
.equals(toMatch)) {
return existingUpgradeIssue;
}
}
return null;
}
@SuppressWarnings("deprecation")
private List<Upgrade> resolveUpgrades() {
List<Upgrade> upgrades = new InteractiveUpgradeResolver(getServices().get(UserInputHandler.class),
new MultithreadedLibraryUpdateResolver(new MavenMetadataVersionResolver(getRepositoryUris().get()),
this.bom.getUpgrade().getPolicy(), getThreads().get())).resolveUpgrades(
matchingLibraries(getLibraries().getOrNull()), this.bom.getLibraries());
return upgrades;
}
private List<Library> matchingLibraries(String pattern) {
if (pattern == null) {
return this.bom.getLibraries();
}
Predicate<String> libraryPredicate = Pattern.compile(pattern).asPredicate();
List<Library> matchingLibraries = this.bom.getLibraries().stream()
.filter((library) -> libraryPredicate.test(library.getName())).collect(Collectors.toList());
if (matchingLibraries.isEmpty()) {
throw new InvalidUserDataException("No libraries matched '" + pattern + "'");
}
return matchingLibraries;
}
protected abstract String issueTitle(Upgrade upgrade);
protected abstract String commitMessage(Upgrade upgrade, int issueNumber);
}

@ -35,10 +35,13 @@ public class Issue {
private final String title;
Issue(RestTemplate rest, int number, String title) {
private final State state;
Issue(RestTemplate rest, int number, String title, State state) {
this.rest = rest;
this.number = number;
this.title = title;
this.state = state;
}
public int getNumber() {
@ -49,6 +52,10 @@ public class Issue {
return this.title;
}
public State getState() {
return this.state;
}
/**
* Labels the issue with the given {@code labels}. Any existing labels are removed.
* @param labels the labels to apply to the issue
@ -58,4 +65,30 @@ public class Issue {
this.rest.put("issues/" + this.number + "/labels", body);
}
public enum State {
/**
* The issue is open.
*/
OPEN,
/**
* The issue is closed.
*/
CLOSED;
static State of(String state) {
if ("open".equals(state)) {
return OPEN;
}
if ("closed".equals(state)) {
return CLOSED;
}
else {
throw new IllegalArgumentException("Unknown state '" + state + "'");
}
}
}
}

@ -85,7 +85,8 @@ final class StandardGitHubRepository implements GitHubRepository {
return get(
"issues?per_page=100&state=all&labels=" + String.join(",", labels) + "&milestone="
+ milestone.getNumber(),
(issue) -> new Issue(this.rest, (Integer) issue.get("number"), (String) issue.get("title")));
(issue) -> new Issue(this.rest, (Integer) issue.get("number"), (String) issue.get("title"),
Issue.State.of((String) issue.get("state"))));
}
@SuppressWarnings({ "rawtypes", "unchecked" })

Loading…
Cancel
Save