commit
a6f357c2fd
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.MavenRepositoryPlugin;
|
||||||
import org.springframework.boot.build.bom.Library.Group;
|
import org.springframework.boot.build.bom.Library.Group;
|
||||||
import org.springframework.boot.build.bom.Library.Module;
|
import org.springframework.boot.build.bom.Library.Module;
|
||||||
|
import org.springframework.boot.build.bom.bomr.MoveToSnapshots;
|
||||||
import org.springframework.boot.build.bom.bomr.UpgradeBom;
|
import org.springframework.boot.build.bom.bomr.UpgradeBom;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -63,6 +64,7 @@ public class BomPlugin implements Plugin<Project> {
|
||||||
project);
|
project);
|
||||||
project.getTasks().create("bomrCheck", CheckBom.class, bom);
|
project.getTasks().create("bomrCheck", CheckBom.class, bom);
|
||||||
project.getTasks().create("bomrUpgrade", UpgradeBom.class, bom);
|
project.getTasks().create("bomrUpgrade", UpgradeBom.class, bom);
|
||||||
|
project.getTasks().create("moveToSnapshots", MoveToSnapshots.class, bom);
|
||||||
new PublishingCustomizer(project, bom).customize();
|
new PublishingCustomizer(project, bom).customize();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.boot.build.bom.bomr;
|
package org.springframework.boot.build.bom.bomr;
|
||||||
|
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
|
import java.net.URI;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
@ -49,13 +50,13 @@ final class MavenMetadataVersionResolver implements VersionResolver {
|
||||||
|
|
||||||
private final RestTemplate rest;
|
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);
|
this(new RestTemplate(Collections.singletonList(new StringHttpMessageConverter())), repositoryUrls);
|
||||||
}
|
}
|
||||||
|
|
||||||
MavenMetadataVersionResolver(RestTemplate restTemplate, Collection<String> repositoryUrls) {
|
MavenMetadataVersionResolver(RestTemplate restTemplate, Collection<URI> repositoryUrls) {
|
||||||
this.rest = restTemplate;
|
this.rest = restTemplate;
|
||||||
this.repositoryUrls = repositoryUrls;
|
this.repositoryUrls = repositoryUrls;
|
||||||
}
|
}
|
||||||
|
|
@ -63,15 +64,15 @@ final class MavenMetadataVersionResolver implements VersionResolver {
|
||||||
@Override
|
@Override
|
||||||
public SortedSet<DependencyVersion> resolveVersions(String groupId, String artifactId) {
|
public SortedSet<DependencyVersion> resolveVersions(String groupId, String artifactId) {
|
||||||
Set<String> versions = new HashSet<>();
|
Set<String> versions = new HashSet<>();
|
||||||
for (String repositoryUrl : this.repositoryUrls) {
|
for (URI repositoryUrl : this.repositoryUrls) {
|
||||||
versions.addAll(resolveVersions(groupId, artifactId, repositoryUrl));
|
versions.addAll(resolveVersions(groupId, artifactId, repositoryUrl));
|
||||||
}
|
}
|
||||||
return versions.stream().map(DependencyVersion::parse).collect(Collectors.toCollection(TreeSet::new));
|
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<>();
|
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 {
|
try {
|
||||||
String metadata = this.rest.getForObject(url, String.class);
|
String metadata = this.rest.getForObject(url, String.class);
|
||||||
Document metadataDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
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,40 +16,14 @@
|
||||||
|
|
||||||
package org.springframework.boot.build.bom.bomr;
|
package org.springframework.boot.build.bom.bomr;
|
||||||
|
|
||||||
import java.io.File;
|
import java.net.URI;
|
||||||
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 javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import org.gradle.api.DefaultTask;
|
|
||||||
import org.gradle.api.InvalidUserDataException;
|
|
||||||
import org.gradle.api.Task;
|
import org.gradle.api.Task;
|
||||||
import org.gradle.api.artifacts.repositories.MavenArtifactRepository;
|
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.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.
|
* {@link Task} to upgrade the libraries managed by a bom.
|
||||||
|
|
@ -57,161 +31,27 @@ import org.springframework.util.StringUtils;
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
* @author Moritz Halbritter
|
* @author Moritz Halbritter
|
||||||
*/
|
*/
|
||||||
public class UpgradeBom extends DefaultTask {
|
public abstract class UpgradeBom extends UpgradeDependencies {
|
||||||
|
|
||||||
private final Set<String> repositoryUrls = new LinkedHashSet<>();
|
|
||||||
|
|
||||||
private final BomExtension bom;
|
|
||||||
|
|
||||||
private String milestone;
|
|
||||||
|
|
||||||
private String libraries;
|
|
||||||
|
|
||||||
private int threads = 2;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public UpgradeBom(BomExtension bom) {
|
public UpgradeBom(BomExtension bom) {
|
||||||
this.bom = bom;
|
super(bom);
|
||||||
getProject().getRepositories().withType(MavenArtifactRepository.class, (repository) -> {
|
getProject().getRepositories().withType(MavenArtifactRepository.class, (repository) -> {
|
||||||
String repositoryUrl = repository.getUrl().toString();
|
URI repositoryUrl = repository.getUrl();
|
||||||
if (!repositoryUrl.endsWith("snapshot")) {
|
if (!repositoryUrl.toString().endsWith("snapshot")) {
|
||||||
this.repositoryUrls.add(repositoryUrl);
|
getRepositoryUris().add(repositoryUrl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Option(option = "milestone", description = "Milestone to which dependency upgrade issues should be assigned")
|
@Override
|
||||||
public void setMilestone(String milestone) {
|
protected String issueTitle(Upgrade upgrade) {
|
||||||
this.milestone = milestone;
|
return "Upgrade to " + upgrade.getLibrary().getName() + " " + upgrade.getVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Option(option = "threads", description = "Number of Threads to use for update resolution")
|
@Override
|
||||||
public void setThreads(String threads) {
|
protected String commitMessage(Upgrade upgrade, int issueNumber) {
|
||||||
this.threads = Integer.parseInt(threads);
|
return issueTitle(upgrade) + "\n\nCloses gh-" + issueNumber;
|
||||||
}
|
|
||||||
|
|
||||||
@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())).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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
/*
|
||||||
|
* 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 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())).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;
|
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.rest = rest;
|
||||||
this.number = number;
|
this.number = number;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getNumber() {
|
public int getNumber() {
|
||||||
|
|
@ -49,6 +52,10 @@ public class Issue {
|
||||||
return this.title;
|
return this.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public State getState() {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Labels the issue with the given {@code labels}. Any existing labels are removed.
|
* Labels the issue with the given {@code labels}. Any existing labels are removed.
|
||||||
* @param labels the labels to apply to the issue
|
* @param labels the labels to apply to the issue
|
||||||
|
|
@ -58,4 +65,30 @@ public class Issue {
|
||||||
this.rest.put("issues/" + this.number + "/labels", body);
|
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 + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,8 @@ final class StandardGitHubRepository implements GitHubRepository {
|
||||||
return get(
|
return get(
|
||||||
"issues?per_page=100&state=all&labels=" + String.join(",", labels) + "&milestone="
|
"issues?per_page=100&state=all&labels=" + String.join(",", labels) + "&milestone="
|
||||||
+ milestone.getNumber(),
|
+ 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" })
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue