瀏覽代碼

[8.19] Add explicit versions in the branches.json (#129637) (#133524)

Versions from branches.json are now used for calculating unreleased
versions for BwC tests.
Gradle task added to modify branches.json.
Possibility to add, remove and update branches in the file with
the version field added next to the branch.
BwC test configuration has been changed to use changed branches.json
file to calculate unreleased ES versions, instead of combining data
from Version.java and branches.json.
Mariusz Józala 1 月之前
父節點
當前提交
84ea7016ca
共有 39 個文件被更改,包括 1087 次插入353 次删除
  1. 0 20
      branches.json
  2. 3 1
      build-conventions/src/main/java/org/elasticsearch/gradle/internal/conventions/VersionPropertiesPlugin.java
  3. 11 0
      build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/fixtures/AbstractGitAwareGradleFuncTest.groovy
  4. 11 11
      build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPluginFuncTest.groovy
  5. 8 8
      build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/InternalDistributionDownloadPluginFuncTest.groovy
  6. 8 8
      build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestCompatTestPluginFuncTest.groovy
  7. 8 0
      build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/transport/AbstractTransportVersionFuncTest.groovy
  8. 0 0
      build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/major1/build.gradle
  9. 0 0
      build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/major2/build.gradle
  10. 0 0
      build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/major3/build.gradle
  11. 0 0
      build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/major4/build.gradle
  12. 0 0
      build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/minor1/build.gradle
  13. 0 0
      build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/minor2/build.gradle
  14. 0 0
      build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/minor3/build.gradle
  15. 0 0
      build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/minor4/build.gradle
  16. 8 6
      build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/settings.gradle
  17. 38 106
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/BwcVersions.java
  18. 46 0
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/BranchesFileParser.java
  19. 28 0
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/DevelopmentBranch.java
  20. 54 24
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java
  21. 1 0
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ReleaseToolsPlugin.java
  22. 176 0
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/UpdateBranchesJsonTask.java
  23. 17 2
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/AbstractYamlRestCompatTestPlugin.java
  24. 230 132
      build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/BwcVersionsSpec.groovy
  25. 111 0
      build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPluginSpec.groovy
  26. 267 0
      build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/release/UpdateBranchesJsonTaskSpec.groovy
  27. 7 1
      build-tools-internal/src/test/java/org/elasticsearch/gradle/AbstractDistributionDownloadPluginTests.java
  28. 5 0
      build-tools/src/main/java/org/elasticsearch/gradle/VersionProperties.java
  29. 34 24
      build-tools/src/testFixtures/groovy/org/elasticsearch/gradle/fixtures/AbstractGradleFuncTest.groovy
  30. 8 5
      build.gradle
  31. 0 0
      distribution/bwc/major1/build.gradle
  32. 0 0
      distribution/bwc/major2/build.gradle
  33. 0 0
      distribution/bwc/major3/build.gradle
  34. 0 0
      distribution/bwc/major4/build.gradle
  35. 0 0
      distribution/bwc/minor1/build.gradle
  36. 0 0
      distribution/bwc/minor2/build.gradle
  37. 0 0
      distribution/bwc/minor3/build.gradle
  38. 0 0
      distribution/bwc/minor4/build.gradle
  39. 8 5
      settings.gradle

+ 0 - 20
branches.json

@@ -1,20 +0,0 @@
-{
-  "notice": "This file is not maintained outside of the main branch and should only be used for tooling.",
-  "branches": [
-    {
-      "branch": "main"
-    },
-    {
-      "branch": "9.0"
-    },
-    {
-      "branch": "8.19"
-    },
-    {
-      "branch": "8.18"
-    },
-    {
-      "branch": "7.17"
-    }
-  ]
-}

+ 3 - 1
build-conventions/src/main/java/org/elasticsearch/gradle/internal/conventions/VersionPropertiesPlugin.java

@@ -18,6 +18,8 @@ import java.io.File;
 
 public class VersionPropertiesPlugin implements Plugin<Project> {
 
+    public static final String VERSIONS_EXT = "versions";
+
     @Override
     public void apply(Project project) {
         File workspaceDir = Util.locateElasticsearchWorkspace(project.getGradle());
@@ -28,6 +30,6 @@ public class VersionPropertiesPlugin implements Plugin<Project> {
                 .registerIfAbsent("versions", VersionPropertiesBuildService.class, spec -> {
             spec.getParameters().getInfoPath().set(infoPath);
         });
-        project.getExtensions().add("versions", serviceProvider.get().getProperties());
+        project.getExtensions().add(VERSIONS_EXT, serviceProvider.get().getProperties());
     }
 }

+ 11 - 0
build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/fixtures/AbstractGitAwareGradleFuncTest.groovy

@@ -26,6 +26,17 @@ abstract class AbstractGitAwareGradleFuncTest extends AbstractGradleFuncTest {
         execute("git clone ${remoteGitRepo.absolutePath} cloned", testProjectDir.root)
         buildFile = new File(testProjectDir.root, 'cloned/build.gradle')
         settingsFile = new File(testProjectDir.root, 'cloned/settings.gradle')
+        versionPropertiesFile = new File(testProjectDir.root, 'cloned/build-tools-internal/version.properties')
+        versionPropertiesFile.text = """
+            elasticsearch     = 9.1.0
+            lucene            = 10.2.2
+
+            bundled_jdk_vendor = openjdk
+            bundled_jdk = 24+36@1f9ff9062db4449d8ca828c504ffae90
+            minimumJdkVersion = 21
+            minimumRuntimeJava = 21
+            minimumCompilerJava = 21
+        """
     }
 
     File setupGitRemote() {

+ 11 - 11
build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPluginFuncTest.groovy

@@ -51,23 +51,23 @@ class InternalDistributionBwcSetupPluginFuncTest extends AbstractGitAwareGradleF
         assertOutputContains(result.output, "[$bwcDistVersion] > Task :distribution:archives:darwin-tar:${expectedAssembleTaskName}")
 
         where:
-        bwcDistVersion | bwcProject    | expectedAssembleTaskName
-        "8.4.0"        | "minor"       | "extractedAssemble"
-        "8.3.0"        | "staged"      | "extractedAssemble"
-        "8.2.1"        | "bugfix"      | "extractedAssemble"
-        "8.1.3"        | "bugfix2"     | "extractedAssemble"
+        bwcDistVersion | bwcProject | expectedAssembleTaskName
+        "8.4.0"        | "major1"   | "extractedAssemble"
+        "8.3.0"        | "major2"   | "extractedAssemble"
+        "8.2.1"        | "major3"   | "extractedAssemble"
+        "8.1.3"        | "major4"   | "extractedAssemble"
     }
 
     @Unroll
     def "supports #platform aarch distributions"() {
         when:
-        def result = gradleRunner(":distribution:bwc:minor:buildBwc${platform.capitalize()}Aarch64Tar",
+        def result = gradleRunner(":distribution:bwc:major1:buildBwc${platform.capitalize()}Aarch64Tar",
                 "-DtestRemoteRepo=" + remoteGitRepo,
                 "-Dbwc.remote=origin",
                 "-Dbwc.dist.version=${bwcDistVersion}-SNAPSHOT")
                 .build()
         then:
-        result.task(":distribution:bwc:minor:buildBwc${platform.capitalize()}Aarch64Tar").outcome == TaskOutcome.SUCCESS
+        result.task(":distribution:bwc:major1:buildBwc${platform.capitalize()}Aarch64Tar").outcome == TaskOutcome.SUCCESS
 
         and: "assemble tasks triggered"
         assertOutputContains(result.output, "[$bwcDistVersion] > Task :distribution:archives:${platform}-aarch64-tar:extractedAssemble")
@@ -87,7 +87,7 @@ class InternalDistributionBwcSetupPluginFuncTest extends AbstractGitAwareGradleF
         }
 
         dependencies {
-            expandedDist project(path: ":distribution:bwc:minor", configuration:"expanded-darwin-tar")
+            expandedDist project(path: ":distribution:bwc:major1", configuration:"expanded-darwin-tar")
         }
 
         tasks.register("resolveExpandedDistribution") {
@@ -109,12 +109,12 @@ class InternalDistributionBwcSetupPluginFuncTest extends AbstractGitAwareGradleF
                 .build()
         then:
         result.task(":resolveExpandedDistribution").outcome == TaskOutcome.SUCCESS
-        result.task(":distribution:bwc:minor:buildBwcDarwinTar").outcome == TaskOutcome.SUCCESS
+        result.task(":distribution:bwc:major1:buildBwcDarwinTar").outcome == TaskOutcome.SUCCESS
         and: "assemble task triggered"
         result.output.contains("[8.4.0] > Task :distribution:archives:darwin-tar:extractedAssemble")
-        result.output.contains("expandedRootPath /distribution/bwc/minor/build/bwc/checkout-8.x/" +
+        result.output.contains("expandedRootPath /distribution/bwc/major1/build/bwc/checkout-8.x/" +
                         "distribution/archives/darwin-tar/build/install")
-        result.output.contains("nested folder /distribution/bwc/minor/build/bwc/checkout-8.x/" +
+        result.output.contains("nested folder /distribution/bwc/major1/build/bwc/checkout-8.x/" +
                         "distribution/archives/darwin-tar/build/install/elasticsearch-8.4.0-SNAPSHOT")
     }
 }

+ 8 - 8
build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/InternalDistributionDownloadPluginFuncTest.groovy

@@ -51,7 +51,7 @@ class InternalDistributionDownloadPluginFuncTest extends AbstractGradleFuncTest
     def "resolves expanded bwc versions from source"() {
         given:
         internalBuild()
-        bwcMinorProjectSetup()
+        bwcMajor1ProjectSetup()
         buildFile << """
             apply plugin: 'elasticsearch.internal-distribution-download'
 
@@ -72,16 +72,16 @@ class InternalDistributionDownloadPluginFuncTest extends AbstractGradleFuncTest
 
         def result = gradleRunner("setupDistro").build()
         then:
-        result.task(":distribution:bwc:minor:buildBwcExpandedTask").outcome == TaskOutcome.SUCCESS
+        result.task(":distribution:bwc:major1:buildBwcExpandedTask").outcome == TaskOutcome.SUCCESS
         result.task(":setupDistro").outcome == TaskOutcome.SUCCESS
-        assertExtractedDistroIsCreated("distribution/bwc/minor/build/install/elastic-distro",
+        assertExtractedDistroIsCreated("distribution/bwc/major1/build/install/elastic-distro",
                 'bwc-marker.txt')
     }
 
     def "fails on resolving bwc versions with no bundled jdk"() {
         given:
         internalBuild()
-        bwcMinorProjectSetup()
+        bwcMajor1ProjectSetup()
         buildFile << """
             apply plugin: 'elasticsearch.internal-distribution-download'
 
@@ -105,12 +105,12 @@ class InternalDistributionDownloadPluginFuncTest extends AbstractGradleFuncTest
                 "without a bundled JDK is not supported.")
     }
 
-    private void bwcMinorProjectSetup() {
+    private void bwcMajor1ProjectSetup() {
         settingsFile << """
-        include ':distribution:bwc:minor'
+        include ':distribution:bwc:major1'
         """
-        def bwcSubProjectFolder = testProjectDir.newFolder("distribution", "bwc", "minor")
-        new File(bwcSubProjectFolder, 'bwc-marker.txt') << "bwc=minor"
+        def bwcSubProjectFolder = testProjectDir.newFolder("distribution", "bwc", "major1")
+        new File(bwcSubProjectFolder, 'bwc-marker.txt') << "bwc=major1"
         new File(bwcSubProjectFolder, 'build.gradle') << """
             apply plugin:'base'
 

+ 8 - 8
build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/test/rest/LegacyYamlRestCompatTestPluginFuncTest.groovy

@@ -41,7 +41,7 @@ class LegacyYamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTe
         given:
         internalBuild()
 
-        subProject(":distribution:bwc:maintenance") << """
+        subProject(":distribution:bwc:major1") << """
         configurations { checkout }
         artifacts {
             checkout(new File(projectDir, "checkoutDir"))
@@ -62,11 +62,11 @@ class LegacyYamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTe
         result.task(transformTask).outcome == TaskOutcome.NO_SOURCE
     }
 
-    def "yamlRestCompatTest executes and copies api and transforms tests from :bwc:maintenance"() {
+    def "yamlRestCompatTest executes and copies api and transforms tests from :bwc:major1"() {
         given:
         internalBuild()
 
-        subProject(":distribution:bwc:maintenance") << """
+        subProject(":distribution:bwc:major1") << """
         configurations { checkout }
         artifacts {
             checkout(new File(projectDir, "checkoutDir"))
@@ -99,8 +99,8 @@ class LegacyYamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTe
         String api = "foo.json"
         String test = "10_basic.yml"
         //add the compatible test and api files, these are the prior version's normal yaml rest tests
-        file("distribution/bwc/maintenance/checkoutDir/rest-api-spec/src/main/resources/rest-api-spec/api/" + api) << ""
-        file("distribution/bwc/maintenance/checkoutDir/src/yamlRestTest/resources/rest-api-spec/test/" + test) << ""
+        file("distribution/bwc/major1/checkoutDir/rest-api-spec/src/main/resources/rest-api-spec/api/" + api) << ""
+        file("distribution/bwc/major1/checkoutDir/src/yamlRestTest/resources/rest-api-spec/test/" + test) << ""
 
         when:
         def result = gradleRunner("yamlRestTestV${compatibleVersion}CompatTest").build()
@@ -146,7 +146,7 @@ class LegacyYamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTe
         given:
         internalBuild()
         withVersionCatalogue()
-        subProject(":distribution:bwc:maintenance") << """
+        subProject(":distribution:bwc:major1") << """
         configurations { checkout }
         artifacts {
             checkout(new File(projectDir, "checkoutDir"))
@@ -187,7 +187,7 @@ class LegacyYamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTe
         given:
         internalBuild()
 
-        subProject(":distribution:bwc:maintenance") << """
+        subProject(":distribution:bwc:major1") << """
         configurations { checkout }
         artifacts {
             checkout(new File(projectDir, "checkoutDir"))
@@ -231,7 +231,7 @@ class LegacyYamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTe
 
         setupRestResources([], [])
 
-        file("distribution/bwc/maintenance/checkoutDir/src/yamlRestTest/resources/rest-api-spec/test/test.yml" ) << """
+        file("distribution/bwc/major1/checkoutDir/src/yamlRestTest/resources/rest-api-spec/test/test.yml" ) << """
         "one":
           - do:
               do_.some.key_to_replace:

+ 8 - 0
build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/transport/AbstractTransportVersionFuncTest.groovy

@@ -116,4 +116,12 @@ class AbstractTransportVersionFuncTest extends AbstractGradleFuncTest {
         execute("git checkout -b main")
         execute("git checkout -b test")
     }
+
+    void setupLocalGitRepo() {
+        execute("git init")
+        execute('git config user.email "build-tool@elastic.co"')
+        execute('git config user.name "Build tool"')
+        execute("git add .")
+        execute('git commit -m "Initial"')
+    }
 }

+ 0 - 0
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/bugfix/build.gradle → build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/major1/build.gradle


+ 0 - 0
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/bugfix2/build.gradle → build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/major2/build.gradle


+ 0 - 0
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/bugfix3/build.gradle → build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/major3/build.gradle


+ 0 - 0
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/bugfix4/build.gradle → build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/major4/build.gradle


+ 0 - 0
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/bugfix5/build.gradle → build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/minor1/build.gradle


+ 0 - 0
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/maintenance/build.gradle → build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/minor2/build.gradle


+ 0 - 0
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/major/build.gradle → build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/minor3/build.gradle


+ 0 - 0
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/minor/build.gradle → build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/minor4/build.gradle


+ 8 - 6
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/settings.gradle

@@ -9,12 +9,14 @@
 
 rootProject.name = "root"
 
-include ":distribution:bwc:bugfix"
-include ":distribution:bwc:bugfix2"
-include ":distribution:bwc:minor"
-include ":distribution:bwc:major"
-include ":distribution:bwc:staged"
-include ":distribution:bwc:maintenance"
+include ":distribution:bwc:major1"
+include ":distribution:bwc:major2"
+include ":distribution:bwc:major3"
+include ":distribution:bwc:major4"
+include ":distribution:bwc:minor1"
+include ":distribution:bwc:minor2"
+include ":distribution:bwc:minor3"
+include ":distribution:bwc:minor4"
 include ":distribution:archives:darwin-tar"
 include ":distribution:archives:oss-darwin-tar"
 include ":distribution:archives:darwin-aarch64-tar"

+ 38 - 106
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/BwcVersions.java

@@ -11,7 +11,7 @@ package org.elasticsearch.gradle.internal;
 import org.elasticsearch.gradle.Architecture;
 import org.elasticsearch.gradle.ElasticsearchDistribution;
 import org.elasticsearch.gradle.Version;
-import org.elasticsearch.gradle.VersionProperties;
+import org.elasticsearch.gradle.internal.info.DevelopmentBranch;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -26,8 +26,6 @@ import java.util.TreeMap;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import static java.util.Collections.reverseOrder;
@@ -68,20 +66,13 @@ import static java.util.Comparator.comparing;
 
 public class BwcVersions implements Serializable {
 
-    private static final Pattern LINE_PATTERN = Pattern.compile(
-        "\\W+public static final Version V_(\\d+)_(\\d+)_(\\d+)(_alpha\\d+|_beta\\d+|_rc\\d+)?.*\\);"
-    );
     private static final String GLIBC_VERSION_ENV_VAR = "GLIBC_VERSION";
 
     private final Version currentVersion;
     private final transient List<Version> versions;
     private final Map<Version, UnreleasedVersionInfo> unreleased;
 
-    public BwcVersions(List<String> versionLines, List<String> developmentBranches) {
-        this(versionLines, Version.fromString(VersionProperties.getElasticsearch()), developmentBranches);
-    }
-
-    public BwcVersions(Version currentVersionProperty, List<Version> allVersions, List<String> developmentBranches) {
+    public BwcVersions(Version currentVersionProperty, List<Version> allVersions, List<DevelopmentBranch> developmentBranches) {
         if (allVersions.isEmpty()) {
             throw new IllegalArgumentException("Could not parse any versions");
         }
@@ -93,20 +84,6 @@ public class BwcVersions implements Serializable {
         this.unreleased = computeUnreleased(developmentBranches);
     }
 
-    // Visible for testing
-    BwcVersions(List<String> versionLines, Version currentVersionProperty, List<String> developmentBranches) {
-        this(currentVersionProperty, parseVersionLines(versionLines), developmentBranches);
-    }
-
-    private static List<Version> parseVersionLines(List<String> versionLines) {
-        return versionLines.stream()
-            .map(LINE_PATTERN::matcher)
-            .filter(Matcher::matches)
-            .map(match -> new Version(Integer.parseInt(match.group(1)), Integer.parseInt(match.group(2)), Integer.parseInt(match.group(3))))
-            .sorted()
-            .toList();
-    }
-
     private void assertCurrentVersionMatchesParsed(Version currentVersionProperty) {
         if (currentVersionProperty.equals(currentVersion) == false) {
             throw new IllegalStateException(
@@ -132,78 +109,46 @@ public class BwcVersions implements Serializable {
         ).stream().map(unreleased::get).forEach(consumer);
     }
 
-    private String getBranchFor(Version version, List<String> developmentBranches) {
-        // If the current version matches a specific feature freeze branch, use that
-        if (developmentBranches.contains(version.getMajor() + "." + version.getMinor())) {
-            return version.getMajor() + "." + version.getMinor();
-        } else if (developmentBranches.contains(version.getMajor() + ".x")) { // Otherwise if an n.x branch exists and we are that major
-            return version.getMajor() + ".x";
-        } else { // otherwise we're the main branch
-            return "main";
-        }
-    }
-
-    private Map<Version, UnreleasedVersionInfo> computeUnreleased(List<String> developmentBranches) {
+    private Map<Version, UnreleasedVersionInfo> computeUnreleased(List<DevelopmentBranch> developmentBranches) {
         Map<Version, UnreleasedVersionInfo> result = new TreeMap<>();
+        Map<String, List<DevelopmentBranch>> bwcBranches = developmentBranches.stream()
+            .filter(developmentBranch -> developmentBranch.version().before(currentVersion))
+            .sorted(reverseOrder(comparing(DevelopmentBranch::version)))
+            .collect(Collectors.groupingBy(branch -> {
+                if (branch.version().getMajor() == currentVersion.getMajor()) {
+                    return "minor";
+                } else if (branch.version().getMajor() == currentVersion.getMajor() - 1) {
+                    return "major";
+                }
+                return "older";
+            }));
+
+        developmentBranches.stream()
+            .filter(branch -> branch.version().equals(currentVersion))
+            .findFirst()
+            .ifPresent(
+                developmentBranch -> result.put(
+                    currentVersion,
+                    new UnreleasedVersionInfo(currentVersion, developmentBranch.name(), ":distribution")
+                )
+            );
 
-        // The current version is always in development
-        String currentBranch = getBranchFor(currentVersion, developmentBranches);
-        result.put(currentVersion, new UnreleasedVersionInfo(currentVersion, currentBranch, ":distribution"));
-
-        // Check for an n.x branch as well
-        if (currentBranch.equals("main") && developmentBranches.stream().anyMatch(s -> s.endsWith(".x"))) {
-            // This should correspond to the latest new minor
-            Version version = versions.stream()
-                .sorted(Comparator.reverseOrder())
-                .filter(v -> v.getMajor() == (currentVersion.getMajor() - 1) && v.getRevision() == 0)
-                .findFirst()
-                .orElseThrow(() -> new IllegalStateException("Unable to determine development version for branch"));
-            String branch = getBranchFor(version, developmentBranches);
-            assert branch.equals(currentVersion.getMajor() - 1 + ".x") : "Expected branch does not match development branch";
-
-            result.put(version, new UnreleasedVersionInfo(version, branch, ":distribution:bwc:minor"));
+        List<DevelopmentBranch> previousMinorBranches = bwcBranches.getOrDefault("minor", Collections.emptyList());
+        for (int i = 0; i < previousMinorBranches.size(); i++) {
+            DevelopmentBranch previousMinorBranch = previousMinorBranches.get(i);
+            result.put(
+                previousMinorBranch.version(),
+                new UnreleasedVersionInfo(previousMinorBranch.version(), previousMinorBranch.name(), ":distribution:bwc:minor" + (i + 1))
+            );
         }
 
-        // Now handle all the feature freeze branches
-        List<String> featureFreezeBranches = developmentBranches.stream()
-            .filter(b -> Pattern.matches("[0-9]+\\.[0-9]+", b))
-            .sorted(reverseOrder(comparing(s -> Version.fromString(s, Version.Mode.RELAXED))))
-            .toList();
-
-        boolean existingBugfix = false;
-        for (int i = 0; i < featureFreezeBranches.size(); i++) {
-            String branch = featureFreezeBranches.get(i);
-            Version version = versions.stream()
-                .sorted(Comparator.reverseOrder())
-                .filter(v -> v.toString().startsWith(branch))
-                .findFirst()
-                .orElse(null);
-
-            // If we don't know about this version we can ignore it
-            if (version == null) {
-                continue;
-            }
-
-            // If this is the current version we can ignore as we've already handled it
-            if (version.equals(currentVersion)) {
-                continue;
-            }
-
-            // We only maintain compatibility back one major so ignore anything older
-            if (currentVersion.getMajor() - version.getMajor() > 1) {
-                continue;
-            }
-
-            // This is the maintenance version
-            if (i == featureFreezeBranches.size() - 1) {
-                result.put(version, new UnreleasedVersionInfo(version, branch, ":distribution:bwc:maintenance"));
-            } else if (version.getRevision() == 0) { // This is the next staged minor
-                result.put(version, new UnreleasedVersionInfo(version, branch, ":distribution:bwc:staged"));
-            } else { // This is a bugfix
-                String project = existingBugfix ? "bugfix2" : "bugfix";
-                result.put(version, new UnreleasedVersionInfo(version, branch, ":distribution:bwc:" + project));
-                existingBugfix = true;
-            }
+        List<DevelopmentBranch> previousMajorBranches = bwcBranches.getOrDefault("major", Collections.emptyList());
+        for (int i = 0; i < previousMajorBranches.size(); i++) {
+            DevelopmentBranch previousMajorBranch = previousMajorBranches.get(i);
+            result.put(
+                previousMajorBranch.version(),
+                new UnreleasedVersionInfo(previousMajorBranch.version(), previousMajorBranch.name(), ":distribution:bwc:major" + (i + 1))
+            );
         }
 
         return Collections.unmodifiableMap(result);
@@ -213,19 +158,6 @@ public class BwcVersions implements Serializable {
         return unreleased.keySet().stream().sorted().toList();
     }
 
-    private void addUnreleased(Set<Version> unreleased, Version current, int index) {
-        if (current.getRevision() == 0) {
-            // If the current version is a new minor, the next version is also unreleased
-            Version next = versions.get(versions.size() - (index + 2));
-            unreleased.add(next);
-
-            // Keep looking through versions until we find the end of unreleased versions
-            addUnreleased(unreleased, next, index + 1);
-        } else {
-            unreleased.add(current);
-        }
-    }
-
     public void compareToAuthoritative(List<Version> authoritativeReleasedVersions) {
         Set<Version> notReallyReleased = new HashSet<>(getReleased());
         notReallyReleased.removeAll(authoritativeReleasedVersions);

+ 46 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/BranchesFileParser.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.info;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.elasticsearch.gradle.Version;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A parser for the branches.json file
+ */
+public class BranchesFileParser {
+
+    private final ObjectMapper objectMapper;
+
+    public BranchesFileParser(ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    public List<DevelopmentBranch> parse(byte[] bytes) {
+        List<DevelopmentBranch> branches = new ArrayList<>();
+        try {
+            JsonNode json = objectMapper.readTree(bytes);
+            for (JsonNode node : json.get("branches")) {
+                branches.add(new DevelopmentBranch(node.get("branch").asText(), Version.fromString(node.get("version").asText())));
+            }
+        } catch (IOException e) {
+            throw new UncheckedIOException("Failed to parse content of branches.json", e);
+        }
+
+        return branches;
+    }
+}

+ 28 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/DevelopmentBranch.java

@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.info;
+
+import org.elasticsearch.gradle.Version;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Information about a development branch used in branches.json file
+ *
+ * @param name Name of the development branch
+ * @param version Elasticsearch version on the development branch
+ */
+public record DevelopmentBranch(String name, Version version) implements Serializable {
+    public DevelopmentBranch {
+        Objects.requireNonNull(name);
+        Objects.requireNonNull(version);
+    }
+}

+ 54 - 24
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java

@@ -8,17 +8,17 @@
  */
 package org.elasticsearch.gradle.internal.info;
 
-import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import org.apache.commons.io.IOUtils;
 import org.elasticsearch.gradle.Architecture;
 import org.elasticsearch.gradle.OS;
-import org.elasticsearch.gradle.VersionProperties;
+import org.elasticsearch.gradle.Version;
 import org.elasticsearch.gradle.internal.BwcVersions;
 import org.elasticsearch.gradle.internal.Jdk;
 import org.elasticsearch.gradle.internal.JdkDownloadPlugin;
 import org.elasticsearch.gradle.internal.conventions.GitInfoPlugin;
+import org.elasticsearch.gradle.internal.conventions.VersionPropertiesPlugin;
 import org.elasticsearch.gradle.internal.conventions.info.GitInfo;
 import org.elasticsearch.gradle.internal.conventions.info.ParallelDetector;
 import org.elasticsearch.gradle.internal.conventions.util.Util;
@@ -56,29 +56,38 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.UncheckedIOException;
+import java.net.URI;
 import java.nio.file.Files;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.Properties;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import javax.inject.Inject;
 
 import static org.elasticsearch.gradle.internal.conventions.GUtils.elvis;
+import static org.elasticsearch.gradle.internal.conventions.VersionPropertiesPlugin.VERSIONS_EXT;
 import static org.elasticsearch.gradle.internal.toolchain.EarlyAccessCatalogJdkToolchainResolver.findLatestEABuildNumber;
 
 public class GlobalBuildInfoPlugin implements Plugin<Project> {
     private static final Logger LOGGER = Logging.getLogger(GlobalBuildInfoPlugin.class);
     private static final String DEFAULT_VERSION_JAVA_FILE_PATH = "server/src/main/java/org/elasticsearch/Version.java";
+    private static final String DEFAULT_BRANCHES_FILE_URL = "https://raw.githubusercontent.com/elastic/elasticsearch/main/branches.json";
+    private static final String BRANCHES_FILE_LOCATION_PROPERTY = "org.elasticsearch.build.branches-file-location";
+    private static final Pattern LINE_PATTERN = Pattern.compile(
+        "\\W+public static final Version V_(\\d+)_(\\d+)_(\\d+)(_alpha\\d+|_beta\\d+|_rc\\d+)?.*\\);"
+    );
 
     private ObjectFactory objectFactory;
     private final JavaInstallationRegistry javaInstallationRegistry;
     private final JvmMetadataDetector metadataDetector;
     private final ProviderFactory providers;
-    private final ObjectMapper objectMapper;
+    private final BranchesFileParser branchesFileParser;
     private JavaToolchainService toolChainService;
     private Project project;
 
@@ -93,7 +102,7 @@ public class GlobalBuildInfoPlugin implements Plugin<Project> {
         this.javaInstallationRegistry = javaInstallationRegistry;
         this.metadataDetector = new ErrorTraceMetadataDetector(metadataDetector);
         this.providers = providers;
-        this.objectMapper = new ObjectMapper();
+        this.branchesFileParser = new BranchesFileParser(new ObjectMapper());
     }
 
     @Override
@@ -113,21 +122,27 @@ public class GlobalBuildInfoPlugin implements Plugin<Project> {
             throw new GradleException("Gradle " + minimumGradleVersion.getVersion() + "+ is required");
         }
 
-        JavaVersion minimumCompilerVersion = JavaVersion.toVersion(getResourceContents("/minimumCompilerVersion"));
-        JavaVersion minimumRuntimeVersion = JavaVersion.toVersion(getResourceContents("/minimumRuntimeVersion"));
+        project.getPlugins().apply(VersionPropertiesPlugin.class);
+        Properties versionProperties = (Properties) project.getExtensions().getByName(VERSIONS_EXT);
+        JavaVersion minimumCompilerVersion = JavaVersion.toVersion(versionProperties.get("minimumCompilerJava"));
+        JavaVersion minimumRuntimeVersion = JavaVersion.toVersion(versionProperties.get("minimumRuntimeJava"));
+
+        String bundledJdkVersion = versionProperties.getProperty("bundled_jdk");
+        String bundledJdkMajorVersion = bundledJdkVersion.split("[.+]")[0];
+        Version elasticsearchVersionProperty = Version.fromString(versionProperties.getProperty("elasticsearch"));
 
         Provider<File> explicitRuntimeJavaHome = findRuntimeJavaHome();
         boolean isRuntimeJavaHomeExplicitlySet = explicitRuntimeJavaHome.isPresent();
         Provider<File> actualRuntimeJavaHome = isRuntimeJavaHomeExplicitlySet
             ? explicitRuntimeJavaHome
-            : resolveJavaHomeFromToolChainService(VersionProperties.getBundledJdkMajorVersion());
+            : resolveJavaHomeFromToolChainService(bundledJdkMajorVersion);
 
         Provider<JvmInstallationMetadata> runtimeJdkMetaData = actualRuntimeJavaHome.map(
             runtimeJavaHome -> metadataDetector.getMetadata(getJavaInstallation(runtimeJavaHome))
         );
         AtomicReference<BwcVersions> cache = new AtomicReference<>();
         Provider<BwcVersions> bwcVersionsProvider = providers.provider(
-            () -> cache.updateAndGet(val -> val == null ? resolveBwcVersions() : val)
+            () -> cache.updateAndGet(val -> val == null ? resolveBwcVersions(elasticsearchVersionProperty) : val)
         );
         BuildParameterExtension buildParams = project.getExtensions()
             .create(
@@ -141,9 +156,7 @@ public class GlobalBuildInfoPlugin implements Plugin<Project> {
                     javaHome -> determineJavaVersion(
                         "runtime java.home",
                         javaHome,
-                        isRuntimeJavaHomeExplicitlySet
-                            ? minimumRuntimeVersion
-                            : JavaVersion.toVersion(VersionProperties.getBundledJdkMajorVersion())
+                        isRuntimeJavaHomeExplicitlySet ? minimumRuntimeVersion : JavaVersion.toVersion(bundledJdkMajorVersion)
                     )
                 ),
                 isRuntimeJavaHomeExplicitlySet,
@@ -204,32 +217,49 @@ public class GlobalBuildInfoPlugin implements Plugin<Project> {
     /* Introspect all versions of ES that may be tested against for backwards
      * compatibility. It is *super* important that this logic is the same as the
      * logic in VersionUtils.java. */
-    private BwcVersions resolveBwcVersions() {
+    private BwcVersions resolveBwcVersions(Version currentElasticsearchVersion) {
         String versionsFilePath = elvis(
             System.getProperty("BWC_VERSION_SOURCE"),
             new File(Util.locateElasticsearchWorkspace(project.getGradle()), DEFAULT_VERSION_JAVA_FILE_PATH).getPath()
         );
         try (var is = new FileInputStream(versionsFilePath)) {
             List<String> versionLines = IOUtils.readLines(is, "UTF-8");
-            return new BwcVersions(versionLines, getDevelopmentBranches());
+            return new BwcVersions(currentElasticsearchVersion, parseVersionLines(versionLines), getDevelopmentBranches());
         } catch (IOException e) {
             throw new IllegalStateException("Unable to resolve to resolve bwc versions from versionsFile.", e);
         }
     }
 
-    private List<String> getDevelopmentBranches() {
-        List<String> branches = new ArrayList<>();
-        File branchesFile = new File(Util.locateElasticsearchWorkspace(project.getGradle()), "branches.json");
-        try (InputStream is = new FileInputStream(branchesFile)) {
-            JsonNode json = objectMapper.readTree(is);
-            for (JsonNode node : json.get("branches")) {
-                branches.add(node.get("branch").asText());
+    private List<Version> parseVersionLines(List<String> versionLines) {
+        return versionLines.stream()
+            .map(LINE_PATTERN::matcher)
+            .filter(Matcher::matches)
+            .map(match -> new Version(Integer.parseInt(match.group(1)), Integer.parseInt(match.group(2)), Integer.parseInt(match.group(3))))
+            .sorted()
+            .toList();
+    }
+
+    private List<DevelopmentBranch> getDevelopmentBranches() {
+        String branchesFileLocation = project.getProviders()
+            .gradleProperty(BRANCHES_FILE_LOCATION_PROPERTY)
+            .getOrElse(DEFAULT_BRANCHES_FILE_URL);
+        LOGGER.info("Reading branches.json from {}", branchesFileLocation);
+        byte[] branchesBytes;
+        if (branchesFileLocation.startsWith("http")) {
+            try (InputStream in = URI.create(branchesFileLocation).toURL().openStream()) {
+                branchesBytes = in.readAllBytes();
+            } catch (IOException e) {
+                throw new UncheckedIOException("Failed to download branches.json from: " + branchesFileLocation, e);
+            }
+        } else {
+            try {
+                branchesBytes = Files.readAllBytes(new File(branchesFileLocation).toPath());
+            } catch (IOException e) {
+                throw new UncheckedIOException("Failed to read branches.json from: " + branchesFileLocation, e);
             }
-        } catch (IOException e) {
-            throw new UncheckedIOException(e);
         }
 
-        return branches;
+        return branchesFileParser.parse(branchesBytes);
     }
 
     private void logGlobalBuildInfo(BuildParameterExtension buildParams) {

+ 1 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ReleaseToolsPlugin.java

@@ -54,6 +54,7 @@ public class ReleaseToolsPlugin implements Plugin<Project> {
         project.getTasks().register("extractCurrentVersions", ExtractCurrentVersionsTask.class);
         project.getTasks().register("tagVersions", TagVersionsTask.class);
         project.getTasks().register("setCompatibleVersions", SetCompatibleVersionsTask.class, t -> t.setThisVersion(version));
+        project.getTasks().register("updateBranchesJson", UpdateBranchesJsonTask.class);
 
         final FileTree yamlFiles = projectDirectory.dir("docs/changelog")
             .getAsFileTree()

+ 176 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/UpdateBranchesJsonTask.java

@@ -0,0 +1,176 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.release;
+
+import com.fasterxml.jackson.core.util.DefaultIndenter;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import org.elasticsearch.gradle.Version;
+import org.elasticsearch.gradle.internal.info.BranchesFileParser;
+import org.elasticsearch.gradle.internal.info.DevelopmentBranch;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.InvalidUserDataException;
+import org.gradle.api.file.ProjectLayout;
+import org.gradle.api.logging.Logger;
+import org.gradle.api.logging.Logging;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.Optional;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.api.tasks.options.Option;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.Comparator;
+import java.util.List;
+
+import javax.inject.Inject;
+
+/**
+ * Updates the branches.json file in the root of the repository
+ */
+public class UpdateBranchesJsonTask extends DefaultTask {
+
+    private static final Logger LOGGER = Logging.getLogger(UpdateBranchesJsonTask.class);
+
+    private final ObjectMapper objectMapper;
+    private final BranchesFileParser branchesFileParser;
+
+    @OutputFile
+    private File branchesFile;
+
+    @Input
+    @Optional
+    private DevelopmentBranch addBranch;
+    @Input
+    @Optional
+    private String removeBranch;
+    @Input
+    @Optional
+    private DevelopmentBranch updateBranch;
+
+    @Inject
+    public UpdateBranchesJsonTask(ProjectLayout projectLayout) {
+        this.objectMapper = new ObjectMapper();
+        this.branchesFileParser = new BranchesFileParser(objectMapper);
+        this.branchesFile = projectLayout.getSettingsDirectory().file("branches.json").getAsFile();
+    }
+
+    public File getBranchesFile() {
+        return branchesFile;
+    }
+
+    public void setBranchesFile(File branchesFile) {
+        this.branchesFile = branchesFile;
+    }
+
+    public DevelopmentBranch getAddBranch() {
+        return addBranch;
+    }
+
+    public String getRemoveBranch() {
+        return removeBranch;
+    }
+
+    public DevelopmentBranch getUpdateBranch() {
+        return updateBranch;
+    }
+
+    @Option(option = "add-branch", description = "Specifies the branch and corresponding version to add in format <branch>:<version>")
+    public void addBranch(String branchAndVersion) {
+        this.addBranch = toDevelopmentBranch(branchAndVersion);
+    }
+
+    @Option(option = "remove-branch", description = "Specifies the branch to remove")
+    public void removeBranch(String branch) {
+        this.removeBranch = branch;
+    }
+
+    @Option(option = "update-branch", description = "Specifies the branch and corresponding version to update in format <branch>:<version>")
+    public void updateBranch(String branchAndVersion) {
+        this.updateBranch = toDevelopmentBranch(branchAndVersion);
+    }
+
+    private DevelopmentBranch toDevelopmentBranch(String branchAndVersion) {
+        String[] parts = branchAndVersion.split(":");
+        if (parts.length != 2) {
+            throw new InvalidUserDataException("Expected branch and version in format <branch>:<version>");
+        }
+        return new DevelopmentBranch(parts[0], Version.fromString(parts[1]));
+    }
+
+    @TaskAction
+    public void executeTask() throws IOException {
+        List<DevelopmentBranch> developmentBranches = readBranches(branchesFile);
+
+        if (addBranch == null && removeBranch == null && updateBranch == null) {
+            throw new InvalidUserDataException("At least one of add-branch, remove-branch or update-branch must be specified");
+        }
+
+        if (addBranch != null) {
+            LOGGER.info("Adding branch {} with version {}", addBranch.name(), addBranch.version());
+            if (developmentBranches.stream().anyMatch(developmentBranch -> developmentBranch.name().equals(addBranch.name()))) {
+                throw new InvalidUserDataException("Branch " + addBranch.name() + " already exists");
+            }
+            developmentBranches.add(addBranch);
+        }
+        if (removeBranch != null) {
+            LOGGER.info("Removing branch {}", removeBranch);
+            if (developmentBranches.stream().noneMatch(developmentBranch -> developmentBranch.name().equals(removeBranch))) {
+                throw new InvalidUserDataException("Branch " + removeBranch + " does not exist");
+            }
+            developmentBranches.removeIf(developmentBranch -> developmentBranch.name().equals(removeBranch));
+        }
+        if (updateBranch != null) {
+            LOGGER.info("Updating branch {} with version {}", updateBranch.name(), updateBranch.version());
+            if (developmentBranches.stream().noneMatch(developmentBranch -> developmentBranch.name().equals(updateBranch.name()))) {
+                throw new InvalidUserDataException("Branch " + updateBranch.name() + " does not exist");
+            }
+            developmentBranches.removeIf(developmentBranch -> developmentBranch.name().equals(updateBranch.name()));
+            developmentBranches.add(updateBranch);
+        }
+
+        developmentBranches.sort(Comparator.comparing(DevelopmentBranch::version).reversed());
+
+        JsonNode jsonNode = objectMapper.readTree(new FileInputStream(branchesFile));
+        ArrayNode updatedBranches = objectMapper.createArrayNode();
+        for (DevelopmentBranch branch : developmentBranches) {
+            ObjectNode objectNode = objectMapper.createObjectNode();
+            objectNode.put("branch", branch.name());
+            objectNode.put("version", branch.version().toString());
+            updatedBranches.add(objectNode);
+        }
+        ((ObjectNode) jsonNode).replace("branches", updatedBranches);
+
+        DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
+        prettyPrinter.indentArraysWith(new DefaultIndenter("  ", DefaultIndenter.SYS_LF));
+        objectMapper.writer(prettyPrinter).writeValue(branchesFile, jsonNode);
+    }
+
+    private List<DevelopmentBranch> readBranches(File branchesFile) {
+        if (branchesFile.isFile() == false) {
+            throw new InvalidUserDataException("File branches.json has not been found in " + branchesFile.getAbsolutePath());
+        }
+
+        try {
+            byte[] branchesBytes = Files.readAllBytes(branchesFile.toPath());
+            return branchesFileParser.parse(branchesBytes);
+        } catch (IOException e) {
+            throw new UncheckedIOException("Failed to read branches.json from " + branchesFile.getPath(), e);
+        }
+    }
+}

+ 17 - 2
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/AbstractYamlRestCompatTestPlugin.java

@@ -12,6 +12,7 @@ package org.elasticsearch.gradle.internal.test.rest.compat.compat;
 import org.elasticsearch.gradle.Version;
 import org.elasticsearch.gradle.VersionProperties;
 import org.elasticsearch.gradle.internal.ElasticsearchJavaBasePlugin;
+import org.elasticsearch.gradle.internal.info.GlobalBuildInfoPlugin;
 import org.elasticsearch.gradle.internal.test.rest.CopyRestApiTask;
 import org.elasticsearch.gradle.internal.test.rest.CopyRestTestsTask;
 import org.elasticsearch.gradle.internal.test.rest.LegacyYamlRestTestPlugin;
@@ -42,11 +43,13 @@ import org.gradle.language.jvm.tasks.ProcessResources;
 import java.io.File;
 import java.nio.file.Path;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.Map;
 
 import javax.inject.Inject;
 
 import static org.elasticsearch.gradle.internal.test.rest.RestTestUtil.setupYamlRestTestDependenciesDefaults;
+import static org.elasticsearch.gradle.internal.util.ParamsUtils.loadBuildParams;
 
 /**
  * Apply this plugin to run the YAML based REST tests from a prior major version against this version's cluster.
@@ -75,6 +78,9 @@ public abstract class AbstractYamlRestCompatTestPlugin implements Plugin<Project
 
     @Override
     public void apply(Project project) {
+        project.getRootProject().getRootProject().getPlugins().apply(GlobalBuildInfoPlugin.class);
+        var buildParams = loadBuildParams(project).get();
+
         final Path compatRestResourcesDir = Path.of("restResources").resolve("v" + COMPATIBLE_VERSION);
         final Path compatSpecsDir = compatRestResourcesDir.resolve("yamlSpecs");
         final Path compatTestsDir = compatRestResourcesDir.resolve("yamlTests");
@@ -90,10 +96,19 @@ public abstract class AbstractYamlRestCompatTestPlugin implements Plugin<Project
         SourceSet yamlTestSourceSet = sourceSets.getByName(YamlRestTestPlugin.YAML_REST_TEST);
         GradleUtils.extendSourceSet(project, YamlRestTestPlugin.YAML_REST_TEST, SOURCE_SET_NAME);
 
+        // determine the previous rest compatibility version and BWC project path
+        int currentMajor = buildParams.getBwcVersions().getCurrentVersion().getMajor();
+        Version lastMinor = buildParams.getBwcVersions()
+            .getUnreleased()
+            .stream()
+            .filter(v -> v.getMajor() == currentMajor - 1)
+            .min(Comparator.reverseOrder())
+            .get();
+        String lastMinorProjectPath = buildParams.getBwcVersions().unreleasedInfo(lastMinor).gradleProjectPath();
+
         // copy compatible rest specs
         Configuration bwcMinorConfig = project.getConfigurations().create(BWC_MINOR_CONFIG_NAME);
-        Dependency bwcMinor = project.getDependencies()
-            .project(Map.of("path", ":distribution:bwc:maintenance", "configuration", "checkout"));
+        Dependency bwcMinor = project.getDependencies().project(Map.of("path", lastMinorProjectPath, "configuration", "checkout"));
         project.getDependencies().add(bwcMinorConfig.getName(), bwcMinor);
 
         String projectPath = project.getPath();

+ 230 - 132
build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/BwcVersionsSpec.groovy

@@ -13,33 +13,40 @@ import spock.lang.Specification
 
 import org.elasticsearch.gradle.Version
 import org.elasticsearch.gradle.internal.BwcVersions.UnreleasedVersionInfo
+import org.elasticsearch.gradle.internal.info.DevelopmentBranch
 
 class BwcVersionsSpec extends Specification {
-    List<String> versionLines = []
+    List<Version> versions = []
 
     def "current version is next major"() {
         given:
-        addVersion('7.17.10', '8.9.0')
-        addVersion('8.14.0', '9.9.0')
-        addVersion('8.14.1', '9.9.0')
-        addVersion('8.14.2', '9.9.0')
-        addVersion('8.15.0', '9.9.0')
-        addVersion('8.15.1', '9.9.0')
-        addVersion('8.15.2', '9.9.0')
-        addVersion('8.16.0', '9.10.0')
-        addVersion('8.16.1', '9.10.0')
-        addVersion('8.17.0', '9.10.0')
-        addVersion('9.0.0', '10.0.0')
+        addVersion('7.17.10')
+        addVersion('8.14.0')
+        addVersion('8.14.1')
+        addVersion('8.14.2')
+        addVersion('8.15.0')
+        addVersion('8.15.1')
+        addVersion('8.15.2')
+        addVersion('8.16.0')
+        addVersion('8.16.1')
+        addVersion('8.17.0')
+        addVersion('9.0.0')
 
         when:
-        def bwc = new BwcVersions(versionLines, v('9.0.0'), ['main', '8.x', '8.16', '8.15', '7.17'])
+        def bwc = new BwcVersions(v('9.0.0'), versions, [
+            branch('main', '9.0.0'),
+            branch('8.x', '8.17.0'),
+            branch('8.16', '8.16.1'),
+            branch('8.15', '8.15.2'),
+            branch('7.17', '7.17.10')
+        ])
         def unreleased = bwc.unreleased.collectEntries { [it, bwc.unreleasedInfo(it)] }
 
         then:
         unreleased == [
-            (v('8.15.2')): new UnreleasedVersionInfo(v('8.15.2'), '8.15', ':distribution:bwc:bugfix2'),
-            (v('8.16.1')): new UnreleasedVersionInfo(v('8.16.1'), '8.16', ':distribution:bwc:bugfix'),
-            (v('8.17.0')): new UnreleasedVersionInfo(v('8.17.0'), '8.x', ':distribution:bwc:minor'),
+            (v('8.15.2')): new UnreleasedVersionInfo(v('8.15.2'), '8.15', ':distribution:bwc:major3'),
+            (v('8.16.1')): new UnreleasedVersionInfo(v('8.16.1'), '8.16', ':distribution:bwc:major2'),
+            (v('8.17.0')): new UnreleasedVersionInfo(v('8.17.0'), '8.x', ':distribution:bwc:major1'),
             (v('9.0.0')): new UnreleasedVersionInfo(v('9.0.0'), 'main', ':distribution'),
         ]
         bwc.wireCompatible == [v('8.17.0'), v('9.0.0')]
@@ -48,53 +55,105 @@ class BwcVersionsSpec extends Specification {
 
     def "current version is next major with staged minor"() {
         given:
-        addVersion('7.17.10', '8.9.0')
-        addVersion('8.14.0', '9.9.0')
-        addVersion('8.14.1', '9.9.0')
-        addVersion('8.14.2', '9.9.0')
-        addVersion('8.15.0', '9.9.0')
-        addVersion('8.15.1', '9.9.0')
-        addVersion('8.15.2', '9.9.0')
-        addVersion('8.16.0', '9.10.0')
-        addVersion('8.16.1', '9.10.0')
-        addVersion('8.17.0', '9.10.0')
-        addVersion('8.18.0', '9.10.0')
-        addVersion('9.0.0', '10.0.0')
+        addVersion('7.17.10')
+        addVersion('8.14.0')
+        addVersion('8.14.1')
+        addVersion('8.14.2')
+        addVersion('8.15.0')
+        addVersion('8.15.1')
+        addVersion('8.15.2')
+        addVersion('8.16.0')
+        addVersion('8.16.1')
+        addVersion('8.17.0')
+        addVersion('8.18.0')
+        addVersion('9.0.0')
 
         when:
-        def bwc = new BwcVersions(versionLines, v('9.0.0'), ['main', '8.x', '8.17', '8.16', '8.15', '7.17'])
+        def bwc = new BwcVersions(v('9.0.0'), versions, [
+            branch('main', '9.0.0'),
+            branch('8.x', '8.18.0'),
+            branch('8.17', '8.17.0'),
+            branch('8.16', '8.16.1'),
+            branch('8.15', '8.15.2'),
+            branch('7.17', '7.17.10'),
+        ])
         def unreleased = bwc.unreleased.collectEntries { [it, bwc.unreleasedInfo(it)] }
 
         then:
         unreleased == [
-            (v('8.15.2')): new UnreleasedVersionInfo(v('8.15.2'), '8.15', ':distribution:bwc:bugfix2'),
-            (v('8.16.1')): new UnreleasedVersionInfo(v('8.16.1'), '8.16', ':distribution:bwc:bugfix'),
-            (v('8.17.0')): new UnreleasedVersionInfo(v('8.17.0'), '8.17', ':distribution:bwc:staged'),
-            (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.x', ':distribution:bwc:minor'),
+            (v('8.15.2')): new UnreleasedVersionInfo(v('8.15.2'), '8.15', ':distribution:bwc:major4'),
+            (v('8.16.1')): new UnreleasedVersionInfo(v('8.16.1'), '8.16', ':distribution:bwc:major3'),
+            (v('8.17.0')): new UnreleasedVersionInfo(v('8.17.0'), '8.17', ':distribution:bwc:major2'),
+            (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.x', ':distribution:bwc:major1'),
             (v('9.0.0')): new UnreleasedVersionInfo(v('9.0.0'), 'main', ':distribution'),
         ]
         bwc.wireCompatible == [v('8.18.0'), v('9.0.0')]
         bwc.indexCompatible == [v('8.14.0'), v('8.14.1'), v('8.14.2'), v('8.15.0'), v('8.15.1'), v('8.15.2'), v('8.16.0'), v('8.16.1'), v('8.17.0'), v('8.18.0'), v('9.0.0')]
     }
 
+    def "current version is next major with two staged minors"() {
+        given:
+        addVersion('7.17.10')
+        addVersion('8.15.0')
+        addVersion('8.15.1')
+        addVersion('8.15.2')
+        addVersion('8.16.0')
+        addVersion('8.16.1')
+        addVersion('8.16.2')
+        addVersion('8.17.0')
+        addVersion('8.17.1')
+        addVersion('8.18.0')
+        addVersion('8.19.0')
+        addVersion('9.0.0')
+        addVersion('9.1.0')
+
+        when:
+        def bwc = new BwcVersions(v('9.1.0'), versions, [
+            branch('main', '9.1.0'),
+            branch('9.0', '9.0.0'),
+            branch('8.x', '8.19.0'),
+            branch('8.18', '8.18.0'),
+            branch('8.17', '8.17.1'),
+            branch('8.16', '8.16.2'),
+            branch('7.17', '7.17.10')
+        ])
+        def unreleased = bwc.unreleased.collectEntries { [it, bwc.unreleasedInfo(it)] }
+
+        then:
+        unreleased == [
+            (v('8.16.2')): new UnreleasedVersionInfo(v('8.16.2'), '8.16', ':distribution:bwc:major4'),
+            (v('8.17.1')): new UnreleasedVersionInfo(v('8.17.1'), '8.17', ':distribution:bwc:major3'),
+            (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.18', ':distribution:bwc:major2'),
+            (v('8.19.0')): new UnreleasedVersionInfo(v('8.19.0'), '8.x', ':distribution:bwc:major1'),
+            (v('9.0.0')): new UnreleasedVersionInfo(v('9.0.0'), '9.0', ':distribution:bwc:minor1'),
+            (v('9.1.0')): new UnreleasedVersionInfo(v('9.1.0'), 'main', ':distribution'),
+        ]
+        bwc.wireCompatible == [v('8.19.0'), v('9.0.0'), v('9.1.0')]
+        bwc.indexCompatible == [v('8.15.0'), v('8.15.1'), v('8.15.2'), v('8.16.0'), v('8.16.1'), v('8.16.2'), v('8.17.0'), v('8.17.1'), v('8.18.0'), v('8.19.0'), v('9.0.0'), v('9.1.0')]
+    }
+
     def "current version is first new minor in major series"() {
         given:
-        addVersion('7.17.10', '8.9.0')
-        addVersion('8.16.0', '9.10.0')
-        addVersion('8.16.1', '9.10.0')
-        addVersion('8.17.0', '9.10.0')
-        addVersion('8.18.0', '9.10.0')
-        addVersion('9.0.0', '10.0.0')
-        addVersion('9.1.0', '10.0.0')
+        addVersion('7.17.10')
+        addVersion('8.16.0')
+        addVersion('8.16.1')
+        addVersion('8.17.0')
+        addVersion('8.18.0')
+        addVersion('9.0.0')
+        addVersion('9.1.0')
 
         when:
-        def bwc = new BwcVersions(versionLines, v('9.1.0'), ['main', '9.0', '8.18'])
+        def bwc = new BwcVersions(v('9.1.0'), versions, [
+            branch('main','9.1.0'),
+            branch('9.0', '9.0.0'),
+            branch('8.18', '8.18.0')
+        ])
         def unreleased = bwc.unreleased.collectEntries { [it, bwc.unreleasedInfo(it)] }
 
         then:
         unreleased == [
-            (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.18', ':distribution:bwc:maintenance'),
-            (v('9.0.0')): new UnreleasedVersionInfo(v('9.0.0'), '9.0', ':distribution:bwc:staged'),
+            (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.18', ':distribution:bwc:major1'),
+            (v('9.0.0')): new UnreleasedVersionInfo(v('9.0.0'), '9.0', ':distribution:bwc:minor1'),
             (v('9.1.0')): new UnreleasedVersionInfo(v('9.1.0'), 'main', ':distribution'),
         ]
         bwc.wireCompatible == [v('8.18.0'), v('9.0.0'), v('9.1.0')]
@@ -103,23 +162,27 @@ class BwcVersionsSpec extends Specification {
 
     def "current version is new minor with single bugfix"() {
         given:
-        addVersion('7.17.10', '8.9.0')
-        addVersion('8.16.0', '9.10.0')
-        addVersion('8.16.1', '9.10.0')
-        addVersion('8.17.0', '9.10.0')
-        addVersion('8.18.0', '9.10.0')
-        addVersion('9.0.0', '10.0.0')
-        addVersion('9.0.1', '10.0.0')
-        addVersion('9.1.0', '10.0.0')
+        addVersion('7.17.10')
+        addVersion('8.16.0')
+        addVersion('8.16.1')
+        addVersion('8.17.0')
+        addVersion('8.18.0')
+        addVersion('9.0.0')
+        addVersion('9.0.1')
+        addVersion('9.1.0')
 
         when:
-        def bwc = new BwcVersions(versionLines, v('9.1.0'), ['main', '9.0', '8.18'])
+        def bwc = new BwcVersions(v('9.1.0'), versions, [
+            branch('main','9.1.0'),
+            branch('9.0','9.0.1'),
+            branch('8.18','8.18.0')
+        ])
         def unreleased = bwc.unreleased.collectEntries { [it, bwc.unreleasedInfo(it)] }
 
         then:
         unreleased == [
-            (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.18', ':distribution:bwc:maintenance'),
-            (v('9.0.1')): new UnreleasedVersionInfo(v('9.0.1'), '9.0', ':distribution:bwc:bugfix'),
+            (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.18', ':distribution:bwc:major1'),
+            (v('9.0.1')): new UnreleasedVersionInfo(v('9.0.1'), '9.0', ':distribution:bwc:minor1'),
             (v('9.1.0')): new UnreleasedVersionInfo(v('9.1.0'), 'main', ':distribution'),
         ]
         bwc.wireCompatible == [v('8.18.0'), v('9.0.0'), v('9.0.1'), v('9.1.0')]
@@ -128,25 +191,30 @@ class BwcVersionsSpec extends Specification {
 
     def "current version is new minor with single bugfix and staged minor"() {
         given:
-        addVersion('7.17.10', '8.9.0')
-        addVersion('8.16.0', '9.10.0')
-        addVersion('8.16.1', '9.10.0')
-        addVersion('8.17.0', '9.10.0')
-        addVersion('8.18.0', '9.10.0')
-        addVersion('9.0.0', '10.0.0')
-        addVersion('9.0.1', '10.0.0')
-        addVersion('9.1.0', '10.0.0')
-        addVersion('9.2.0', '10.0.0')
+        addVersion('7.17.10')
+        addVersion('8.16.0')
+        addVersion('8.16.1')
+        addVersion('8.17.0')
+        addVersion('8.18.0')
+        addVersion('9.0.0')
+        addVersion('9.0.1')
+        addVersion('9.1.0')
+        addVersion('9.2.0')
 
         when:
-        def bwc = new BwcVersions(versionLines, v('9.2.0'), ['main', '9.1', '9.0', '8.18'])
+        def bwc = new BwcVersions(v('9.2.0'), versions, [
+            branch('main','9.2.0'),
+            branch('9.1','9.1.0'),
+            branch('9.0','9.0.1'),
+            branch('8.18', '8.18.0')
+        ])
         def unreleased = bwc.unreleased.collectEntries { [it, bwc.unreleasedInfo(it)] }
 
         then:
         unreleased == [
-            (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.18', ':distribution:bwc:maintenance'),
-            (v('9.0.1')): new UnreleasedVersionInfo(v('9.0.1'), '9.0', ':distribution:bwc:bugfix'),
-            (v('9.1.0')): new UnreleasedVersionInfo(v('9.1.0'), '9.1', ':distribution:bwc:staged'),
+            (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.18', ':distribution:bwc:major1'),
+            (v('9.0.1')): new UnreleasedVersionInfo(v('9.0.1'), '9.0', ':distribution:bwc:minor2'),
+            (v('9.1.0')): new UnreleasedVersionInfo(v('9.1.0'), '9.1', ':distribution:bwc:minor1'),
             (v('9.2.0')): new UnreleasedVersionInfo(v('9.2.0'), 'main', ':distribution'),
         ]
         bwc.wireCompatible == [v('8.18.0'), v('9.0.0'), v('9.0.1'), v('9.1.0'), v('9.2.0')]
@@ -155,30 +223,37 @@ class BwcVersionsSpec extends Specification {
 
     def "current version is next minor"() {
         given:
-        addVersion('7.16.3', '8.9.0')
-        addVersion('7.17.0', '8.9.0')
-        addVersion('7.17.1', '8.9.0')
-        addVersion('8.14.0', '9.9.0')
-        addVersion('8.14.1', '9.9.0')
-        addVersion('8.14.2', '9.9.0')
-        addVersion('8.15.0', '9.9.0')
-        addVersion('8.15.1', '9.9.0')
-        addVersion('8.15.2', '9.9.0')
-        addVersion('8.16.0', '9.10.0')
-        addVersion('8.16.1', '9.10.0')
-        addVersion('8.17.0', '9.10.0')
-        addVersion('8.17.1', '9.10.0')
-        addVersion('8.18.0', '9.10.0')
+        addVersion('7.16.3')
+        addVersion('7.17.0')
+        addVersion('7.17.1')
+        addVersion('8.14.0')
+        addVersion('8.14.1')
+        addVersion('8.14.2')
+        addVersion('8.15.0')
+        addVersion('8.15.1')
+        addVersion('8.15.2')
+        addVersion('8.16.0')
+        addVersion('8.16.1')
+        addVersion('8.17.0')
+        addVersion('8.17.1')
+        addVersion('8.18.0')
 
         when:
-        def bwc = new BwcVersions(versionLines, v('8.18.0'), ['main', '8.x', '8.17', '8.16', '7.17'])
+        def bwc = new BwcVersions(v('8.18.0'), versions, [
+            branch('main', '9.1.0'),
+            branch('9.0', '9.0.1'),
+            branch('8.x', '8.18.0'),
+            branch('8.17', '8.17.1'),
+            branch('8.16', '8.16.1'),
+            branch('7.17', '7.17.1'),
+        ])
         def unreleased = bwc.unreleased.collectEntries { [it, bwc.unreleasedInfo(it)] }
 
         then:
         unreleased == [
-            (v('7.17.1')): new UnreleasedVersionInfo(v('7.17.1'), '7.17', ':distribution:bwc:maintenance'),
-            (v('8.16.1')): new UnreleasedVersionInfo(v('8.16.1'), '8.16', ':distribution:bwc:bugfix2'),
-            (v('8.17.1')): new UnreleasedVersionInfo(v('8.17.1'), '8.17', ':distribution:bwc:bugfix'),
+            (v('7.17.1')): new UnreleasedVersionInfo(v('7.17.1'), '7.17', ':distribution:bwc:major1'),
+            (v('8.16.1')): new UnreleasedVersionInfo(v('8.16.1'), '8.16', ':distribution:bwc:minor2'),
+            (v('8.17.1')): new UnreleasedVersionInfo(v('8.17.1'), '8.17', ':distribution:bwc:minor1'),
             (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.x', ':distribution'),
         ]
         bwc.wireCompatible == [v('7.17.0'), v('7.17.1'), v('8.14.0'), v('8.14.1'), v('8.14.2'), v('8.15.0'), v('8.15.1'), v('8.15.2'), v('8.16.0'), v('8.16.1'), v('8.17.0'), v('8.17.1'), v('8.18.0')]
@@ -187,30 +262,37 @@ class BwcVersionsSpec extends Specification {
 
     def "current version is new minor with staged minor"() {
         given:
-        addVersion('7.16.3', '8.9.0')
-        addVersion('7.17.0', '8.9.0')
-        addVersion('7.17.1', '8.9.0')
-        addVersion('8.14.0', '9.9.0')
-        addVersion('8.14.1', '9.9.0')
-        addVersion('8.14.2', '9.9.0')
-        addVersion('8.15.0', '9.9.0')
-        addVersion('8.15.1', '9.9.0')
-        addVersion('8.15.2', '9.9.0')
-        addVersion('8.16.0', '9.10.0')
-        addVersion('8.16.1', '9.10.0')
-        addVersion('8.17.0', '9.10.0')
-        addVersion('8.18.0', '9.10.0')
+        addVersion('7.16.3')
+        addVersion('7.17.0')
+        addVersion('7.17.1')
+        addVersion('8.14.0')
+        addVersion('8.14.1')
+        addVersion('8.14.2')
+        addVersion('8.15.0')
+        addVersion('8.15.1')
+        addVersion('8.15.2')
+        addVersion('8.16.0')
+        addVersion('8.16.1')
+        addVersion('8.17.0')
+        addVersion('8.18.0')
 
         when:
-        def bwc = new BwcVersions(versionLines, v('8.18.0'), ['main', '8.x', '8.17', '8.16', '8.15', '7.17'])
+        def bwc = new BwcVersions(v('8.18.0'), versions, [
+            branch('main', '9.0.0'),
+            branch('8.x', '8.18.0'),
+            branch('8.17', '8.17.0'),
+            branch('8.16', '8.16.1'),
+            branch('8.15', '8.15.2'),
+            branch('7.17', '7.17.1'),
+        ])
         def unreleased = bwc.unreleased.collectEntries { [it, bwc.unreleasedInfo(it)] }
 
         then:
         unreleased == [
-            (v('7.17.1')): new UnreleasedVersionInfo(v('7.17.1'), '7.17', ':distribution:bwc:maintenance'),
-            (v('8.15.2')): new UnreleasedVersionInfo(v('8.15.2'), '8.15', ':distribution:bwc:bugfix2'),
-            (v('8.16.1')): new UnreleasedVersionInfo(v('8.16.1'), '8.16', ':distribution:bwc:bugfix'),
-            (v('8.17.0')): new UnreleasedVersionInfo(v('8.17.0'), '8.17', ':distribution:bwc:staged'),
+            (v('7.17.1')): new UnreleasedVersionInfo(v('7.17.1'), '7.17', ':distribution:bwc:major1'),
+            (v('8.15.2')): new UnreleasedVersionInfo(v('8.15.2'), '8.15', ':distribution:bwc:minor3'),
+            (v('8.16.1')): new UnreleasedVersionInfo(v('8.16.1'), '8.16', ':distribution:bwc:minor2'),
+            (v('8.17.0')): new UnreleasedVersionInfo(v('8.17.0'), '8.17', ':distribution:bwc:minor1'),
             (v('8.18.0')): new UnreleasedVersionInfo(v('8.18.0'), '8.x', ':distribution'),
         ]
         bwc.wireCompatible == [v('7.17.0'), v('7.17.1'), v('8.14.0'), v('8.14.1'), v('8.14.2'), v('8.15.0'), v('8.15.1'), v('8.15.2'), v('8.16.0'), v('8.16.1'), v('8.17.0'), v('8.18.0')]
@@ -219,26 +301,33 @@ class BwcVersionsSpec extends Specification {
 
     def "current version is first bugfix"() {
         given:
-        addVersion('7.16.3', '8.9.0')
-        addVersion('7.17.0', '8.9.0')
-        addVersion('7.17.1', '8.9.0')
-        addVersion('8.14.0', '9.9.0')
-        addVersion('8.14.1', '9.9.0')
-        addVersion('8.14.2', '9.9.0')
-        addVersion('8.15.0', '9.9.0')
-        addVersion('8.15.1', '9.9.0')
-        addVersion('8.15.2', '9.9.0')
-        addVersion('8.16.0', '9.10.0')
-        addVersion('8.16.1', '9.10.0')
+        addVersion('7.16.3')
+        addVersion('7.17.0')
+        addVersion('7.17.1')
+        addVersion('8.14.0')
+        addVersion('8.14.1')
+        addVersion('8.14.2')
+        addVersion('8.15.0')
+        addVersion('8.15.1')
+        addVersion('8.15.2')
+        addVersion('8.16.0')
+        addVersion('8.16.1')
 
         when:
-        def bwc = new BwcVersions(versionLines, v('8.16.1'), ['main', '8.x', '8.17', '8.16', '8.15', '7.17'])
+        def bwc = new BwcVersions(v('8.16.1'), versions, [
+            branch('main','9.0.1'),
+            branch('8.x','8.18.0'),
+            branch('8.17','8.17.0'),
+            branch('8.16','8.16.1'),
+            branch('8.15','8.15.2'),
+            branch('7.17','7.17.1'),
+        ])
         def unreleased = bwc.unreleased.collectEntries { [it, bwc.unreleasedInfo(it)] }
 
         then:
         unreleased == [
-            (v('7.17.1')): new UnreleasedVersionInfo(v('7.17.1'), '7.17', ':distribution:bwc:maintenance'),
-            (v('8.15.2')): new UnreleasedVersionInfo(v('8.15.2'), '8.15', ':distribution:bwc:bugfix'),
+            (v('7.17.1')): new UnreleasedVersionInfo(v('7.17.1'), '7.17', ':distribution:bwc:major1'),
+            (v('8.15.2')): new UnreleasedVersionInfo(v('8.15.2'), '8.15', ':distribution:bwc:minor1'),
             (v('8.16.1')): new UnreleasedVersionInfo(v('8.16.1'), '8.16', ':distribution'),
         ]
         bwc.wireCompatible == [v('7.17.0'), v('7.17.1'), v('8.14.0'), v('8.14.1'), v('8.14.2'), v('8.15.0'), v('8.15.1'), v('8.15.2'), v('8.16.0'), v('8.16.1')]
@@ -247,37 +336,46 @@ class BwcVersionsSpec extends Specification {
 
     def "current version is second bugfix"() {
         given:
-        addVersion('7.16.3', '8.9.0')
-        addVersion('7.17.0', '8.9.0')
-        addVersion('7.17.1', '8.9.0')
-        addVersion('8.14.0', '9.9.0')
-        addVersion('8.14.1', '9.9.0')
-        addVersion('8.14.2', '9.9.0')
-        addVersion('8.15.0', '9.9.0')
-        addVersion('8.15.1', '9.9.0')
-        addVersion('8.15.2', '9.9.0')
+        addVersion('7.16.3')
+        addVersion('7.17.0')
+        addVersion('7.17.1')
+        addVersion('8.14.0')
+        addVersion('8.14.1')
+        addVersion('8.14.2')
+        addVersion('8.15.0')
+        addVersion('8.15.1')
+        addVersion('8.15.2')
 
         when:
-        def bwc = new BwcVersions(versionLines, v('8.15.2'), ['main', '8.x', '8.17', '8.16', '8.15', '7.17'])
+        def bwc = new BwcVersions(v('8.15.2'), versions, [
+            branch('main', '9.0.1'),
+            branch('8.x', '8.18.1'),
+            branch('8.17', '8.17.2'),
+            branch('8.16', '8.16.10'),
+            branch('8.15', '8.15.2'),
+            branch('7.17', '7.17.1'),
+        ])
         def unreleased = bwc.unreleased.collectEntries { [it, bwc.unreleasedInfo(it)] }
 
         then:
         unreleased == [
-            (v('7.17.1')): new UnreleasedVersionInfo(v('7.17.1'), '7.17', ':distribution:bwc:maintenance'),
+            (v('7.17.1')): new UnreleasedVersionInfo(v('7.17.1'), '7.17', ':distribution:bwc:major1'),
             (v('8.15.2')): new UnreleasedVersionInfo(v('8.15.2'), '8.15', ':distribution'),
         ]
         bwc.wireCompatible == [v('7.17.0'), v('7.17.1'), v('8.14.0'), v('8.14.1'), v('8.14.2'), v('8.15.0'), v('8.15.1'), v('8.15.2')]
         bwc.indexCompatible == [v('7.16.3'), v('7.17.0'), v('7.17.1'), v('8.14.0'), v('8.14.1'), v('8.14.2'), v('8.15.0'), v('8.15.1'), v('8.15.2')]
     }
 
-    private void addVersion(String elasticsearch, String lucene) {
-        def es = Version.fromString(elasticsearch)
-        def l = Version.fromString(lucene)
-        versionLines << "    public static final Version V_${es.major}_${es.minor}_${es.revision} = new Version(0000000, org.apache.lucene.util.Version.LUCENE_${l.major}_${l.minor}_${l.revision});".toString()
+    private void addVersion(String elasticsearch) {
+        versions.add(Version.fromString(elasticsearch))
     }
 
     private Version v(String version) {
         return Version.fromString(version)
     }
 
+    private DevelopmentBranch branch(String name, String version) {
+        return new DevelopmentBranch(name, v(version))
+    }
+
 }

+ 111 - 0
build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPluginSpec.groovy

@@ -0,0 +1,111 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.info
+
+import groovy.json.JsonOutput
+import spock.lang.Specification
+import spock.lang.TempDir
+
+import org.elasticsearch.gradle.Version
+import org.elasticsearch.gradle.internal.BwcVersions
+import org.gradle.api.Project
+import org.gradle.api.provider.Provider
+import org.gradle.api.provider.ProviderFactory
+import org.gradle.testfixtures.ProjectBuilder
+
+import java.nio.file.Path
+
+class GlobalBuildInfoPluginSpec extends Specification {
+
+    @TempDir
+    File projectRoot
+
+    Project project
+
+    def setup() {
+        project = ProjectBuilder.builder()
+            .withProjectDir(projectRoot)
+            .withName("bwcTestProject")
+            .build()
+        project = Spy(project)
+        project.getRootProject() >> project
+
+        File buildToolsInternalDir = new File(projectRoot, "build-tools-internal")
+        buildToolsInternalDir.mkdirs()
+        new File(buildToolsInternalDir, "version.properties").text = """
+            elasticsearch     = 9.1.0
+            lucene            = 10.2.2
+
+            bundled_jdk_vendor = openjdk
+            bundled_jdk = 24+36@1f9ff9062db4449d8ca828c504ffae90
+            minimumJdkVersion = 21
+            minimumRuntimeJava = 21
+            minimumCompilerJava = 21
+        """
+        File versionFileDir = new File(projectRoot, "server/src/main/java/org/elasticsearch")
+        versionFileDir.mkdirs()
+        new File(versionFileDir, "Version.java").text = """
+            package org.elasticsearch;
+            public class Version {
+                public static final Version V_8_17_8 = new Version(8_17_08_99);
+                public static final Version V_8_18_0 = new Version(8_18_00_99);
+                public static final Version V_8_18_1 = new Version(8_18_01_99);
+                public static final Version V_8_18_2 = new Version(8_18_02_99);
+                public static final Version V_8_18_3 = new Version(8_18_03_99);
+                public static final Version V_8_19_0 = new Version(8_19_00_99);
+                public static final Version V_9_0_0 = new Version(9_00_00_99);
+                public static final Version V_9_0_1 = new Version(9_00_01_99);
+                public static final Version V_9_0_2 = new Version(9_00_02_99);
+                public static final Version V_9_0_3 = new Version(9_00_03_99);
+                public static final Version V_9_1_0 = new Version(9_01_00_99);
+                public static final Version CURRENT = V_9_1_0;
+
+            }
+        """
+    }
+
+    def "resolve unreleased versions from branches file set by Gradle property"() {
+        given:
+        ProviderFactory providerFactorySpy = Spy(project.getProviders())
+        Path branchesJsonPath = projectRoot.toPath().resolve("myBranches.json")
+        Provider<String> gradleBranchesLocationProvider = project.providers.provider { return branchesJsonPath.toString() }
+        providerFactorySpy.gradleProperty("org.elasticsearch.build.branches-file-location") >> gradleBranchesLocationProvider
+        project.getProviders() >> providerFactorySpy
+        branchesJsonPath.text = branchesJson(
+            [
+                new DevelopmentBranch("main", Version.fromString("9.1.0")),
+                new DevelopmentBranch("9.0", Version.fromString("9.0.3")),
+                new DevelopmentBranch("8.19", Version.fromString("8.19.1")),
+                new DevelopmentBranch("8.18", Version.fromString("8.18.2")),
+            ]
+        )
+
+        when:
+        project.objects.newInstance(GlobalBuildInfoPlugin).apply(project)
+        BuildParameterExtension ext = project.extensions.getByType(BuildParameterExtension)
+        BwcVersions bwcVersions = ext.bwcVersions
+
+        then:
+        bwcVersions != null
+        bwcVersions.unreleased.toSet() == ["9.1.0", "9.0.3", "8.19.1", "8.18.2"].collect { Version.fromString(it) }.toSet()
+    }
+
+    String branchesJson(List<DevelopmentBranch> branches) {
+        Map<String, Object> branchesFileContent = [
+            branches: branches.collect { branch ->
+                [
+                    branch : branch.name(),
+                    version: branch.version().toString(),
+                ]
+            }
+        ]
+        return JsonOutput.prettyPrint(JsonOutput.toJson(branchesFileContent))
+    }
+}

+ 267 - 0
build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/release/UpdateBranchesJsonTaskSpec.groovy

@@ -0,0 +1,267 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.release
+
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+import spock.lang.Specification
+import spock.lang.Subject
+
+import org.elasticsearch.gradle.Version
+import org.elasticsearch.gradle.internal.info.DevelopmentBranch
+import org.gradle.api.InvalidUserDataException
+import org.gradle.api.Project
+import org.gradle.testfixtures.ProjectBuilder
+import spock.lang.TempDir
+
+class UpdateBranchesJsonTaskSpec extends Specification {
+
+    @Subject
+    UpdateBranchesJsonTask task
+
+    @TempDir
+    File projectRoot
+    JsonSlurper jsonSlurper = new JsonSlurper()
+
+    def setup() {
+        Project project = ProjectBuilder.builder().withProjectDir(projectRoot).build()
+        task = project.tasks.register("updateBranchesJson", UpdateBranchesJsonTask).get()
+        task.branchesFile = new File(projectRoot, "branches.json")
+    }
+
+    def "add new branch to branches.json (sorted by version) when --add-branch is specified"() {
+        given:
+        branchesJson([
+            new DevelopmentBranch("main", Version.fromString("9.1.0")),
+            new DevelopmentBranch("8.18", Version.fromString("8.18.2")),
+            new DevelopmentBranch("8.17", Version.fromString("8.17.7")),
+        ])
+        task.addBranch('8.19:8.19.0')
+
+        when:
+        task.executeTask()
+
+        then:
+        def branchesFile = new File(projectRoot, "branches.json")
+        def json = jsonSlurper.parse(branchesFile)
+        json.branches == [
+                [branch: 'main', version: '9.1.0'],
+                [branch: '8.19', version: '8.19.0'],
+                [branch: '8.18', version: '8.18.2'],
+                [branch: '8.17', version: '8.17.7'],
+        ]
+    }
+
+    def "remove branch from branches.json when --remove-branch is specified"() {
+        given:
+        branchesJson([
+            new DevelopmentBranch("main", Version.fromString("9.1.0")),
+            new DevelopmentBranch("8.18", Version.fromString("8.18.2")),
+            new DevelopmentBranch("8.17", Version.fromString("8.17.7")),
+        ])
+        task.removeBranch('8.18')
+
+        when:
+        task.executeTask()
+
+        then:
+        def branchesFile = new File(projectRoot, "branches.json")
+        def json = jsonSlurper.parse(branchesFile)
+        json.branches == [
+                [branch: 'main', version: '9.1.0'],
+                [branch: '8.17', version: '8.17.7'],
+        ]
+    }
+
+    def "update branch version when --update-branch is specified"() {
+        given:
+        branchesJson([
+            new DevelopmentBranch("main", Version.fromString("9.1.0")),
+            new DevelopmentBranch("8.18", Version.fromString("8.18.2")),
+            new DevelopmentBranch("8.17", Version.fromString("8.17.7")),
+        ])
+        task.updateBranch('8.18:8.18.3')
+
+        when:
+        task.executeTask()
+
+        then:
+        def branchesFile = new File(projectRoot, "branches.json")
+        def json = jsonSlurper.parse(branchesFile)
+        json.branches == [
+                [branch: 'main', version: '9.1.0'],
+                [branch: '8.18', version: '8.18.3'],
+                [branch: '8.17', version: '8.17.7'],
+        ]
+    }
+
+    def "change branches.json when multiple options are specified"() {
+        given:
+        branchesJson([
+            new DevelopmentBranch("main", Version.fromString("9.1.0")),
+            new DevelopmentBranch("8.18", Version.fromString("8.18.2")),
+            new DevelopmentBranch("8.17", Version.fromString("8.17.7")),
+        ])
+        task.addBranch('8.19:8.19.0')
+        task.removeBranch('8.18')
+        task.updateBranch('8.17:8.17.8')
+
+        when:
+        task.executeTask()
+
+        then:
+        def branchesFile = new File(projectRoot, "branches.json")
+        def json = jsonSlurper.parse(branchesFile)
+        json.branches == [
+                [branch: 'main', version: '9.1.0'],
+                [branch: '8.19', version: '8.19.0'],
+                [branch: '8.17', version: '8.17.8'],
+        ]
+    }
+
+    def "fail when no --add-branch, --remove-branch or --update-branch is specified"() {
+        given:
+        branchesJson([
+            new DevelopmentBranch("main", Version.fromString("9.1.0")),
+            new DevelopmentBranch("8.18", Version.fromString("8.18.2")),
+            new DevelopmentBranch("8.17", Version.fromString("8.17.7")),
+        ])
+
+        when:
+        task.executeTask()
+
+        then:
+        def branchesFile = new File(projectRoot, "branches.json")
+        def json = jsonSlurper.parse(branchesFile)
+        json.branches == [
+            [branch: 'main', version: '9.1.0'],
+            [branch: '8.18', version: '8.18.2'],
+            [branch: '8.17', version: '8.17.7'],
+        ]
+        thrown(InvalidUserDataException)
+    }
+
+    def "fail when adding a branch that already exists"() {
+        given:
+        branchesJson([
+            new DevelopmentBranch("main", Version.fromString("9.1.0")),
+            new DevelopmentBranch("8.18", Version.fromString("8.18.2")),
+            new DevelopmentBranch("8.17", Version.fromString("8.17.7")),
+        ])
+        task.addBranch('8.18:8.18.3')
+
+        when:
+        task.executeTask()
+
+        then:
+        def branchesFile = new File(projectRoot, "branches.json")
+        def json = jsonSlurper.parse(branchesFile)
+        json.branches == [
+                [branch: 'main', version: '9.1.0'],
+                [branch: '8.18', version: '8.18.2'],
+                [branch: '8.17', version: '8.17.7'],
+        ]
+        thrown(InvalidUserDataException)
+    }
+
+    def "fail when removing a branch that does not exist"() {
+        given:
+        branchesJson([
+            new DevelopmentBranch("main", Version.fromString("9.1.0")),
+            new DevelopmentBranch("8.18", Version.fromString("8.18.2")),
+            new DevelopmentBranch("8.17", Version.fromString("8.17.7")),
+        ])
+        task.removeBranch('8.19')
+
+        when:
+        task.executeTask()
+
+        then:
+        def branchesFile = new File(projectRoot, "branches.json")
+        def json = jsonSlurper.parse(branchesFile)
+        json.branches == [
+                [branch: 'main', version: '9.1.0'],
+                [branch: '8.18', version: '8.18.2'],
+                [branch: '8.17', version: '8.17.7'],
+        ]
+        thrown(InvalidUserDataException)
+    }
+
+    def "fail when updating a branch that does not exist"() {
+        given:
+        branchesJson([
+            new DevelopmentBranch("main", Version.fromString("9.1.0")),
+            new DevelopmentBranch("8.18", Version.fromString("8.18.2")),
+            new DevelopmentBranch("8.17", Version.fromString("8.17.7")),
+        ])
+        task.updateBranch('8.19:8.19.0')
+
+        when:
+        task.executeTask()
+
+        then:
+        def branchesFile = new File(projectRoot, "branches.json")
+        def json = jsonSlurper.parse(branchesFile)
+        json.branches == [
+                [branch: 'main', version: '9.1.0'],
+                [branch: '8.18', version: '8.18.2'],
+                [branch: '8.17', version: '8.17.7'],
+        ]
+        thrown(InvalidUserDataException)
+    }
+
+    def "fail when adding a branch with an invalid version"() {
+        given:
+        branchesJson([
+            new DevelopmentBranch("main", Version.fromString("9.1.0")),
+            new DevelopmentBranch("8.18", Version.fromString("8.18.2")),
+            new DevelopmentBranch("8.17", Version.fromString("8.17.7")),
+        ])
+
+        when:
+        task.addBranch('8.19:invalid')
+        task.executeTask()
+
+        then:
+        def branchesFile = new File(projectRoot, "branches.json")
+        def json = jsonSlurper.parse(branchesFile)
+        json.branches == [
+                [branch: 'main', version: '9.1.0'],
+                [branch: '8.18', version: '8.18.2'],
+                [branch: '8.17', version: '8.17.7'],
+        ]
+        thrown(IllegalArgumentException)
+    }
+
+    def "fail when branches.json is missing"() {
+        given:
+        task.updateBranch('8.19:8.19.0')
+
+        when:
+        task.executeTask()
+
+        then:
+        def exception = thrown(InvalidUserDataException)
+        exception.message.contains("branches.json has not been found")
+    }
+
+    void branchesJson(List<DevelopmentBranch> branches) {
+        File branchesFile = new File(projectRoot, "branches.json")
+        Map<String, Object> branchesFileContent = [
+            branches: branches.collect { branch ->
+                [
+                    branch: branch.name(),
+                    version: branch.version().toString(),
+                ]
+            }
+        ]
+        branchesFile.text = JsonOutput.prettyPrint(JsonOutput.toJson(branchesFileContent))
+    }
+}

+ 7 - 1
build-tools-internal/src/test/java/org/elasticsearch/gradle/AbstractDistributionDownloadPluginTests.java

@@ -10,6 +10,7 @@
 package org.elasticsearch.gradle;
 
 import org.elasticsearch.gradle.internal.BwcVersions;
+import org.elasticsearch.gradle.internal.info.DevelopmentBranch;
 import org.gradle.api.NamedDomainObjectContainer;
 import org.gradle.api.Project;
 import org.gradle.testfixtures.ProjectBuilder;
@@ -29,7 +30,12 @@ public class AbstractDistributionDownloadPluginTests {
     protected static final Version BWC_STAGED_VERSION = Version.fromString("1.0.0");
     protected static final Version BWC_BUGFIX_VERSION = Version.fromString("1.0.1");
     protected static final Version BWC_MAINTENANCE_VERSION = Version.fromString("0.90.1");
-    protected static final List<String> DEVELOPMENT_BRANCHES = Arrays.asList("main", "1.1", "1.0", "0.90");
+    protected static final List<DevelopmentBranch> DEVELOPMENT_BRANCHES = Arrays.asList(
+        new DevelopmentBranch("main", Version.fromString("2.0.0")),
+        new DevelopmentBranch("1.1", Version.fromString("1.1.0")),
+        new DevelopmentBranch("1.0", Version.fromString("1.0.1")),
+        new DevelopmentBranch("0.90", Version.fromString("0.90.1"))
+    );
 
     protected static final BwcVersions BWC_MINOR = new BwcVersions(
         BWC_MAJOR_VERSION,

+ 5 - 0
build-tools/src/main/java/org/elasticsearch/gradle/VersionProperties.java

@@ -16,7 +16,12 @@ import java.util.Properties;
 
 /**
  * Accessor for shared dependency versions used by elasticsearch, namely the elasticsearch and lucene versions.
+ *
+ * @deprecated use ext values set by {@link org.elasticsearch.gradle.internal.conventions.VersionPropertiesPlugin} or
+ * {@link org.elasticsearch.gradle.internal.conventions.VersionPropertiesBuildService}
+ *
  */
+@Deprecated
 public class VersionProperties {
 
     public static String getElasticsearch() {

+ 34 - 24
build-tools/src/testFixtures/groovy/org/elasticsearch/gradle/fixtures/AbstractGradleFuncTest.groovy

@@ -9,25 +9,22 @@
 
 package org.elasticsearch.gradle.fixtures
 
+import spock.lang.Specification
+import spock.lang.TempDir
+
 import org.apache.commons.io.FileUtils
 import org.apache.commons.io.IOUtils
 import org.elasticsearch.gradle.internal.test.BuildConfigurationAwareGradleRunner
 import org.elasticsearch.gradle.internal.test.InternalAwareGradleRunner
 import org.elasticsearch.gradle.internal.test.NormalizeOutputGradleRunner
 import org.elasticsearch.gradle.internal.test.TestResultExtension
-import org.gradle.internal.component.external.model.ComponentVariant
 import org.gradle.testkit.runner.BuildResult
 import org.gradle.testkit.runner.GradleRunner
 import org.junit.Rule
 import org.junit.rules.TemporaryFolder
-import spock.lang.Specification
-import spock.lang.TempDir
 
 import java.lang.management.ManagementFactory
 import java.nio.charset.StandardCharsets
-import java.nio.file.Files
-import java.io.File
-import java.nio.file.Path
 import java.util.jar.JarEntry
 import java.util.jar.JarOutputStream
 import java.util.zip.ZipEntry
@@ -47,6 +44,7 @@ abstract class AbstractGradleFuncTest extends Specification {
     File buildFile
     File propertiesFile
     File projectDir
+    File versionPropertiesFile
 
     protected boolean configurationCacheCompatible = true
     protected boolean buildApiRestrictionsDisabled = false
@@ -57,6 +55,18 @@ abstract class AbstractGradleFuncTest extends Specification {
         settingsFile << "rootProject.name = 'hello-world'\n"
         buildFile = testProjectDir.newFile('build.gradle')
         propertiesFile = testProjectDir.newFile('gradle.properties')
+        File buildToolsDir = testProjectDir.newFolder("build-tools-internal")
+        versionPropertiesFile = new File(buildToolsDir, 'version.properties')
+        versionPropertiesFile.text = """
+            elasticsearch     = 9.1.0
+            lucene            = 10.2.2
+
+            bundled_jdk_vendor = openjdk
+            bundled_jdk = 24+36@1f9ff9062db4449d8ca828c504ffae90
+            minimumJdkVersion = 21
+            minimumRuntimeJava = 21
+            minimumCompilerJava = 21
+        """
         propertiesFile <<
             "org.gradle.java.installations.fromEnv=JAVA_HOME,RUNTIME_JAVA_HOME,JAVA15_HOME,JAVA14_HOME,JAVA13_HOME,JAVA12_HOME,JAVA11_HOME,JAVA8_HOME"
 
@@ -162,10 +172,10 @@ abstract class AbstractGradleFuncTest extends Specification {
     File internalBuild(
             List<String> extraPlugins = [],
             String maintenance = "7.16.10",
-            String bugfix2 = "8.1.3",
-            String bugfix = "8.2.1",
-            String staged = "8.3.0",
-            String minor = "8.4.0",
+            String major4 = "8.1.3",
+            String major3 = "8.2.1",
+            String major2 = "8.3.0",
+            String major1 = "8.4.0",
             String current = "9.0.0"
     ) {
         buildFile << """plugins {
@@ -175,31 +185,31 @@ abstract class AbstractGradleFuncTest extends Specification {
         import org.elasticsearch.gradle.Architecture
 
         import org.elasticsearch.gradle.internal.BwcVersions
+        import org.elasticsearch.gradle.internal.info.DevelopmentBranch
         import org.elasticsearch.gradle.Version
 
         Version currentVersion = Version.fromString("${current}")
         def versionList = [
           Version.fromString("$maintenance"),
-          Version.fromString("$bugfix2"),
-          Version.fromString("$bugfix"),
-          Version.fromString("$staged"),
-          Version.fromString("$minor"),
+          Version.fromString("$major4"),
+          Version.fromString("$major3"),
+          Version.fromString("$major2"),
+          Version.fromString("$major1"),
           currentVersion
         ]
 
-        BwcVersions versions = new BwcVersions(currentVersion, versionList, ['main', '8.x', '8.3', '8.2', '8.1', '7.16'])
-        buildParams.setBwcVersions(project.provider { versions} )
+        BwcVersions versions = new BwcVersions(currentVersion, versionList, [
+          new DevelopmentBranch('main', Version.fromString("$current")),
+          new DevelopmentBranch('8.x', Version.fromString("$major1")),
+          new DevelopmentBranch('8.3', Version.fromString("$major2")),
+          new DevelopmentBranch('8.2', Version.fromString("$major3")),
+          new DevelopmentBranch('8.1', Version.fromString("$major4")),
+          new DevelopmentBranch('7.16', Version.fromString("$maintenance")),
+        ])
+        buildParams.setBwcVersions(project.provider { versions } )
         """
     }
 
-    void setupLocalGitRepo() {
-        execute("git init")
-        execute('git config user.email "build-tool@elastic.co"')
-        execute('git config user.name "Build tool"')
-        execute("git add .")
-        execute('git commit -m "Initial"')
-    }
-
     void execute(String command, File workingDir = testProjectDir.root) {
         def proc = command.execute(Collections.emptyList(), workingDir)
         proc.waitFor()

+ 8 - 5
build.gradle

@@ -322,11 +322,14 @@ allprojects {
       resolveJavaToolChain = true
 
       // ensure we have best possible caching of bwc builds
-      dependsOn ":distribution:bwc:bugfix:buildBwcLinuxTar"
-      dependsOn ":distribution:bwc:bugfix2:buildBwcLinuxTar"
-      dependsOn ":distribution:bwc:minor:buildBwcLinuxTar"
-      dependsOn ":distribution:bwc:staged:buildBwcLinuxTar"
-      dependsOn ":distribution:bwc:staged2:buildBwcLinuxTar"
+      dependsOn ":distribution:bwc:major1:buildBwcLinuxTar"
+      dependsOn ":distribution:bwc:major2:buildBwcLinuxTar"
+      dependsOn ":distribution:bwc:major3:buildBwcLinuxTar"
+      dependsOn ":distribution:bwc:major4:buildBwcLinuxTar"
+      dependsOn ":distribution:bwc:minor1:buildBwcLinuxTar"
+      dependsOn ":distribution:bwc:minor2:buildBwcLinuxTar"
+      dependsOn ":distribution:bwc:minor3:buildBwcLinuxTar"
+      dependsOn ":distribution:bwc:minor4:buildBwcLinuxTar"
     }
     if (project.path.contains("fixture")) {
       dependsOn tasks.withType(ComposePull)

+ 0 - 0
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/staged/build.gradle → distribution/bwc/major1/build.gradle


+ 0 - 0
build-tools-internal/src/integTest/resources/org/elasticsearch/gradle/internal/fake_git/remote/distribution/bwc/staged2/build.gradle → distribution/bwc/major2/build.gradle


+ 0 - 0
distribution/bwc/bugfix/build.gradle → distribution/bwc/major3/build.gradle


+ 0 - 0
distribution/bwc/bugfix2/build.gradle → distribution/bwc/major4/build.gradle


+ 0 - 0
distribution/bwc/maintenance/build.gradle → distribution/bwc/minor1/build.gradle


+ 0 - 0
distribution/bwc/minor/build.gradle → distribution/bwc/minor2/build.gradle


+ 0 - 0
distribution/bwc/staged/build.gradle → distribution/bwc/minor3/build.gradle


+ 0 - 0
distribution/bwc/minor4/build.gradle


+ 8 - 5
settings.gradle

@@ -79,11 +79,14 @@ List projects = [
   'distribution:packages:deb',
   'distribution:packages:aarch64-rpm',
   'distribution:packages:rpm',
-  'distribution:bwc:bugfix',
-  'distribution:bwc:bugfix2',
-  'distribution:bwc:maintenance',
-  'distribution:bwc:minor',
-  'distribution:bwc:staged',
+  'distribution:bwc:major1',
+  'distribution:bwc:major2',
+  'distribution:bwc:major3',
+  'distribution:bwc:major4',
+  'distribution:bwc:minor1',
+  'distribution:bwc:minor2',
+  'distribution:bwc:minor3',
+  'distribution:bwc:minor4',
   'distribution:tools:java-version-checker',
   'distribution:tools:cli-launcher',
   'distribution:tools:server-cli',