diff --git a/src/main/java/io/micronaut/build/catalogs/MicronautVersionCatalogUpdatePlugin.java b/src/main/java/io/micronaut/build/catalogs/MicronautVersionCatalogUpdatePlugin.java index 6cdf0e5b..69778ecc 100644 --- a/src/main/java/io/micronaut/build/catalogs/MicronautVersionCatalogUpdatePlugin.java +++ b/src/main/java/io/micronaut/build/catalogs/MicronautVersionCatalogUpdatePlugin.java @@ -28,6 +28,7 @@ public void apply(Project project) { task.getIgnoredModules().convention(Collections.emptySet()); task.getRejectedVersionsPerModule().convention(Collections.emptyMap()); task.getAllowMajorUpdates().convention(false); + task.getAllowMinorUpdates().convention(true); }); tasks.register("useLatestVersions", Copy.class, task -> { VersionCatalogUpdate dependent = updater.get(); diff --git a/src/main/java/io/micronaut/build/catalogs/tasks/VersionCatalogUpdate.java b/src/main/java/io/micronaut/build/catalogs/tasks/VersionCatalogUpdate.java index a98ffdcb..7c1f9cbf 100644 --- a/src/main/java/io/micronaut/build/catalogs/tasks/VersionCatalogUpdate.java +++ b/src/main/java/io/micronaut/build/catalogs/tasks/VersionCatalogUpdate.java @@ -23,6 +23,7 @@ import io.micronaut.build.catalogs.internal.VersionModel; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; +import org.gradle.api.artifacts.ComponentSelection; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.artifacts.Dependency; @@ -80,6 +81,9 @@ public abstract class VersionCatalogUpdate extends DefaultTask { @Input public abstract Property getAllowMajorUpdates(); + @Input + public abstract Property getAllowMinorUpdates(); + @Input public abstract MapProperty getRejectedVersionsPerModule(); @@ -134,6 +138,7 @@ private void updateCatalog(File inputCatalog, File outputCatalog, File logFile) } List lines = Files.readAllLines(inputCatalog.toPath(), StandardCharsets.UTF_8); boolean allowMajorUpdate = getAllowMajorUpdates().get(); + boolean allowMinorUpdate = getAllowMinorUpdates().get(); VersionCatalogTomlModel model = parser.getModel(); DependencyHandler dependencies = getProject().getDependencies(); ConfigurationContainer configurations = getProject().getConfigurations(); @@ -181,14 +186,7 @@ private void updateCatalog(File inputCatalog, File outputCatalog, File logFile) log.println("Rejecting version " + candidateVersion + " because of configuration. It matches regular expression: " + rejected); } } - if (!allowMajorUpdate) { - String major = majorVersionOf(required); - String candidateMajor = majorVersionOf(candidateVersion); - if (!major.equals(candidateMajor)) { - rules.reject("Rejecting major version " + candidateMajor); - log.println("Rejecting " + candidateModule.getModuleIdentifier() + " version " + candidateVersion + " because it's not the same major version"); - } - } + maybeRejectVersionByMinorMajor(rules, allowMajorUpdate, allowMinorUpdate, required, candidateVersion, log, candidateModule); } } } @@ -286,6 +284,31 @@ private void updateCatalog(File inputCatalog, File outputCatalog, File logFile) } } + // Visible for testing + static void maybeRejectVersionByMinorMajor(ComponentSelection rules, + boolean allowMajorUpdate, + boolean allowMinorUpdate, + String currentVersion, + String candidateVersion, + PrintWriter log, + ModuleComponentIdentifier candidateModule) { + if (!allowMajorUpdate || !allowMinorUpdate) { + int major = majorVersionOf(currentVersion); + int candidateMajor = majorVersionOf(candidateVersion); + if (major != candidateMajor && !allowMajorUpdate) { + rules.reject("Rejecting major version " + candidateMajor); + log.println("Rejecting " + candidateModule.getModuleIdentifier() + " version " + candidateVersion + " because it's not the same major version"); + } else if (major == candidateMajor && !allowMinorUpdate) { + int minor = minorVersionOf(currentVersion); + int candidateMinor = minorVersionOf(candidateVersion); + if (minor!=candidateMinor) { + rules.reject("Rejecting minor version " + candidateMinor); + log.println("Rejecting " + candidateModule.getModuleIdentifier() + " version " + candidateVersion + " because it's not the same minor version"); + } + } + } + } + private static String requiredVersionOf(Library library) { RichVersion version = library.getVersion().getVersion(); if (version != null) { @@ -307,11 +330,38 @@ private static PrintWriter newPrintWriter(File file) throws FileNotFoundExceptio return new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); } - private static String majorVersionOf(String version) { + static int majorVersionOf(String version) { + int idx = version.indexOf("."); + if (idx < 0) { + return safeParseInt(version); + } + return safeParseInt(version.substring(0, idx)); + } + + static int minorVersionOf(String version) { int idx = version.indexOf("."); if (idx < 0) { - return version; + return 0; + } + var bugfixIdx = version.indexOf(".", idx + 1); + if (bugfixIdx < 0) { + return safeParseInt(version.substring(idx + 1)); + } + return safeParseInt(version.substring(idx + 1, bugfixIdx)); + } + + private static int safeParseInt(String pollutedVersion) { + int idx = 0; + while (idx < pollutedVersion.length() && Character.isDigit(pollutedVersion.charAt(idx))) { + idx++; + } + if (idx == 0) { + return 0; + } + try { + return Integer.parseInt(pollutedVersion.substring(0, idx)); + } catch (NumberFormatException ex) { + return 0; } - return version.substring(0, idx); } } diff --git a/src/test/groovy/io/micronaut/build/catalogs/tasks/VersionCatalogUpdateTest.groovy b/src/test/groovy/io/micronaut/build/catalogs/tasks/VersionCatalogUpdateTest.groovy new file mode 100644 index 00000000..619f5e9f --- /dev/null +++ b/src/test/groovy/io/micronaut/build/catalogs/tasks/VersionCatalogUpdateTest.groovy @@ -0,0 +1,70 @@ +package io.micronaut.build.catalogs.tasks + +import org.gradle.api.artifacts.ComponentSelection +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import spock.lang.Specification + +import static io.micronaut.build.catalogs.tasks.VersionCatalogUpdate.maybeRejectVersionByMinorMajor + +class VersionCatalogUpdateTest extends Specification { + def "test major and minor version extraction"() { + expect: + VersionCatalogUpdate.majorVersionOf(version) == expectedMajor + VersionCatalogUpdate.minorVersionOf(version) == expectedMinor + + where: + version | expectedMajor | expectedMinor + "1.2.3" | 1 | 2 + "1.2" | 1 | 2 + "1" | 1 | 0 + "1.2.3.4" | 1 | 2 + "3.5-beta" | 3 | 5 + "3.5-beta1" | 3 | 5 + "2.1.0-rc1" | 2 | 1 + "128.256.12" | 128 | 256 + // not semantic versioning, edge cases to make sure + // the implementation is robust enough + "abc.def" | 0 | 0 + "oh.123noes" | 0 | 123 + "" | 0 | 0 + "wut" | 0 | 0 + } + + def "tests rejection rules"() { + def componentSelection = Mock(ComponentSelection) + def log = Stub(PrintWriter) + def id = Stub(ModuleComponentIdentifier) { + getGroup() >> "io.micronaut" + getModule() >> "micronaut-core" + getVersion() >> candidateVersion + } + + when: + maybeRejectVersionByMinorMajor( + componentSelection, + allowMajorUpdate, + allowMinorUpdate, + currentVersion, + candidateVersion, + log, + id + ) + + then: + reject * componentSelection.reject(_) + + where: + allowMajorUpdate | allowMinorUpdate | currentVersion | candidateVersion | reject + false | false | '1.0.0' | '2.0.0' | 1 + false | false | '1.0.0' | '1.1.0' | 1 + false | false | '1.0.0' | '1.0.1' | 0 + false | true | '1.0.0' | '1.1.0' | 0 + false | true | '1.0.0' | '2.0.0' | 1 + true | false | '1.0.0' | '2.0.0' | 0 + true | false | '1.0.0' | '1.1.0' | 1 + true | false | '1.0.0' | '1.0.1' | 0 + true | true | '1.0.0' | '2.0.0' | 0 + true | true | '1.0.0' | '1.1.0' | 0 + true | true | '1.0.0' | '1.0.1' | 0 + } +}