Add UpdateDependencies Plugin
Closes gh-2343
This commit is contained in:
19
build.gradle
19
build.gradle
@@ -25,6 +25,7 @@ buildscript {
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.convention.root'
|
||||
apply plugin: 'org.springframework.security.update-dependencies'
|
||||
|
||||
group = 'org.springframework.session'
|
||||
description = 'Spring Session'
|
||||
@@ -46,3 +47,21 @@ nohttp {
|
||||
source.exclude "spring-session-docs/.gradle/nodejs/**"
|
||||
source.exclude "spring-session-docs/modules/ROOT/examples/**/build/**"
|
||||
}
|
||||
|
||||
updateDependenciesSettings {
|
||||
gitHub {
|
||||
organization = "spring-projects"
|
||||
repository = "spring-session"
|
||||
}
|
||||
dependencyExcludes {
|
||||
majorVersionBump()
|
||||
minorVersionBump()
|
||||
alphaBetaVersions()
|
||||
snapshotVersions()
|
||||
}
|
||||
addFiles({
|
||||
return [
|
||||
project.file("gradle/dependency-management.gradle")
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ plugins {
|
||||
id "java-gradle-plugin"
|
||||
id "java"
|
||||
id "groovy"
|
||||
id 'com.apollographql.apollo' version '2.4.5'
|
||||
}
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
@@ -46,6 +47,10 @@ gradlePlugin {
|
||||
id = "org.springframework.propdeps"
|
||||
implementationClass = "org.springframework.gradle.propdeps.PropDepsPlugin"
|
||||
}
|
||||
updateDependencies {
|
||||
id = "org.springframework.security.update-dependencies"
|
||||
implementationClass = "org.springframework.security.convention.versions.UpdateDependenciesPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation CreateIssueInput($assigneeId: ID!, $labelIds: [ID!], $milestoneId: ID!, $repositoryId: ID!, $title: String!) {
|
||||
createIssue(input: {assigneeIds: [$assigneeId], labelIds: $labelIds, milestoneId: $milestoneId, projectIds: [], repositoryId: $repositoryId, title: $title}) {
|
||||
issue {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
query FindCreateIssueInput($owner: String!, $name: String!, $labelQuery: String, $milestoneName: String) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
id
|
||||
labels(query: $labelQuery, first: 1) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
milestones(query: $milestoneName, states: [OPEN], first: 1) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
viewer {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
query RateLimit {
|
||||
rateLimit {
|
||||
limit
|
||||
cost
|
||||
remaining
|
||||
resetAt
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019-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.security.convention.versions;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Scanner;
|
||||
|
||||
class CommandLineUtils {
|
||||
static void runCommand(File dir, String... args) {
|
||||
try {
|
||||
Process process = new ProcessBuilder()
|
||||
.directory(dir)
|
||||
.command(args)
|
||||
.start();
|
||||
writeLinesTo(process.getInputStream(), System.out);
|
||||
writeLinesTo(process.getErrorStream(), System.out);
|
||||
if (process.waitFor() != 0) {
|
||||
new RuntimeException("Failed to run " + Arrays.toString(args));
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new RuntimeException("Failed to run " + Arrays.toString(args), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeLinesTo(InputStream input, PrintStream out) {
|
||||
Scanner scanner = new Scanner(input);
|
||||
while(scanner.hasNextLine()) {
|
||||
out.println(scanner.nextLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019-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.security.convention.versions;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.function.Function;
|
||||
|
||||
class FileUtils {
|
||||
static void replaceFileText(File file, Function<String, String> replaceText) {
|
||||
String buildFileText = readString(file);
|
||||
String updatedBuildFileText = replaceText.apply(buildFileText);
|
||||
writeString(file, updatedBuildFileText);
|
||||
}
|
||||
|
||||
static String readString(File file) {
|
||||
try {
|
||||
byte[] bytes = Files.readAllBytes(file.toPath());
|
||||
return new String(bytes);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeString(File file, String text) {
|
||||
try {
|
||||
Files.write(file.toPath(), text.getBytes());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package org.springframework.security.convention.versions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.apollographql.apollo.ApolloCall;
|
||||
import com.apollographql.apollo.ApolloClient;
|
||||
import com.apollographql.apollo.api.Input;
|
||||
import com.apollographql.apollo.api.Response;
|
||||
import com.apollographql.apollo.exception.ApolloException;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.RetrySpec;
|
||||
|
||||
public class GitHubApi {
|
||||
|
||||
private final ApolloClient apolloClient;
|
||||
|
||||
public GitHubApi(String githubToken) {
|
||||
if (githubToken == null) {
|
||||
throw new IllegalArgumentException("githubToken is required. You can set it using -PgitHubAccessToken=");
|
||||
}
|
||||
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
|
||||
clientBuilder.addInterceptor(new AuthorizationInterceptor(githubToken));
|
||||
this.apolloClient = ApolloClient.builder()
|
||||
.serverUrl("https://api.github.com/graphql")
|
||||
.okHttpClient(clientBuilder.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public Mono<FindCreateIssueResult> findCreateIssueInput(String owner, String name, String milestone) {
|
||||
String label = "\"type: dependency-upgrade\"";
|
||||
FindCreateIssueInputQuery findCreateIssueInputQuery = new FindCreateIssueInputQuery(owner, name, Input.optional(label), Input.optional(milestone));
|
||||
return Mono.create( sink -> this.apolloClient.query(findCreateIssueInputQuery)
|
||||
.enqueue(new ApolloCall.Callback<>() {
|
||||
@Override
|
||||
public void onResponse(@NotNull Response<FindCreateIssueInputQuery.Data> response) {
|
||||
if (response.hasErrors()) {
|
||||
sink.error(new RuntimeException(response.getErrors().stream().map(e -> e.getMessage()).collect(Collectors.joining(" "))));
|
||||
} else {
|
||||
FindCreateIssueInputQuery.Data data = response.getData();
|
||||
FindCreateIssueInputQuery.Repository repository = data.repository();
|
||||
List<String> labels = repository.labels().nodes().stream().map(FindCreateIssueInputQuery.Node::id).collect(Collectors.toList());
|
||||
if (labels.isEmpty()) {
|
||||
sink.error(new IllegalArgumentException("Could not find label for " + label));
|
||||
return;
|
||||
}
|
||||
Optional<String> firstMilestoneId = repository.milestones().nodes().stream().map(FindCreateIssueInputQuery.Node1::id).findFirst();
|
||||
if (!firstMilestoneId.isPresent()) {
|
||||
sink.error(new IllegalArgumentException("Could not find OPEN milestone id for " + milestone));
|
||||
return;
|
||||
}
|
||||
String milestoneId = firstMilestoneId.get();
|
||||
String repositoryId = repository.id();
|
||||
String assigneeId = data.viewer().id();
|
||||
sink.success(new FindCreateIssueResult(repositoryId, labels, milestoneId, assigneeId));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull ApolloException e) {
|
||||
sink.error(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public static class FindCreateIssueResult {
|
||||
private final String repositoryId;
|
||||
private final List<String> labelIds;
|
||||
private final String milestoneId;
|
||||
private final String assigneeId;
|
||||
|
||||
public FindCreateIssueResult(String repositoryId, List<String> labelIds, String milestoneId, String assigneeId) {
|
||||
this.repositoryId = repositoryId;
|
||||
this.labelIds = labelIds;
|
||||
this.milestoneId = milestoneId;
|
||||
this.assigneeId = assigneeId;
|
||||
}
|
||||
|
||||
public String getRepositoryId() {
|
||||
return repositoryId;
|
||||
}
|
||||
|
||||
public List<String> getLabelIds() {
|
||||
return labelIds;
|
||||
}
|
||||
|
||||
public String getMilestoneId() {
|
||||
return milestoneId;
|
||||
}
|
||||
|
||||
public String getAssigneeId() {
|
||||
return assigneeId;
|
||||
}
|
||||
}
|
||||
|
||||
public Mono<RateLimitQuery.RateLimit> findRateLimit() {
|
||||
return Mono.create( sink -> this.apolloClient.query(new RateLimitQuery())
|
||||
.enqueue(new ApolloCall.Callback<>() {
|
||||
@Override
|
||||
public void onResponse(@NotNull Response<RateLimitQuery.Data> response) {
|
||||
if (response.hasErrors()) {
|
||||
sink.error(new RuntimeException(response.getErrors().stream().map(e -> e.getMessage()).collect(Collectors.joining(" "))));
|
||||
} else {
|
||||
sink.success(response.getData().rateLimit());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull ApolloException e) {
|
||||
sink.error(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public Mono<Integer> createIssue(String repositoryId, String title, List<String> labelIds, String milestoneId, String assigneeId) {
|
||||
CreateIssueInputMutation createIssue = new CreateIssueInputMutation.Builder()
|
||||
.repositoryId(repositoryId)
|
||||
.title(title)
|
||||
.labelIds(labelIds)
|
||||
.milestoneId(milestoneId)
|
||||
.assigneeId(assigneeId)
|
||||
.build();
|
||||
return Mono.create( sink -> this.apolloClient.mutate(createIssue)
|
||||
.enqueue(new ApolloCall.Callback<>() {
|
||||
@Override
|
||||
public void onResponse(@NotNull Response<CreateIssueInputMutation.Data> response) {
|
||||
if (response.hasErrors()) {
|
||||
String message = response.getErrors().stream().map(e -> e.getMessage() + " " + e.getCustomAttributes() + " " + e.getLocations()).collect(Collectors.joining(" "));
|
||||
if (message.contains("was submitted too quickly")) {
|
||||
sink.error(new SubmittedTooQuick(message));
|
||||
} else {
|
||||
sink.error(new RuntimeException(message));
|
||||
}
|
||||
} else {
|
||||
sink.success(response.getData().createIssue().issue().number());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull ApolloException e) {
|
||||
sink.error(e);
|
||||
}
|
||||
}))
|
||||
.retryWhen(
|
||||
RetrySpec.fixedDelay(3, Duration.ofMinutes(1))
|
||||
.filter(SubmittedTooQuick.class::isInstance)
|
||||
.doBeforeRetry(r -> System.out.println("Pausing for 1 minute and then retrying due to receiving \"submitted too quickly\" error from GitHub API"))
|
||||
)
|
||||
.cast(Integer.class);
|
||||
}
|
||||
|
||||
public static class SubmittedTooQuick extends RuntimeException {
|
||||
public SubmittedTooQuick(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AuthorizationInterceptor implements Interceptor {
|
||||
|
||||
private final String token;
|
||||
|
||||
public AuthorizationInterceptor(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public okhttp3.Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request().newBuilder()
|
||||
.addHeader("Authorization", "Bearer " + this.token).build();
|
||||
return chain.proceed(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2019-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.security.convention.versions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.w3c.dom.Document;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
class TransitiveDependencyLookupUtils {
|
||||
static String OIDC_SDK_NAME = "oauth2-oidc-sdk";
|
||||
static String NIMBUS_JOSE_JWT_NAME = "nimbus-jose-jwt";
|
||||
|
||||
private static OkHttpClient client = new OkHttpClient();
|
||||
|
||||
static String lookupJwtVersion(String oauthSdcVersion) {
|
||||
Request request = new Request.Builder()
|
||||
.get()
|
||||
.url("https://repo.maven.apache.org/maven2/com/nimbusds/" + OIDC_SDK_NAME + "/" + oauthSdcVersion + "/" + OIDC_SDK_NAME + "-" + oauthSdcVersion + ".pom")
|
||||
.build();
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("Unexpected code " + response);
|
||||
}
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
return getVersion(inputStream);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getVersion(InputStream inputStream) throws ParserConfigurationException, IOException, SAXException, XPathExpressionException {
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
DocumentBuilder db = dbf.newDocumentBuilder();
|
||||
|
||||
Document doc = db.parse(inputStream);
|
||||
|
||||
doc.getDocumentElement().normalize();
|
||||
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
return xPath.evaluate("/project/dependencies/dependency/version[../artifactId/text() = \"" + NIMBUS_JOSE_JWT_NAME + "\"]", doc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package org.springframework.security.convention.versions;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionRulesWithCurrent;
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionWithCurrent;
|
||||
import org.gradle.api.Action;
|
||||
|
||||
public class UpdateDependenciesExtension {
|
||||
private Supplier<List<File>> files;
|
||||
|
||||
private UpdateMode updateMode = UpdateMode.COMMIT;
|
||||
|
||||
private DependencyExcludes dependencyExcludes = new DependencyExcludes();
|
||||
|
||||
private GitHub gitHub = new GitHub();
|
||||
|
||||
public UpdateDependenciesExtension(Supplier<List<File>> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public void setUpdateMode(UpdateMode updateMode) {
|
||||
this.updateMode = updateMode;
|
||||
}
|
||||
|
||||
public UpdateMode getUpdateMode() {
|
||||
return updateMode;
|
||||
}
|
||||
|
||||
GitHub getGitHub() {
|
||||
return this.gitHub;
|
||||
}
|
||||
|
||||
DependencyExcludes getExcludes() {
|
||||
return dependencyExcludes;
|
||||
}
|
||||
|
||||
Supplier<List<File>> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public void setFiles(Supplier<List<File>> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public void addFiles(Supplier<List<File>> files) {
|
||||
Supplier<List<File>> original = this.files;
|
||||
setFiles(() -> {
|
||||
List<File> result = new ArrayList<>(original.get());
|
||||
result.addAll(files.get());
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public void dependencyExcludes(Action<DependencyExcludes> excludes) {
|
||||
excludes.execute(this.dependencyExcludes);
|
||||
}
|
||||
|
||||
public void gitHub(Action<GitHub> gitHub) {
|
||||
gitHub.execute(this.gitHub);
|
||||
}
|
||||
|
||||
public enum UpdateMode {
|
||||
COMMIT,
|
||||
GITHUB_ISSUE
|
||||
}
|
||||
|
||||
public class GitHub {
|
||||
private String organization;
|
||||
|
||||
private String repository;
|
||||
|
||||
private String accessToken;
|
||||
|
||||
private String milestone;
|
||||
|
||||
public String getOrganization() {
|
||||
return organization;
|
||||
}
|
||||
|
||||
public void setOrganization(String organization) {
|
||||
this.organization = organization;
|
||||
}
|
||||
|
||||
public String getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public void setRepository(String repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
public String getMilestone() {
|
||||
return milestone;
|
||||
}
|
||||
|
||||
public void setMilestone(String milestone) {
|
||||
this.milestone = milestone;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consider creating some Predicates instead since they are composible
|
||||
*/
|
||||
public class DependencyExcludes {
|
||||
private List<Action<ComponentSelectionWithCurrent>> actions = new ArrayList<>();
|
||||
private List<Action<ComponentSelectionRulesWithCurrent>> components = new ArrayList<>();
|
||||
|
||||
List<Action<ComponentSelectionWithCurrent>> getActions() {
|
||||
return actions;
|
||||
}
|
||||
|
||||
public List<Action<ComponentSelectionRulesWithCurrent>> getComponents() {
|
||||
return components;
|
||||
}
|
||||
|
||||
public DependencyExcludes alphaBetaVersions() {
|
||||
this.actions.add(excludeVersionWithRegex("(?i).*?(alpha|beta).*", "an alpha or beta version"));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DependencyExcludes majorVersionBump() {
|
||||
this.actions.add((selection) -> {
|
||||
String currentVersion = selection.getCurrentVersion();
|
||||
int separator = currentVersion.indexOf(".");
|
||||
String major = separator > 0 ? currentVersion.substring(0, separator) : currentVersion;
|
||||
String candidateVersion = selection.getCandidate().getVersion();
|
||||
Pattern calVerPattern = Pattern.compile("\\d\\d\\d\\d.*");
|
||||
boolean isCalVer = calVerPattern.matcher(candidateVersion).matches();
|
||||
if (!isCalVer && !candidateVersion.startsWith(major)) {
|
||||
selection.reject("Cannot upgrade to new Major Version");
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public DependencyExcludes minorVersionBump() {
|
||||
this.actions.add(createExcludeMinorVersionBump());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Action<ComponentSelectionWithCurrent> createExcludeMinorVersionBump() {
|
||||
return (selection) -> {
|
||||
String currentVersion = selection.getCurrentVersion();
|
||||
int majorSeparator = currentVersion.indexOf(".");
|
||||
int separator = currentVersion.indexOf(".", majorSeparator + 1);
|
||||
String majorMinor = separator > 0 ? currentVersion.substring(0, separator) : currentVersion;
|
||||
String candidateVersion = selection.getCandidate().getVersion();
|
||||
if (!candidateVersion.startsWith(majorMinor)) {
|
||||
selection.reject("Cannot upgrade to new Minor Version");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public DependencyExcludes releaseCandidatesVersions() {
|
||||
this.actions.add(excludeVersionWithRegex("(?i).*?rc.*", "a release candidate version"));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DependencyExcludes milestoneVersions() {
|
||||
this.actions.add(excludeVersionWithRegex("(?i).*?m\\d+.*", "a milestone version"));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DependencyExcludes snapshotVersions() {
|
||||
this.actions.add(excludeVersionWithRegex(".*?-SNAPSHOT.*", "a SNAPSHOT version"));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DependencyExcludes addRule(Action<ComponentSelectionRulesWithCurrent> rule) {
|
||||
this.components.add(rule);
|
||||
return this;
|
||||
}
|
||||
|
||||
private Action<ComponentSelectionWithCurrent> excludeVersionWithRegex(String regex, String reason) {
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
return (selection) -> {
|
||||
String candidateVersion = selection.getCandidate().getVersion();
|
||||
if (pattern.matcher(candidateVersion).matches()) {
|
||||
selection.reject(candidateVersion + " is not allowed because it is " + reason);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright 2019-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.security.convention.versions;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SortedSet;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.github.benmanes.gradle.versions.reporter.result.Dependency;
|
||||
import com.github.benmanes.gradle.versions.reporter.result.DependencyOutdated;
|
||||
import com.github.benmanes.gradle.versions.reporter.result.Result;
|
||||
import com.github.benmanes.gradle.versions.reporter.result.VersionAvailable;
|
||||
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask;
|
||||
import com.github.benmanes.gradle.versions.updates.gradle.GradleUpdateResult;
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionRulesWithCurrent;
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ComponentSelectionWithCurrent;
|
||||
import com.github.benmanes.gradle.versions.updates.resolutionstrategy.ResolutionStrategyWithCurrent;
|
||||
import groovy.lang.Closure;
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.springframework.security.convention.versions.TransitiveDependencyLookupUtils.NIMBUS_JOSE_JWT_NAME;
|
||||
import static org.springframework.security.convention.versions.TransitiveDependencyLookupUtils.OIDC_SDK_NAME;
|
||||
|
||||
public class UpdateDependenciesPlugin implements Plugin<Project> {
|
||||
private GitHubApi gitHubApi;
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
UpdateDependenciesExtension updateDependenciesSettings = project.getExtensions().create("updateDependenciesSettings", UpdateDependenciesExtension.class, defaultFiles(project));
|
||||
if (project.hasProperty("updateMode")) {
|
||||
String updateMode = String.valueOf(project.findProperty("updateMode"));
|
||||
updateDependenciesSettings.setUpdateMode(UpdateDependenciesExtension.UpdateMode.valueOf(updateMode));
|
||||
}
|
||||
if (project.hasProperty("nextVersion")) {
|
||||
String nextVersion = String.valueOf(project.findProperty("nextVersion"));
|
||||
updateDependenciesSettings.getGitHub().setMilestone(nextVersion);
|
||||
}
|
||||
if (project.hasProperty("gitHubAccessToken")) {
|
||||
String gitHubAccessToken = String.valueOf(project.findProperty("gitHubAccessToken"));
|
||||
updateDependenciesSettings.getGitHub().setAccessToken(gitHubAccessToken);
|
||||
}
|
||||
project.getTasks().register("updateDependencies", DependencyUpdatesTask.class, new Action<DependencyUpdatesTask>() {
|
||||
@Override
|
||||
public void execute(DependencyUpdatesTask updateDependencies) {
|
||||
updateDependencies.setDescription("Update the dependencies");
|
||||
updateDependencies.setCheckConstraints(true);
|
||||
updateDependencies.setOutputFormatter(new Closure<Void>(null) {
|
||||
@Override
|
||||
public Void call(Object argument) {
|
||||
Result result = (Result) argument;
|
||||
if (gitHubApi == null && updateDependenciesSettings.getUpdateMode() != UpdateDependenciesExtension.UpdateMode.COMMIT) {
|
||||
gitHubApi = new GitHubApi(updateDependenciesSettings.getGitHub().getAccessToken());
|
||||
}
|
||||
updateDependencies(result, project, updateDependenciesSettings);
|
||||
updateGradleVersion(result, project, updateDependenciesSettings);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
updateDependencies.resolutionStrategy(new Action<ResolutionStrategyWithCurrent>() {
|
||||
@Override
|
||||
public void execute(ResolutionStrategyWithCurrent resolution) {
|
||||
resolution.componentSelection(new Action<ComponentSelectionRulesWithCurrent>() {
|
||||
@Override
|
||||
public void execute(ComponentSelectionRulesWithCurrent components) {
|
||||
updateDependenciesSettings.getExcludes().getActions().forEach((action) -> {
|
||||
components.all(action);
|
||||
});
|
||||
updateDependenciesSettings.getExcludes().getComponents().forEach((action) -> {
|
||||
action.execute(components);
|
||||
});
|
||||
components.all((selection) -> {
|
||||
ModuleComponentIdentifier candidate = selection.getCandidate();
|
||||
if (candidate.getGroup().startsWith("org.apache.directory.") && !candidate.getVersion().equals(selection.getCurrentVersion())) {
|
||||
selection.reject("org.apache.directory.* has breaking changes in newer versions");
|
||||
}
|
||||
});
|
||||
String jaxbBetaRegex = ".*?b\\d+.*";
|
||||
components.withModule("javax.xml.bind:jaxb-api", excludeWithRegex(jaxbBetaRegex, "Reject jaxb-api beta versions"));
|
||||
components.withModule("com.sun.xml.bind:jaxb-impl", excludeWithRegex(jaxbBetaRegex, "Reject jaxb-api beta versions"));
|
||||
components.withModule("commons-collections:commons-collections", excludeWithRegex("^\\d{3,}.*", "Reject commons-collections date based releases"));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateDependencies(Result result, Project project, UpdateDependenciesExtension updateDependenciesSettings) {
|
||||
SortedSet<DependencyOutdated> dependencies = result.getOutdated().getDependencies();
|
||||
if (dependencies.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, List<DependencyOutdated>> groups = new LinkedHashMap<>();
|
||||
dependencies.forEach(outdated -> {
|
||||
groups.computeIfAbsent(outdated.getGroup(), (key) -> new ArrayList<>()).add(outdated);
|
||||
});
|
||||
List<DependencyOutdated> nimbusds = groups.getOrDefault("com.nimbusds", new ArrayList<>());
|
||||
DependencyOutdated oidcSdc = nimbusds.stream().filter(d -> d.getName().equals(OIDC_SDK_NAME)).findFirst().orElseGet(() -> null);
|
||||
if(oidcSdc != null) {
|
||||
String oidcVersion = updatedVersion(oidcSdc);
|
||||
String jwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(oidcVersion);
|
||||
|
||||
Dependency nimbusJoseJwtDependency = result.getCurrent().getDependencies().stream().filter(d -> d.getName().equals(NIMBUS_JOSE_JWT_NAME)).findFirst().get();
|
||||
DependencyOutdated outdatedJwt = new DependencyOutdated();
|
||||
outdatedJwt.setVersion(nimbusJoseJwtDependency.getVersion());
|
||||
outdatedJwt.setGroup(oidcSdc.getGroup());
|
||||
outdatedJwt.setName(NIMBUS_JOSE_JWT_NAME);
|
||||
VersionAvailable available = new VersionAvailable();
|
||||
available.setRelease(jwtVersion);
|
||||
outdatedJwt.setAvailable(available);
|
||||
nimbusds.add(outdatedJwt);
|
||||
}
|
||||
File gradlePropertiesFile = project.getRootProject().file(Project.GRADLE_PROPERTIES);
|
||||
Mono<GitHubApi.FindCreateIssueResult> createIssueResult = createIssueResultMono(updateDependenciesSettings);
|
||||
List<File> filesWithDependencies = updateDependenciesSettings.getFiles().get();
|
||||
groups.forEach((group, outdated) -> {
|
||||
outdated.forEach((dependency) -> {
|
||||
String ga = dependency.getGroup() + ":" + dependency.getName() + ":";
|
||||
String originalDependency = ga + dependency.getVersion();
|
||||
String replacementDependency = ga + updatedVersion(dependency);
|
||||
System.out.println("Update " + originalDependency + " to " + replacementDependency);
|
||||
filesWithDependencies.forEach((fileWithDependency) -> {
|
||||
updateDependencyInlineVersion(fileWithDependency, dependency);
|
||||
updateDependencyWithVersionVariable(fileWithDependency, gradlePropertiesFile, dependency);
|
||||
});
|
||||
});
|
||||
|
||||
// commit
|
||||
DependencyOutdated firstDependency = outdated.get(0);
|
||||
String updatedVersion = updatedVersion(firstDependency);
|
||||
String title = outdated.size() == 1 ? "Update " + firstDependency.getName() + " to " + updatedVersion : "Update " + firstDependency.getGroup() + " to " + updatedVersion;
|
||||
afterGroup(updateDependenciesSettings, project.getRootDir(), title, createIssueResult);
|
||||
});
|
||||
}
|
||||
|
||||
private void afterGroup(UpdateDependenciesExtension updateDependenciesExtension, File rootDir, String title, Mono<GitHubApi.FindCreateIssueResult> createIssueResultMono) {
|
||||
|
||||
String commitMessage = title;
|
||||
if (updateDependenciesExtension.getUpdateMode() == UpdateDependenciesExtension.UpdateMode.GITHUB_ISSUE) {
|
||||
GitHubApi.FindCreateIssueResult createIssueResult = createIssueResultMono.block();
|
||||
Integer issueNumber = gitHubApi.createIssue(createIssueResult.getRepositoryId(), title, createIssueResult.getLabelIds(), createIssueResult.getMilestoneId(), createIssueResult.getAssigneeId()).delayElement(Duration.ofSeconds(1)).block();
|
||||
commitMessage += "\n\nCloses gh-" + issueNumber;
|
||||
}
|
||||
CommandLineUtils.runCommand(rootDir, "git", "commit", "-am", commitMessage);
|
||||
}
|
||||
|
||||
private Mono<GitHubApi.FindCreateIssueResult> createIssueResultMono(UpdateDependenciesExtension updateDependenciesExtension) {
|
||||
return Mono.defer(() -> {
|
||||
UpdateDependenciesExtension.GitHub gitHub = updateDependenciesExtension.getGitHub();
|
||||
return gitHubApi.findCreateIssueInput(gitHub.getOrganization(), gitHub.getRepository(), gitHub.getMilestone()).cache();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateGradleVersion(Result result, Project project, UpdateDependenciesExtension updateDependenciesSettings) {
|
||||
if (!result.getGradle().isEnabled()) {
|
||||
return;
|
||||
}
|
||||
GradleUpdateResult current = result.getGradle().getCurrent();
|
||||
GradleUpdateResult running = result.getGradle().getRunning();
|
||||
if (current.compareTo(running) > 0) {
|
||||
String title = "Update Gradle to " + current.getVersion();
|
||||
System.out.println(title);
|
||||
CommandLineUtils.runCommand(project.getRootDir(), "./gradlew", "wrapper", "--gradle-version", current.getVersion(), "--no-daemon");
|
||||
afterGroup(updateDependenciesSettings, project.getRootDir(), title, createIssueResultMono(updateDependenciesSettings));
|
||||
}
|
||||
}
|
||||
|
||||
private static Supplier<List<File>> defaultFiles(Project project) {
|
||||
return () -> {
|
||||
List<File> result = new ArrayList<>();
|
||||
result.add(project.getBuildFile());
|
||||
project.getChildProjects().values().forEach((childProject) ->
|
||||
result.add(childProject.getBuildFile())
|
||||
);
|
||||
result.add(project.getRootProject().file("buildSrc/build.gradle"));
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
static Action<ComponentSelectionWithCurrent> excludeWithRegex(String regex, String reason) {
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
return (selection) -> {
|
||||
String candidateVersion = selection.getCandidate().getVersion();
|
||||
if (pattern.matcher(candidateVersion).matches()) {
|
||||
selection.reject(candidateVersion + " is not allowed because it is " + reason);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static void updateDependencyInlineVersion(File buildFile, DependencyOutdated dependency){
|
||||
String ga = dependency.getGroup() + ":" + dependency.getName() + ":";
|
||||
String originalDependency = ga + dependency.getVersion();
|
||||
String replacementDependency = ga + updatedVersion(dependency);
|
||||
FileUtils.replaceFileText(buildFile, buildFileText -> buildFileText.replace(originalDependency, replacementDependency));
|
||||
}
|
||||
|
||||
static void updateDependencyWithVersionVariable(File scanFile, File gradlePropertiesFile, DependencyOutdated dependency) {
|
||||
if (!gradlePropertiesFile.exists()) {
|
||||
return;
|
||||
}
|
||||
FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> {
|
||||
String ga = dependency.getGroup() + ":" + dependency.getName() + ":";
|
||||
Pattern pattern = Pattern.compile("\"" + ga + "\\$\\{?([^'\"]+?)\\}?\"");
|
||||
String buildFileText = FileUtils.readString(scanFile);
|
||||
Matcher matcher = pattern.matcher(buildFileText);
|
||||
while (matcher.find()) {
|
||||
String versionVariable = matcher.group(1);
|
||||
gradlePropertiesText = gradlePropertiesText.replace(versionVariable + "=" + dependency.getVersion(), versionVariable + "=" + updatedVersion(dependency));
|
||||
}
|
||||
return gradlePropertiesText;
|
||||
});
|
||||
}
|
||||
|
||||
private static String updatedVersion(DependencyOutdated dependency) {
|
||||
VersionAvailable available = dependency.getAvailable();
|
||||
String release = available.getRelease();
|
||||
if (release != null) {
|
||||
return release;
|
||||
}
|
||||
return available.getMilestone();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user