From 85f71c534f03776f0bf47cc8e15fb5c93d1d5aca Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 6 May 2026 21:50:22 -0400 Subject: [PATCH 1/4] Fix Detect Breaking Changes version selection logic for previous released version Signed-off-by: Craig Perkins --- .../opensearch/gradle/VersionSelection.java | 45 +++++++++++++++++++ .../gradle/VersionSelectionTests.java | 43 ++++++++++++++++++ server/build.gradle | 17 +++---- 3 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java create mode 100644 buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java diff --git a/buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java b/buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java new file mode 100644 index 0000000000000..b41e94464c89c --- /dev/null +++ b/buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.gradle; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class VersionSelection { + + private VersionSelection() {} + + /** + * Selects the latest released OpenSearch version before the current minor line. + * Maintenance branches must not compare against same-minor or newer-minor releases. + */ + public static String latestReleasedBeforeCurrentMinor(String currentVersion, List releasedVersions) { + Version current = Version.fromString(currentVersion); + List candidates = releasedVersions.stream() + .filter(version -> version.matches("\\d+\\.\\d+\\.\\d+")) + .map(Version::fromString) + .filter(releasedVersion -> isBeforeCurrentMinor(current, releasedVersion)) + .sorted(Comparator.naturalOrder()) + .collect(Collectors.toList()); + + if (candidates.isEmpty()) { + throw new IllegalStateException("Unable to find a released version before " + current + "'s minor line"); + } + + return candidates.get(candidates.size() - 1).toString(); + } + + private static boolean isBeforeCurrentMinor(Version current, Version releasedVersion) { + if (releasedVersion.getMajor() < current.getMajor()) { + return true; + } + return releasedVersion.getMajor() == current.getMajor() && releasedVersion.getMinor() < current.getMinor(); + } +} diff --git a/buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java b/buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java new file mode 100644 index 0000000000000..b611aae1a698a --- /dev/null +++ b/buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.gradle; + +import org.opensearch.gradle.test.GradleUnitTestCase; +import org.junit.Test; + +import java.util.List; + +public class VersionSelectionTests extends GradleUnitTestCase { + + @Test + public void selectsLatestReleasedVersionBeforeCurrentMinor() { + assertEquals("3.4.0", VersionSelection.latestReleasedBeforeCurrentMinor("3.5.1", List.of("3.4.0", "3.5.0", "3.6.0"))); + } + + @Test + public void selectsLatestPatchBeforeCurrentMinor() { + assertEquals("3.6.1", VersionSelection.latestReleasedBeforeCurrentMinor("3.7.0", List.of("3.5.0", "3.6.0", "3.6.1"))); + } + + @Test + public void selectsLatestReleaseFromPreviousMajorForFirstMinor() { + assertEquals("3.6.1", VersionSelection.latestReleasedBeforeCurrentMinor("4.0.0", List.of("3.6.0", "3.6.1", "4.0.0"))); + } + + @Test + public void ignoresQualifiedReleaseCandidates() { + assertEquals( + "2.19.5", + VersionSelection.latestReleasedBeforeCurrentMinor( + "3.0.0-beta1", + List.of("2.19.4", "2.19.5", "3.0.0-alpha1", "3.0.0-beta1", "3.0.0") + ) + ); + } +} diff --git a/server/build.gradle b/server/build.gradle index 5deedc4aa725b..27527e9383d70 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -186,18 +186,15 @@ tasks.named('forbiddenApisInternalClusterTest').configure { forbidSleep() } // Set to current version by default def japicmpCompareTarget = System.getProperty("japicmp.compare.version") -if (japicmpCompareTarget == null) { /* use latest released version */ +if (japicmpCompareTarget == null) { /* use latest released version from the previous minor line */ // Read the list from maven central. - // Fetch the metadata and parse the xml into Version instances, pick the latest one + // Fetch the metadata and parse the xml into Version instances, pick the latest previous minor. japicmpCompareTarget = new URL('https://repo1.maven.org/maven2/org/opensearch/opensearch/maven-metadata.xml').openStream().withStream { s -> - new XmlParser().parse(s) - .versioning.versions.version - .collect { it.text() }.findAll { it ==~ /\d+\.\d+\.\d+/ } - .collect { org.opensearch.gradle.Version.fromString(it) } - .toSorted() - .last() - .toString() - } + org.opensearch.gradle.VersionSelection.latestReleasedBeforeCurrentMinor( + org.opensearch.gradle.VersionProperties.getOpenSearch(), + new XmlParser().parse(s).versioning.versions.version.collect { it.text() } + ) + } } def generateModulesList = tasks.register("generateModulesList") { From 05f82a57c0660578e65aff93d59d23d768e2da81 Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Wed, 6 May 2026 22:54:14 -0400 Subject: [PATCH 2/4] Change log level to lifecycle Signed-off-by: Craig Perkins --- server/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build.gradle b/server/build.gradle index 27527e9383d70..0b1513c3d9be1 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -511,7 +511,7 @@ tasks.named("sourcesJar").configure { /** Compares the current build against a laltest released version or the version supplied through 'japicmp.compare.version' system property */ tasks.register("japicmp", me.champeau.gradle.japicmp.JapicmpTask) { - logger.info("Comparing public APIs from ${version} to ${japicmpCompareTarget}") + logger.lifecycle("Comparing public APIs from ${version} to ${japicmpCompareTarget}") // See please https://github.com/siom79/japicmp/issues/201 compatibilityChangeExcludes = [ "METHOD_ABSTRACT_NOW_DEFAULT", "METHOD_ADDED_TO_INTERFACE" ] oldClasspath.from(files("${buildDir}/japicmp-target/opensearch-${japicmpCompareTarget}.jar")) From cd1512a6960691dc4d38852e8cd8552f6b7bd76e Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 7 May 2026 11:24:37 -0400 Subject: [PATCH 3/4] Update version selection logic Signed-off-by: Craig Perkins --- .../opensearch/gradle/VersionSelection.java | 19 ++++----- .../gradle/VersionSelectionTests.java | 34 ++++++++++----- server/build.gradle | 42 +++++++++++-------- 3 files changed, 54 insertions(+), 41 deletions(-) diff --git a/buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java b/buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java index b41e94464c89c..e3fd8e4ecbd07 100644 --- a/buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java +++ b/buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java @@ -17,29 +17,24 @@ public class VersionSelection { private VersionSelection() {} /** - * Selects the latest released OpenSearch version before the current minor line. - * Maintenance branches must not compare against same-minor or newer-minor releases. + * Selects the immediate prior released version to compare against for API compatibility. + * Returns null if no prior version exists on the same major line (e.g. initial major release), + * signaling that the check should be skipped. */ - public static String latestReleasedBeforeCurrentMinor(String currentVersion, List releasedVersions) { + public static String latestPriorReleasedVersion(String currentVersion, List releasedVersions) { Version current = Version.fromString(currentVersion); List candidates = releasedVersions.stream() .filter(version -> version.matches("\\d+\\.\\d+\\.\\d+")) .map(Version::fromString) - .filter(releasedVersion -> isBeforeCurrentMinor(current, releasedVersion)) + .filter(v -> v.getMajor() == current.getMajor()) + .filter(v -> v.before(current)) .sorted(Comparator.naturalOrder()) .collect(Collectors.toList()); if (candidates.isEmpty()) { - throw new IllegalStateException("Unable to find a released version before " + current + "'s minor line"); + return null; } return candidates.get(candidates.size() - 1).toString(); } - - private static boolean isBeforeCurrentMinor(Version current, Version releasedVersion) { - if (releasedVersion.getMajor() < current.getMajor()) { - return true; - } - return releasedVersion.getMajor() == current.getMajor() && releasedVersion.getMinor() < current.getMinor(); - } } diff --git a/buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java b/buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java index b611aae1a698a..01c9902917028 100644 --- a/buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java +++ b/buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java @@ -16,28 +16,40 @@ public class VersionSelectionTests extends GradleUnitTestCase { @Test - public void selectsLatestReleasedVersionBeforeCurrentMinor() { - assertEquals("3.4.0", VersionSelection.latestReleasedBeforeCurrentMinor("3.5.1", List.of("3.4.0", "3.5.0", "3.6.0"))); + public void selectsImmediatePriorVersion() { + assertEquals("3.5.0", VersionSelection.latestPriorReleasedVersion("3.5.1", List.of("3.4.0", "3.5.0", "3.6.0"))); } @Test - public void selectsLatestPatchBeforeCurrentMinor() { - assertEquals("3.6.1", VersionSelection.latestReleasedBeforeCurrentMinor("3.7.0", List.of("3.5.0", "3.6.0", "3.6.1"))); + public void selectsLatestPatchOnSameMinor() { + assertEquals("3.6.1", VersionSelection.latestPriorReleasedVersion("3.7.0", List.of("3.5.0", "3.6.0", "3.6.1"))); } @Test - public void selectsLatestReleaseFromPreviousMajorForFirstMinor() { - assertEquals("3.6.1", VersionSelection.latestReleasedBeforeCurrentMinor("4.0.0", List.of("3.6.0", "3.6.1", "4.0.0"))); + public void returnsNullForInitialMajorRelease() { + assertNull(VersionSelection.latestPriorReleasedVersion("4.0.0", List.of("3.6.0", "3.6.1"))); } @Test - public void ignoresQualifiedReleaseCandidates() { + public void ignoresQualifiedVersions() { assertEquals( "2.19.5", - VersionSelection.latestReleasedBeforeCurrentMinor( - "3.0.0-beta1", - List.of("2.19.4", "2.19.5", "3.0.0-alpha1", "3.0.0-beta1", "3.0.0") - ) + VersionSelection.latestPriorReleasedVersion("2.20.0", List.of("2.19.4", "2.19.5", "2.20.0-alpha1", "2.20.0-beta1")) ); } + + @Test + public void returnsNullForPreReleaseOfInitialMajor() { + assertNull(VersionSelection.latestPriorReleasedVersion("4.0.0-beta1", List.of("3.6.0", "3.6.1"))); + } + + @Test + public void selectsPriorVersionOnSameMajorOnly() { + assertEquals("4.0.0", VersionSelection.latestPriorReleasedVersion("4.1.0", List.of("3.6.0", "3.6.1", "4.0.0"))); + } + + @Test + public void handlesSnapshotVersion() { + assertEquals("3.5.0", VersionSelection.latestPriorReleasedVersion("3.6.0-SNAPSHOT", List.of("3.4.0", "3.5.0"))); + } } diff --git a/server/build.gradle b/server/build.gradle index 0b1513c3d9be1..97b0657bc7373 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -186,15 +186,18 @@ tasks.named('forbiddenApisInternalClusterTest').configure { forbidSleep() } // Set to current version by default def japicmpCompareTarget = System.getProperty("japicmp.compare.version") -if (japicmpCompareTarget == null) { /* use latest released version from the previous minor line */ +if (japicmpCompareTarget == null) { /* use latest released version on the same major line */ // Read the list from maven central. - // Fetch the metadata and parse the xml into Version instances, pick the latest previous minor. + // Fetch the metadata and parse the xml into Version instances, pick the immediate prior release. japicmpCompareTarget = new URL('https://repo1.maven.org/maven2/org/opensearch/opensearch/maven-metadata.xml').openStream().withStream { s -> - org.opensearch.gradle.VersionSelection.latestReleasedBeforeCurrentMinor( + org.opensearch.gradle.VersionSelection.latestPriorReleasedVersion( org.opensearch.gradle.VersionProperties.getOpenSearch(), new XmlParser().parse(s).versioning.versions.version.collect { it.text() } ) } + if (japicmpCompareTarget == null) { + logger.lifecycle("No prior released version found on the same major line. Skipping japicmp.") + } } def generateModulesList = tasks.register("generateModulesList") { @@ -509,22 +512,25 @@ tasks.named("sourcesJar").configure { } } -/** Compares the current build against a laltest released version or the version supplied through 'japicmp.compare.version' system property */ +/** Compares the current build against a latest released version or the version supplied through 'japicmp.compare.version' system property */ tasks.register("japicmp", me.champeau.gradle.japicmp.JapicmpTask) { - logger.lifecycle("Comparing public APIs from ${version} to ${japicmpCompareTarget}") - // See please https://github.com/siom79/japicmp/issues/201 - compatibilityChangeExcludes = [ "METHOD_ABSTRACT_NOW_DEFAULT", "METHOD_ADDED_TO_INTERFACE" ] - oldClasspath.from(files("${buildDir}/japicmp-target/opensearch-${japicmpCompareTarget}.jar")) - newClasspath.from(tasks.named('jar')) - onlyModified = true - failOnModification = true - ignoreMissingClasses = true - failOnSourceIncompatibility = true - annotationIncludes = ['@org.opensearch.common.annotation.PublicApi', '@org.opensearch.common.annotation.DeprecatedApi'] - annotationExcludes = ['@org.opensearch.common.annotation.InternalApi', '@org.opensearch.common.annotation.ExperimentalApi'] - txtOutputFile = layout.buildDirectory.file("reports/java-compatibility/report.txt") - htmlOutputFile = layout.buildDirectory.file("reports/java-compatibility/report.html") - dependsOn downloadJapicmpCompareTarget + enabled = japicmpCompareTarget != null + if (japicmpCompareTarget != null) { + logger.lifecycle("Comparing public APIs from ${version} to ${japicmpCompareTarget}") + // See please https://github.com/siom79/japicmp/issues/201 + compatibilityChangeExcludes = [ "METHOD_ABSTRACT_NOW_DEFAULT", "METHOD_ADDED_TO_INTERFACE" ] + oldClasspath.from(files("${buildDir}/japicmp-target/opensearch-${japicmpCompareTarget}.jar")) + newClasspath.from(tasks.named('jar')) + onlyModified = true + failOnModification = true + ignoreMissingClasses = true + failOnSourceIncompatibility = true + annotationIncludes = ['@org.opensearch.common.annotation.PublicApi', '@org.opensearch.common.annotation.DeprecatedApi'] + annotationExcludes = ['@org.opensearch.common.annotation.InternalApi', '@org.opensearch.common.annotation.ExperimentalApi'] + txtOutputFile = layout.buildDirectory.file("reports/java-compatibility/report.txt") + htmlOutputFile = layout.buildDirectory.file("reports/java-compatibility/report.html") + dependsOn downloadJapicmpCompareTarget + } } /** If the Java API Comparison task failed, print a hint if the change should be merged from its target branch */ From 3fdf706889f06304ba1e931524f2516542432efb Mon Sep 17 00:00:00 2001 From: Craig Perkins Date: Thu, 7 May 2026 13:01:43 -0400 Subject: [PATCH 4/4] Address comments Signed-off-by: Craig Perkins --- .../opensearch/gradle/VersionSelection.java | 40 -------------- .../gradle/VersionSelectionTests.java | 55 ------------------- server/build.gradle | 17 +++--- 3 files changed, 10 insertions(+), 102 deletions(-) delete mode 100644 buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java delete mode 100644 buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java diff --git a/buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java b/buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java deleted file mode 100644 index e3fd8e4ecbd07..0000000000000 --- a/buildSrc/src/main/java/org/opensearch/gradle/VersionSelection.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.gradle; - -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; - -public class VersionSelection { - - private VersionSelection() {} - - /** - * Selects the immediate prior released version to compare against for API compatibility. - * Returns null if no prior version exists on the same major line (e.g. initial major release), - * signaling that the check should be skipped. - */ - public static String latestPriorReleasedVersion(String currentVersion, List releasedVersions) { - Version current = Version.fromString(currentVersion); - List candidates = releasedVersions.stream() - .filter(version -> version.matches("\\d+\\.\\d+\\.\\d+")) - .map(Version::fromString) - .filter(v -> v.getMajor() == current.getMajor()) - .filter(v -> v.before(current)) - .sorted(Comparator.naturalOrder()) - .collect(Collectors.toList()); - - if (candidates.isEmpty()) { - return null; - } - - return candidates.get(candidates.size() - 1).toString(); - } -} diff --git a/buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java b/buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java deleted file mode 100644 index 01c9902917028..0000000000000 --- a/buildSrc/src/test/java/org/opensearch/gradle/VersionSelectionTests.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.gradle; - -import org.opensearch.gradle.test.GradleUnitTestCase; -import org.junit.Test; - -import java.util.List; - -public class VersionSelectionTests extends GradleUnitTestCase { - - @Test - public void selectsImmediatePriorVersion() { - assertEquals("3.5.0", VersionSelection.latestPriorReleasedVersion("3.5.1", List.of("3.4.0", "3.5.0", "3.6.0"))); - } - - @Test - public void selectsLatestPatchOnSameMinor() { - assertEquals("3.6.1", VersionSelection.latestPriorReleasedVersion("3.7.0", List.of("3.5.0", "3.6.0", "3.6.1"))); - } - - @Test - public void returnsNullForInitialMajorRelease() { - assertNull(VersionSelection.latestPriorReleasedVersion("4.0.0", List.of("3.6.0", "3.6.1"))); - } - - @Test - public void ignoresQualifiedVersions() { - assertEquals( - "2.19.5", - VersionSelection.latestPriorReleasedVersion("2.20.0", List.of("2.19.4", "2.19.5", "2.20.0-alpha1", "2.20.0-beta1")) - ); - } - - @Test - public void returnsNullForPreReleaseOfInitialMajor() { - assertNull(VersionSelection.latestPriorReleasedVersion("4.0.0-beta1", List.of("3.6.0", "3.6.1"))); - } - - @Test - public void selectsPriorVersionOnSameMajorOnly() { - assertEquals("4.0.0", VersionSelection.latestPriorReleasedVersion("4.1.0", List.of("3.6.0", "3.6.1", "4.0.0"))); - } - - @Test - public void handlesSnapshotVersion() { - assertEquals("3.5.0", VersionSelection.latestPriorReleasedVersion("3.6.0-SNAPSHOT", List.of("3.4.0", "3.5.0"))); - } -} diff --git a/server/build.gradle b/server/build.gradle index 97b0657bc7373..791f733527400 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -186,14 +186,17 @@ tasks.named('forbiddenApisInternalClusterTest').configure { forbidSleep() } // Set to current version by default def japicmpCompareTarget = System.getProperty("japicmp.compare.version") -if (japicmpCompareTarget == null) { /* use latest released version on the same major line */ - // Read the list from maven central. - // Fetch the metadata and parse the xml into Version instances, pick the immediate prior release. +if (japicmpCompareTarget == null) { + // Fetch released versions from maven central, pick the latest on the same major line before current. + def currentVersion = org.opensearch.gradle.Version.fromString(org.opensearch.gradle.VersionProperties.getOpenSearch()) japicmpCompareTarget = new URL('https://repo1.maven.org/maven2/org/opensearch/opensearch/maven-metadata.xml').openStream().withStream { s -> - org.opensearch.gradle.VersionSelection.latestPriorReleasedVersion( - org.opensearch.gradle.VersionProperties.getOpenSearch(), - new XmlParser().parse(s).versioning.versions.version.collect { it.text() } - ) + new XmlParser().parse(s).versioning.versions.version + .collect { it.text() } + .findAll { it ==~ /\d+\.\d+\.\d+/ } + .collect { org.opensearch.gradle.Version.fromString(it) } + .findAll { it.getMajor() == currentVersion.getMajor() && it.before(currentVersion) } + .toSorted() + .with { it.empty ? null : it.last().toString() } } if (japicmpCompareTarget == null) { logger.lifecycle("No prior released version found on the same major line. Skipping japicmp.")