Browse Source

Make improvements to the release notes generator (#83525)

Forward-port of the `build-tools-internal` parts of #83341.
Rory Hunter 3 years ago
parent
commit
bafdcbd2f1
21 changed files with 412 additions and 509 deletions
  1. 1 1
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ValidateJsonAgainstSchemaTask.java
  2. 1 1
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ValidateYamlAgainstSchemaTask.java
  3. 27 103
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/BreakingChangesGenerator.java
  4. 21 66
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ChangelogEntry.java
  5. 13 39
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/GenerateReleaseNotesTask.java
  6. 15 31
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/PruneChangelogsTask.java
  7. 2 6
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ReleaseHighlightsGenerator.java
  8. 1 5
      build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ReleaseToolsPlugin.java
  9. 7 30
      build-tools-internal/src/main/resources/changelog-schema.json
  10. 0 39
      build-tools-internal/src/main/resources/templates/breaking-changes-area.asciidoc
  11. 63 18
      build-tools-internal/src/main/resources/templates/breaking-changes.asciidoc
  12. 11 1
      build-tools-internal/src/main/resources/templates/release-highlights.asciidoc
  13. 36 28
      build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.java
  14. 6 5
      build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/GenerateReleaseNotesTaskTest.java
  15. 11 65
      build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/PruneChangelogsTaskTests.java
  16. 18 0
      build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/ReleaseHighlightsGeneratorTest.java
  17. 0 33
      build-tools-internal/src/test/resources/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.generateAreaFile.asciidoc
  18. 0 36
      build-tools-internal/src/test/resources/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.generateIndexFile.asciidoc
  19. 149 0
      build-tools-internal/src/test/resources/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.generateMigrationFile.asciidoc
  20. 1 2
      build-tools-internal/src/test/resources/org/elasticsearch/gradle/internal/release/ReleaseHighlightsGeneratorTest.generateFile.asciidoc
  21. 29 0
      build-tools-internal/src/test/resources/org/elasticsearch/gradle/internal/release/ReleaseHighlightsGeneratorTest.noHighlights.generateFile.asciidoc

+ 1 - 1
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ValidateJsonAgainstSchemaTask.java

@@ -42,7 +42,7 @@ import java.util.Set;
 import java.util.stream.StreamSupport;
 import java.util.stream.StreamSupport;
 
 
 /**
 /**
- * Incremental task to validate a set of JSON files against against a schema.
+ * Incremental task to validate a set of JSON files against a schema.
  */
  */
 public class ValidateJsonAgainstSchemaTask extends DefaultTask {
 public class ValidateJsonAgainstSchemaTask extends DefaultTask {
     private File jsonSchema;
     private File jsonSchema;

+ 1 - 1
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ValidateYamlAgainstSchemaTask.java

@@ -12,7 +12,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 
 
 /**
 /**
- * Incremental task to validate a set of YAML files against against a schema.
+ * Incremental task to validate a set of YAML files against a schema.
  */
  */
 public class ValidateYamlAgainstSchemaTask extends ValidateJsonAgainstSchemaTask {
 public class ValidateYamlAgainstSchemaTask extends ValidateJsonAgainstSchemaTask {
     @Override
     @Override

+ 27 - 103
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/BreakingChangesGenerator.java

@@ -11,7 +11,6 @@ package org.elasticsearch.gradle.internal.release;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.annotations.VisibleForTesting;
 
 
 import org.elasticsearch.gradle.VersionProperties;
 import org.elasticsearch.gradle.VersionProperties;
-import org.gradle.api.GradleException;
 
 
 import java.io.File;
 import java.io.File;
 import java.io.FileWriter;
 import java.io.FileWriter;
@@ -19,140 +18,65 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Files;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Objects;
-import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
 
 
 import static java.util.Comparator.comparing;
 import static java.util.Comparator.comparing;
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.toCollection;
+import static java.util.stream.Collectors.toList;
 
 
 /**
 /**
- * Generates the page that contains an index into the breaking changes and lists deprecations for a minor version release,
- * and the individual pages for each breaking area.
+ * Generates the page that contains breaking changes deprecations for a minor release series.
  */
  */
 public class BreakingChangesGenerator {
 public class BreakingChangesGenerator {
 
 
-    // Needs to match `changelog-schema.json`
-    private static final List<String> BREAKING_AREAS = List.of(
-        "Cluster and node setting",
-        "Command line tool",
-        "Index setting",
-        "JVM option",
-        "Java API",
-        "Logging",
-        "Mapping",
-        "Packaging",
-        "Painless",
-        "REST API",
-        "System requirement",
-        "Transform"
-    );
-
-    static void update(
-        File indexTemplateFile,
-        File indexOutputFile,
-        File outputDirectory,
-        File areaTemplateFile,
-        List<ChangelogEntry> entries
-    ) throws IOException {
-        if (outputDirectory.exists()) {
-            if (outputDirectory.isDirectory() == false) {
-                throw new GradleException("Path [" + outputDirectory + "] exists but isn't a directory!");
-            }
-        } else {
-            Files.createDirectory(outputDirectory.toPath());
-        }
-
-        try (FileWriter output = new FileWriter(indexOutputFile)) {
+    static void update(File migrationTemplateFile, File migrationOutputFile, List<ChangelogEntry> entries) throws IOException {
+        try (FileWriter output = new FileWriter(migrationOutputFile)) {
             output.write(
             output.write(
-                generateIndexFile(
+                generateMigrationFile(
                     QualifiedVersion.of(VersionProperties.getElasticsearch()),
                     QualifiedVersion.of(VersionProperties.getElasticsearch()),
-                    Files.readString(indexTemplateFile.toPath()),
+                    Files.readString(migrationTemplateFile.toPath()),
                     entries
                     entries
                 )
                 )
             );
             );
         }
         }
-
-        String areaTemplate = Files.readString(areaTemplateFile.toPath());
-
-        for (String breakingArea : BREAKING_AREAS) {
-            final List<ChangelogEntry.Breaking> entriesForArea = entries.stream()
-                .map(ChangelogEntry::getBreaking)
-                .filter(entry -> entry != null && breakingArea.equals(entry.getArea()))
-                .collect(Collectors.toList());
-
-            if (entriesForArea.isEmpty()) {
-                continue;
-            }
-
-            final String outputFilename = breakingArea.toLowerCase(Locale.ROOT).replaceFirst(" and", "").replaceAll(" ", "-")
-                + "-changes.asciidoc";
-
-            try (FileWriter output = new FileWriter(outputDirectory.toPath().resolve(outputFilename).toFile())) {
-                output.write(
-                    generateBreakingAreaFile(
-                        QualifiedVersion.of(VersionProperties.getElasticsearch()),
-                        areaTemplate,
-                        breakingArea,
-                        entriesForArea
-                    )
-                );
-            }
-        }
     }
     }
 
 
     @VisibleForTesting
     @VisibleForTesting
-    static String generateIndexFile(QualifiedVersion version, String template, List<ChangelogEntry> entries) throws IOException {
-        final Map<String, List<ChangelogEntry.Deprecation>> deprecationsByArea = entries.stream()
+    static String generateMigrationFile(QualifiedVersion version, String template, List<ChangelogEntry> entries) throws IOException {
+        final Map<Boolean, Map<String, List<ChangelogEntry.Deprecation>>> deprecationsByNotabilityByArea = entries.stream()
             .map(ChangelogEntry::getDeprecation)
             .map(ChangelogEntry::getDeprecation)
             .filter(Objects::nonNull)
             .filter(Objects::nonNull)
             .sorted(comparing(ChangelogEntry.Deprecation::getTitle))
             .sorted(comparing(ChangelogEntry.Deprecation::getTitle))
-            .collect(groupingBy(ChangelogEntry.Deprecation::getArea, TreeMap::new, Collectors.toList()));
-
-        final List<String> breakingIncludeList = entries.stream()
-            .filter(each -> each.getBreaking() != null)
-            .map(each -> each.getBreaking().getArea().toLowerCase(Locale.ROOT).replaceFirst(" and", "").replaceAll(" ", "-"))
-            .distinct()
-            .sorted()
-            .toList();
-
-        final Map<String, Object> bindings = new HashMap<>();
-        bindings.put("breakingIncludeList", breakingIncludeList);
-        bindings.put("deprecationsByArea", deprecationsByArea);
-        bindings.put("isElasticsearchSnapshot", version.isSnapshot());
-        bindings.put("majorDotMinor", version.major() + "." + version.minor());
-        bindings.put("majorMinor", String.valueOf(version.major()) + version.minor());
-        bindings.put("nextMajor", (version.major() + 1) + ".0");
-        bindings.put("version", version);
-
-        return TemplateUtils.render(template, bindings);
-    }
+            .collect(
+                groupingBy(
+                    ChangelogEntry.Deprecation::isNotable,
+                    TreeMap::new,
+                    groupingBy(ChangelogEntry.Deprecation::getArea, TreeMap::new, toList())
+                )
+            );
 
 
-    @VisibleForTesting
-    static String generateBreakingAreaFile(
-        QualifiedVersion version,
-        String template,
-        String breakingArea,
-        List<ChangelogEntry.Breaking> entriesForArea
-    ) throws IOException {
-        final Map<Boolean, Set<ChangelogEntry.Breaking>> breakingEntriesByNotability = entriesForArea.stream()
+        final Map<Boolean, Map<String, List<ChangelogEntry.Breaking>>> breakingByNotabilityByArea = entries.stream()
+            .map(ChangelogEntry::getBreaking)
+            .filter(Objects::nonNull)
+            .sorted(comparing(ChangelogEntry.Breaking::getTitle))
             .collect(
             .collect(
                 groupingBy(
                 groupingBy(
                     ChangelogEntry.Breaking::isNotable,
                     ChangelogEntry.Breaking::isNotable,
-                    toCollection(() -> new TreeSet<>(comparing(ChangelogEntry.Breaking::getTitle)))
+                    TreeMap::new,
+                    groupingBy(ChangelogEntry.Breaking::getArea, TreeMap::new, toList())
                 )
                 )
             );
             );
 
 
         final Map<String, Object> bindings = new HashMap<>();
         final Map<String, Object> bindings = new HashMap<>();
-        bindings.put("breakingArea", breakingArea);
-        bindings.put("breakingEntriesByNotability", breakingEntriesByNotability);
-        bindings.put("breakingAreaAnchor", breakingArea.toLowerCase(Locale.ROOT).replaceFirst(" and", "").replaceAll(" ", "_"));
+        bindings.put("breakingByNotabilityByArea", breakingByNotabilityByArea);
+        bindings.put("deprecationsByNotabilityByArea", deprecationsByNotabilityByArea);
+        bindings.put("isElasticsearchSnapshot", version.isSnapshot());
+        bindings.put("majorDotMinor", version.major() + "." + version.minor());
         bindings.put("majorMinor", String.valueOf(version.major()) + version.minor());
         bindings.put("majorMinor", String.valueOf(version.major()) + version.minor());
+        bindings.put("nextMajor", (version.major() + 1) + ".0");
+        bindings.put("version", version);
 
 
         return TemplateUtils.render(template, bindings);
         return TemplateUtils.render(template, bindings);
     }
     }

+ 21 - 66
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ChangelogEntry.java

@@ -40,6 +40,11 @@ public class ChangelogEntry {
 
 
     private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
     private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
 
 
+    /**
+     * Create a new instance by parsing the supplied file
+     * @param file the YAML file to parse
+     * @return a new instance
+     */
     public static ChangelogEntry parse(File file) {
     public static ChangelogEntry parse(File file) {
         try {
         try {
             return yamlMapper.readValue(file, ChangelogEntry.class);
             return yamlMapper.readValue(file, ChangelogEntry.class);
@@ -209,7 +214,11 @@ public class ChangelogEntry {
         }
         }
     }
     }
 
 
-    public static class Breaking {
+    public static class Breaking extends CompatibilityChange {}
+
+    public static class Deprecation extends CompatibilityChange {}
+
+    abstract static class CompatibilityChange {
         private String area;
         private String area;
         private String title;
         private String title;
         private String details;
         private String details;
@@ -277,13 +286,13 @@ public class ChangelogEntry {
             if (o == null || getClass() != o.getClass()) {
             if (o == null || getClass() != o.getClass()) {
                 return false;
                 return false;
             }
             }
-            Breaking breaking = (Breaking) o;
-            return notable == breaking.notable
-                && Objects.equals(area, breaking.area)
-                && Objects.equals(title, breaking.title)
-                && Objects.equals(details, breaking.details)
-                && Objects.equals(impact, breaking.impact)
-                && Objects.equals(essSettingChange, breaking.essSettingChange);
+            CompatibilityChange breaking = (CompatibilityChange) o;
+            return notable == breaking.isNotable()
+                && Objects.equals(area, breaking.getArea())
+                && Objects.equals(title, breaking.getTitle())
+                && Objects.equals(details, breaking.getDetails())
+                && Objects.equals(impact, breaking.getImpact())
+                && Objects.equals(essSettingChange, breaking.isEssSettingChange());
         }
         }
 
 
         @Override
         @Override
@@ -294,7 +303,8 @@ public class ChangelogEntry {
         @Override
         @Override
         public String toString() {
         public String toString() {
             return String.format(
             return String.format(
-                "Breaking{area='%s', title='%s', details='%s', impact='%s', notable=%s, essSettingChange=%s}",
+                "%s{area='%s', title='%s', details='%s', impact='%s', notable=%s, essSettingChange=%s}",
+                this.getClass().getSimpleName(),
                 area,
                 area,
                 title,
                 title,
                 details,
                 details,
@@ -305,66 +315,11 @@ public class ChangelogEntry {
         }
         }
     }
     }
 
 
-    public static class Deprecation {
-        private String area;
-        private String title;
-        private String body;
-
-        public String getArea() {
-            return area;
-        }
-
-        public void setArea(String area) {
-            this.area = area;
-        }
-
-        public String getTitle() {
-            return title;
-        }
-
-        public void setTitle(String title) {
-            this.title = title;
-        }
-
-        public String getBody() {
-            return body;
-        }
-
-        public void setBody(String body) {
-            this.body = body;
-        }
-
-        public String getAnchor() {
-            return generatedAnchor(this.title);
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) {
-                return true;
-            }
-            if (o == null || getClass() != o.getClass()) {
-                return false;
-            }
-            Deprecation that = (Deprecation) o;
-            return Objects.equals(area, that.area) && Objects.equals(title, that.title) && Objects.equals(body, that.body);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(area, title, body);
-        }
-
-        @Override
-        public String toString() {
-            return String.format("Deprecation{area='%s', title='%s', body='%s'}", area, title, body);
-        }
-    }
-
     private static String generatedAnchor(String input) {
     private static String generatedAnchor(String input) {
-        final List<String> excludes = List.of("the", "is", "a", "and");
+        final List<String> excludes = List.of("the", "is", "a", "and", "now", "that");
 
 
         final String[] words = input.toLowerCase(Locale.ROOT)
         final String[] words = input.toLowerCase(Locale.ROOT)
+            .replaceAll("'", "")
             .replaceAll("[^\\w]+", "_")
             .replaceAll("[^\\w]+", "_")
             .replaceFirst("^_+", "")
             .replaceFirst("^_+", "")
             .replaceFirst("_+$", "")
             .replaceFirst("_+$", "")

+ 13 - 39
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/GenerateReleaseNotesTask.java

@@ -14,8 +14,6 @@ import org.elasticsearch.gradle.VersionProperties;
 import org.gradle.api.DefaultTask;
 import org.gradle.api.DefaultTask;
 import org.gradle.api.GradleException;
 import org.gradle.api.GradleException;
 import org.gradle.api.file.ConfigurableFileCollection;
 import org.gradle.api.file.ConfigurableFileCollection;
-import org.gradle.api.file.Directory;
-import org.gradle.api.file.DirectoryProperty;
 import org.gradle.api.file.FileCollection;
 import org.gradle.api.file.FileCollection;
 import org.gradle.api.file.RegularFile;
 import org.gradle.api.file.RegularFile;
 import org.gradle.api.file.RegularFileProperty;
 import org.gradle.api.file.RegularFileProperty;
@@ -24,7 +22,6 @@ import org.gradle.api.logging.Logging;
 import org.gradle.api.model.ObjectFactory;
 import org.gradle.api.model.ObjectFactory;
 import org.gradle.api.tasks.InputFile;
 import org.gradle.api.tasks.InputFile;
 import org.gradle.api.tasks.InputFiles;
 import org.gradle.api.tasks.InputFiles;
-import org.gradle.api.tasks.OutputDirectory;
 import org.gradle.api.tasks.OutputFile;
 import org.gradle.api.tasks.OutputFile;
 import org.gradle.api.tasks.TaskAction;
 import org.gradle.api.tasks.TaskAction;
 import org.gradle.process.ExecOperations;
 import org.gradle.process.ExecOperations;
@@ -37,13 +34,13 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 
 
 import javax.inject.Inject;
 import javax.inject.Inject;
 
 
 import static java.util.Comparator.naturalOrder;
 import static java.util.Comparator.naturalOrder;
-import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 import static java.util.stream.Collectors.toSet;
 
 
 /**
 /**
@@ -58,13 +55,11 @@ public class GenerateReleaseNotesTask extends DefaultTask {
     private final RegularFileProperty releaseNotesTemplate;
     private final RegularFileProperty releaseNotesTemplate;
     private final RegularFileProperty releaseHighlightsTemplate;
     private final RegularFileProperty releaseHighlightsTemplate;
     private final RegularFileProperty breakingChangesTemplate;
     private final RegularFileProperty breakingChangesTemplate;
-    private final RegularFileProperty breakingChangesAreaTemplate;
 
 
     private final RegularFileProperty releaseNotesIndexFile;
     private final RegularFileProperty releaseNotesIndexFile;
     private final RegularFileProperty releaseNotesFile;
     private final RegularFileProperty releaseNotesFile;
     private final RegularFileProperty releaseHighlightsFile;
     private final RegularFileProperty releaseHighlightsFile;
-    private final RegularFileProperty breakingChangesIndexFile;
-    private final DirectoryProperty breakingChangesDirectory;
+    private final RegularFileProperty breakingChangesMigrationFile;
 
 
     private final GitWrapper gitWrapper;
     private final GitWrapper gitWrapper;
 
 
@@ -76,13 +71,11 @@ public class GenerateReleaseNotesTask extends DefaultTask {
         releaseNotesTemplate = objectFactory.fileProperty();
         releaseNotesTemplate = objectFactory.fileProperty();
         releaseHighlightsTemplate = objectFactory.fileProperty();
         releaseHighlightsTemplate = objectFactory.fileProperty();
         breakingChangesTemplate = objectFactory.fileProperty();
         breakingChangesTemplate = objectFactory.fileProperty();
-        breakingChangesAreaTemplate = objectFactory.fileProperty();
 
 
         releaseNotesIndexFile = objectFactory.fileProperty();
         releaseNotesIndexFile = objectFactory.fileProperty();
         releaseNotesFile = objectFactory.fileProperty();
         releaseNotesFile = objectFactory.fileProperty();
         releaseHighlightsFile = objectFactory.fileProperty();
         releaseHighlightsFile = objectFactory.fileProperty();
-        breakingChangesIndexFile = objectFactory.fileProperty();
-        breakingChangesDirectory = objectFactory.directoryProperty();
+        breakingChangesMigrationFile = objectFactory.fileProperty();
 
 
         gitWrapper = new GitWrapper(execOperations);
         gitWrapper = new GitWrapper(execOperations);
     }
     }
@@ -136,15 +129,13 @@ public class GenerateReleaseNotesTask extends DefaultTask {
         LOGGER.info("Generating breaking changes / deprecations notes...");
         LOGGER.info("Generating breaking changes / deprecations notes...");
         BreakingChangesGenerator.update(
         BreakingChangesGenerator.update(
             this.breakingChangesTemplate.get().getAsFile(),
             this.breakingChangesTemplate.get().getAsFile(),
-            this.breakingChangesIndexFile.get().getAsFile(),
-            this.breakingChangesDirectory.get().getAsFile(),
-            this.breakingChangesAreaTemplate.get().getAsFile(),
+            this.breakingChangesMigrationFile.get().getAsFile(),
             entries
             entries
         );
         );
     }
     }
 
 
     /**
     /**
-     * Find all tags in the `major.minor` series for the supplied version
+     * Find all tags in the major series for the supplied version
      * @param gitWrapper used to call `git`
      * @param gitWrapper used to call `git`
      * @param currentVersion the version to base the query upon
      * @param currentVersion the version to base the query upon
      * @return all versions in the series
      * @return all versions in the series
@@ -152,7 +143,8 @@ public class GenerateReleaseNotesTask extends DefaultTask {
     @VisibleForTesting
     @VisibleForTesting
     static Set<QualifiedVersion> getVersions(GitWrapper gitWrapper, String currentVersion) {
     static Set<QualifiedVersion> getVersions(GitWrapper gitWrapper, String currentVersion) {
         QualifiedVersion v = QualifiedVersion.of(currentVersion);
         QualifiedVersion v = QualifiedVersion.of(currentVersion);
-        Set<QualifiedVersion> versions = gitWrapper.listVersions("v" + v.major() + '.' + v.minor() + ".*").collect(toSet());
+        final String pattern = "v" + v.major() + ".*";
+        Set<QualifiedVersion> versions = gitWrapper.listVersions(pattern).collect(toSet());
         versions.add(v);
         versions.add(v);
         return versions;
         return versions;
     }
     }
@@ -183,13 +175,13 @@ public class GenerateReleaseNotesTask extends DefaultTask {
         QualifiedVersion currentVersion = QualifiedVersion.of(versionString);
         QualifiedVersion currentVersion = QualifiedVersion.of(versionString);
 
 
         // Find all tags for this minor series, using a wildcard tag pattern.
         // Find all tags for this minor series, using a wildcard tag pattern.
-        String tagWildcard = "v%d.%d*".formatted(currentVersion.major(), currentVersion.minor());
+        String tagWildcard = String.format(Locale.ROOT, "v%d.%d*", currentVersion.major(), currentVersion.minor());
 
 
         final List<QualifiedVersion> earlierVersions = gitWrapper.listVersions(tagWildcard)
         final List<QualifiedVersion> earlierVersions = gitWrapper.listVersions(tagWildcard)
             // Only keep earlier versions, and if `currentVersion` is a prerelease, then only prereleases too.
             // Only keep earlier versions, and if `currentVersion` is a prerelease, then only prereleases too.
             .filter(each -> each.isBefore(currentVersion) && (currentVersion.hasQualifier() == each.hasQualifier()))
             .filter(each -> each.isBefore(currentVersion) && (currentVersion.hasQualifier() == each.hasQualifier()))
             .sorted(naturalOrder())
             .sorted(naturalOrder())
-            .collect(toList());
+            .toList();
 
 
         if (earlierVersions.isEmpty()) {
         if (earlierVersions.isEmpty()) {
             throw new GradleException("Failed to find git tags prior to [v" + currentVersion + "]");
             throw new GradleException("Failed to find git tags prior to [v" + currentVersion + "]");
@@ -348,29 +340,11 @@ public class GenerateReleaseNotesTask extends DefaultTask {
     }
     }
 
 
     @OutputFile
     @OutputFile
-    public RegularFileProperty getBreakingChangesIndexFile() {
-        return breakingChangesIndexFile;
+    public RegularFileProperty getBreakingChangesMigrationFile() {
+        return breakingChangesMigrationFile;
     }
     }
 
 
-    public void setBreakingChangesIndexFile(RegularFile file) {
-        this.breakingChangesIndexFile.set(file);
-    }
-
-    public void setBreakingChangesDirectory(Directory breakingChangesDirectory) {
-        this.breakingChangesDirectory.set(breakingChangesDirectory);
-    }
-
-    @OutputDirectory
-    public DirectoryProperty getBreakingChangesDirectory() {
-        return breakingChangesDirectory;
-    }
-
-    @InputFile
-    public RegularFileProperty getBreakingChangesAreaTemplate() {
-        return breakingChangesAreaTemplate;
-    }
-
-    public void setBreakingChangesAreaTemplate(RegularFile file) {
-        this.breakingChangesAreaTemplate.set(file);
+    public void setBreakingChangesMigrationFile(RegularFile file) {
+        this.breakingChangesMigrationFile.set(file);
     }
     }
 }
 }

+ 15 - 31
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/PruneChangelogsTask.java

@@ -20,7 +20,6 @@ import org.gradle.api.logging.Logging;
 import org.gradle.api.model.ObjectFactory;
 import org.gradle.api.model.ObjectFactory;
 import org.gradle.api.tasks.Internal;
 import org.gradle.api.tasks.Internal;
 import org.gradle.api.tasks.TaskAction;
 import org.gradle.api.tasks.TaskAction;
-import org.gradle.api.tasks.options.Option;
 import org.gradle.process.ExecOperations;
 import org.gradle.process.ExecOperations;
 
 
 import java.io.File;
 import java.io.File;
@@ -47,8 +46,6 @@ public class PruneChangelogsTask extends DefaultTask {
     private final GitWrapper gitWrapper;
     private final GitWrapper gitWrapper;
     private final Path rootDir;
     private final Path rootDir;
 
 
-    private boolean dryRun;
-
     @Inject
     @Inject
     public PruneChangelogsTask(Project project, ObjectFactory objectFactory, ExecOperations execOperations) {
     public PruneChangelogsTask(Project project, ObjectFactory objectFactory, ExecOperations execOperations) {
         changelogs = objectFactory.fileCollection();
         changelogs = objectFactory.fileCollection();
@@ -65,16 +62,6 @@ public class PruneChangelogsTask extends DefaultTask {
         this.changelogs = files;
         this.changelogs = files;
     }
     }
 
 
-    @Internal
-    public boolean isDryRun() {
-        return dryRun;
-    }
-
-    @Option(option = "dry-run", description = "Find and print files to prune but don't actually delete them")
-    public void setDryRun(boolean dryRun) {
-        this.dryRun = dryRun;
-    }
-
     @TaskAction
     @TaskAction
     public void executeTask() {
     public void executeTask() {
         findAndDeleteFiles(
         findAndDeleteFiles(
@@ -82,7 +69,6 @@ public class PruneChangelogsTask extends DefaultTask {
             files -> files.stream().filter(each -> each.delete() == false).collect(Collectors.toSet()),
             files -> files.stream().filter(each -> each.delete() == false).collect(Collectors.toSet()),
             QualifiedVersion.of(VersionProperties.getElasticsearch()),
             QualifiedVersion.of(VersionProperties.getElasticsearch()),
             this.getChangelogs().getFiles(),
             this.getChangelogs().getFiles(),
-            this.dryRun,
             this.rootDir
             this.rootDir
         );
         );
     }
     }
@@ -93,7 +79,6 @@ public class PruneChangelogsTask extends DefaultTask {
         DeleteHelper deleteHelper,
         DeleteHelper deleteHelper,
         QualifiedVersion version,
         QualifiedVersion version,
         Set<File> allFilesInCheckout,
         Set<File> allFilesInCheckout,
-        boolean dryRun,
         Path rootDir
         Path rootDir
     ) {
     ) {
         if (allFilesInCheckout.isEmpty()) {
         if (allFilesInCheckout.isEmpty()) {
@@ -117,20 +102,18 @@ public class PruneChangelogsTask extends DefaultTask {
             return;
             return;
         }
         }
 
 
-        LOGGER.warn("The following changelog files {} be deleted:", dryRun ? "can" : "will");
+        LOGGER.warn("The following changelog files will be deleted:");
         LOGGER.warn("");
         LOGGER.warn("");
         filesToDelete.forEach(file -> LOGGER.warn("\t{}", rootDir.relativize(file.toPath())));
         filesToDelete.forEach(file -> LOGGER.warn("\t{}", rootDir.relativize(file.toPath())));
 
 
-        if (dryRun == false) {
-            final Set<File> failedToDelete = deleteHelper.deleteFiles(filesToDelete);
+        final Set<File> failedToDelete = deleteHelper.deleteFiles(filesToDelete);
 
 
-            if (failedToDelete.isEmpty() == false) {
-                throw new GradleException(
-                    "Failed to delete some files:\n\n"
-                        + failedToDelete.stream().map(file -> "\t" + rootDir.relativize(file.toPath())).collect(Collectors.joining("\n"))
-                        + "\n"
-                );
-            }
+        if (failedToDelete.isEmpty() == false) {
+            throw new GradleException(
+                "Failed to delete some files:\n\n"
+                    + failedToDelete.stream().map(file -> "\t" + rootDir.relativize(file.toPath())).collect(Collectors.joining("\n"))
+                    + "\n"
+            );
         }
         }
     }
     }
 
 
@@ -152,9 +135,9 @@ public class PruneChangelogsTask extends DefaultTask {
     }
     }
 
 
     /**
     /**
-     * Find the releases prior to the supplied version. If the supplied version is the very first in a new
-     * major series, then the method will look tag in the previous major series. Otherwise, all git tags
-     * in the current major series will be inspected.
+     * Find the releases prior to the supplied version. The current major and the previous major are both
+     * listed, since changes may be backported to the prior major e.g. in the event of a series bug
+     * or security problem.
      *
      *
      * @param gitWrapper used for git operations
      * @param gitWrapper used for git operations
      * @param version the git tags are inspected relative to this version
      * @param version the git tags are inspected relative to this version
@@ -162,10 +145,11 @@ public class PruneChangelogsTask extends DefaultTask {
      */
      */
     @VisibleForTesting
     @VisibleForTesting
     static Stream<QualifiedVersion> findPreviousVersion(GitWrapper gitWrapper, QualifiedVersion version) {
     static Stream<QualifiedVersion> findPreviousVersion(GitWrapper gitWrapper, QualifiedVersion version) {
-        final int majorSeries = version.minor() == 0 && version.revision() == 0 ? version.major() - 1 : version.major();
-        final String tagPattern = "v" + majorSeries + ".*";
+        final String currentMajorPattern = "v" + version.major() + ".*";
+        final String previousMajorPattern = "v" + (version.major() - 1) + ".*";
 
 
-        return gitWrapper.listVersions(tagPattern).filter(v -> v.isBefore(version));
+        return Stream.concat(gitWrapper.listVersions(currentMajorPattern), gitWrapper.listVersions(previousMajorPattern))
+            .filter(v -> v.isBefore(version));
     }
     }
 
 
     /**
     /**

+ 2 - 6
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ReleaseHighlightsGenerator.java

@@ -41,13 +41,9 @@ public class ReleaseHighlightsGenerator {
 
 
         if (version.minor() > 0) {
         if (version.minor() > 0) {
             final int major = version.major();
             final int major = version.major();
-            for (int minor = version.minor(); minor >= 0; minor--) {
+            for (int minor = version.minor() - 1; minor >= 0; minor--) {
                 String majorMinor = major + "." + minor;
                 String majorMinor = major + "." + minor;
-                String fileSuffix = "";
-                if (major == 7 && minor < 7) {
-                    fileSuffix = "-" + majorMinor + ".0";
-                }
-                priorVersions.add("{ref-bare}/" + majorMinor + "/release-highlights" + fileSuffix + ".html[" + majorMinor + "]");
+                priorVersions.add("{ref-bare}/" + majorMinor + "/release-highlights.html[" + majorMinor + "]");
             }
             }
         }
         }
 
 

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

@@ -84,15 +84,11 @@ public class ReleaseToolsPlugin implements Plugin<Project> {
             task.setReleaseHighlightsFile(projectDirectory.file("docs/reference/release-notes/highlights.asciidoc"));
             task.setReleaseHighlightsFile(projectDirectory.file("docs/reference/release-notes/highlights.asciidoc"));
 
 
             task.setBreakingChangesTemplate(projectDirectory.file(RESOURCES + "templates/breaking-changes.asciidoc"));
             task.setBreakingChangesTemplate(projectDirectory.file(RESOURCES + "templates/breaking-changes.asciidoc"));
-            task.setBreakingChangesIndexFile(
+            task.setBreakingChangesMigrationFile(
                 projectDirectory.file(
                 projectDirectory.file(
                     String.format("docs/reference/migration/migrate_%d_%d.asciidoc", version.getMajor(), version.getMinor())
                     String.format("docs/reference/migration/migrate_%d_%d.asciidoc", version.getMajor(), version.getMinor())
                 )
                 )
             );
             );
-            task.setBreakingChangesAreaTemplate(projectDirectory.file(RESOURCES + "templates/breaking-changes-area.asciidoc"));
-            task.setBreakingChangesDirectory(
-                projectDirectory.dir(String.format("docs/reference/migration/migrate_%d_%d", version.getMajor(), version.getMinor()))
-            );
 
 
             task.dependsOn(validateChangelogsTask);
             task.dependsOn(validateChangelogsTask);
         });
         });

+ 7 - 30
build-tools-internal/src/main/resources/changelog-schema.json

@@ -78,8 +78,8 @@
             "Stats",
             "Stats",
             "Store",
             "Store",
             "Suggesters",
             "Suggesters",
-            "TLS",
             "Task Management",
             "Task Management",
+            "TLS",
             "Transform",
             "Transform",
             "TSDB",
             "TSDB",
             "Watcher"
             "Watcher"
@@ -109,10 +109,10 @@
           "$ref": "#/definitions/Highlight"
           "$ref": "#/definitions/Highlight"
         },
         },
         "breaking": {
         "breaking": {
-          "$ref": "#/definitions/Breaking"
+          "$ref": "#/definitions/CompatibilityChange"
         },
         },
         "deprecation": {
         "deprecation": {
-          "$ref": "#/definitions/Deprecation"
+          "$ref": "#/definitions/CompatibilityChange"
         }
         }
       },
       },
       "required": [
       "required": [
@@ -140,10 +140,10 @@
       ],
       ],
       "additionalProperties": false
       "additionalProperties": false
     },
     },
-    "Breaking": {
+    "CompatibilityChange": {
       "properties": {
       "properties": {
         "area": {
         "area": {
-          "$ref": "#/definitions/breakingArea"
+          "$ref": "#/definitions/compatibilityChangeArea"
         },
         },
         "title": {
         "title": {
           "type": "string",
           "type": "string",
@@ -172,35 +172,12 @@
       ],
       ],
       "additionalProperties": false
       "additionalProperties": false
     },
     },
-    "Deprecation": {
-      "properties": {
-        "area": {
-          "$ref": "#/definitions/breakingArea"
-        },
-        "title": {
-          "type": "string",
-          "minLength": 1
-        },
-        "body": {
-          "type": "string",
-          "minLength": 1
-        },
-        "ess_setting_change": {
-          "type": "boolean"
-        }
-      },
-      "required": [
-        "area",
-        "title",
-        "body"
-      ],
-      "additionalProperties": false
-    },
-    "breakingArea": {
+    "compatibilityChangeArea": {
       "type": "string",
       "type": "string",
       "enum": [
       "enum": [
         "Cluster and node setting",
         "Cluster and node setting",
         "Command line tool",
         "Command line tool",
+        "CRUD",
         "Index setting",
         "Index setting",
         "JVM option",
         "JVM option",
         "Java API",
         "Java API",

+ 0 - 39
build-tools-internal/src/main/resources/templates/breaking-changes-area.asciidoc

@@ -1,39 +0,0 @@
-[discrete]
-[[breaking_${majorMinor}_${breakingAreaAnchor}]]
-==== ${breakingArea}
-
-//NOTE: The notable-breaking-changes tagged regions are re-used in the
-//Installation and Upgrade Guide
-
-TIP: {ess-setting-change}
-
-<%
-[true, false].each { isNotable ->
-    def breakingChanges = breakingEntriesByNotability.getOrDefault(isNotable, [])
-
-    if (breakingChanges.isEmpty() == false) {
-        if (isNotable) {
-            /* No newline here, one will be added below */
-            print "// tag::notable-breaking-changes[]"
-        }
-
-        for (breaking in breakingChanges) { %>
-[[${ breaking.anchor }]]
-. ${breaking.title}${ breaking.essSettingChange ? ' {ess-icon}' : '' }
-[%collapsible]
-====
-*Details* +
-${breaking.details.trim()}
-
-*Impact* +
-${breaking.impact.trim()}
-====
-<%
-        }
-
-        if (isNotable) {
-            print "// end::notable-breaking-changes[]\n"
-        }
-    }
-}
-%>

+ 63 - 18
build-tools-internal/src/main/resources/templates/breaking-changes.asciidoc

@@ -11,16 +11,14 @@ See also <<release-highlights>> and <<es-release-notes>>.
 <% if (isElasticsearchSnapshot) { %>
 <% if (isElasticsearchSnapshot) { %>
 coming::[${version}]
 coming::[${version}]
 <% } %>
 <% } %>
-//NOTE: The notable-breaking-changes tagged regions are re-used in the
-//Installation and Upgrade Guide
-<% if (breakingIncludeList.isEmpty() == false) { %>
+<% if (breakingByNotabilityByArea.isEmpty() == false) { %>
 [discrete]
 [discrete]
 [[breaking-changes-${majorDotMinor}]]
 [[breaking-changes-${majorDotMinor}]]
 === Breaking changes
 === Breaking changes
 
 
 The following changes in {es} ${majorDotMinor} might affect your applications
 The following changes in {es} ${majorDotMinor} might affect your applications
 and prevent them from operating normally.
 and prevent them from operating normally.
-Before upgrading to ${majorDotMinor} review these changes and take the described steps
+Before upgrading to ${majorDotMinor}, review these changes and take the described steps
 to mitigate the impact.
 to mitigate the impact.
 
 
 NOTE: Breaking changes introduced in minor versions are
 NOTE: Breaking changes introduced in minor versions are
@@ -31,20 +29,51 @@ To find out if you are using any deprecated functionality,
 enable <<deprecation-logging, deprecation logging>>.
 enable <<deprecation-logging, deprecation logging>>.
 
 
 <%
 <%
-  for (include in breakingIncludeList) {
-      print "include::migrate_${version.major}_${version.minor}/${include}.asciidoc[]\n";
-  }
+    [true, false].each { isNotable ->
+        def breakingByArea = breakingByNotabilityByArea.getOrDefault(isNotable, [])
+        if (breakingByArea.isEmpty() == false) {
+            if (isNotable) {
+                /* No newline here, one will be added below */
+                print "// NOTE: The notable-breaking-changes tagged regions are re-used in the\n"
+                print "// Installation and Upgrade Guide\n"
+                print "// tag::notable-breaking-changes[]"
+            }
 
 
+            breakingByArea.eachWithIndex { area, breakingChanges, i ->
+                print "\n[discrete]\n"
+                print "[[breaking_${majorMinor}_${ area.toLowerCase().replaceAll("[^a-z0-9]+", "_") }_changes]]\n"
+                print "==== ${area} changes\n"
+
+                for (breaking in breakingChanges) { %>
+[[${ breaking.anchor }]]
+.${breaking.title}
+[%collapsible]
+====
+*Details* +
+${breaking.details.trim()}
+
+*Impact* +
+${breaking.impact.trim()}
+====
+<%
+                }
+            }
+
+            if (isNotable) {
+                print "// end::notable-breaking-changes[]\n"
+            }
+        }
+    }
 }
 }
 
 
-if (deprecationsByArea.empty == false) { %>
+if (deprecationsByNotabilityByArea.isEmpty() == false) { %>
 
 
 [discrete]
 [discrete]
 [[deprecated-${majorDotMinor}]]
 [[deprecated-${majorDotMinor}]]
 === Deprecations
 === Deprecations
 
 
 The following functionality has been deprecated in {es} ${majorDotMinor}
 The following functionality has been deprecated in {es} ${majorDotMinor}
-and will be removed in ${nextMajor}.
+and will be removed in a future version.
 While this won't have an immediate impact on your applications,
 While this won't have an immediate impact on your applications,
 we strongly encourage you take the described steps to update your code
 we strongly encourage you take the described steps to update your code
 after upgrading to ${majorDotMinor}.
 after upgrading to ${majorDotMinor}.
@@ -52,24 +81,40 @@ after upgrading to ${majorDotMinor}.
 NOTE: Significant changes in behavior are deprecated in a minor release and
 NOTE: Significant changes in behavior are deprecated in a minor release and
 the old behavior is supported until the next major release.
 the old behavior is supported until the next major release.
 To find out if you are using any deprecated functionality,
 To find out if you are using any deprecated functionality,
-enable <<deprecation-logging, deprecation logging>>."
+enable <<deprecation-logging, deprecation logging>>.
 
 
 <%
 <%
-deprecationsByArea.eachWithIndex { area, deprecations, i ->
-    print "\n[discrete]\n"
-    print "[[deprecations_${majorMinor}_${ area.toLowerCase().replaceAll("[^a-z0-9]+", "_") }]]"
-    print "==== ${area} deprecations"
+    [true, false].each { isNotable ->
+        def deprecationsByArea = deprecationsByNotabilityByArea.getOrDefault(isNotable, [])
+        if (deprecationsByArea.isEmpty() == false) {
+            if (isNotable) {
+                /* No newline here, one will be added below */
+                print "// tag::notable-breaking-changes[]"
+            }
 
 
-    for (deprecation in deprecations) { %>
+            deprecationsByArea.eachWithIndex { area, deprecations, i ->
+                print "\n[discrete]\n"
+                print "[[deprecations_${majorMinor}_${ area.toLowerCase().replaceAll("[^a-z0-9]+", "_") }]]\n"
+                print "==== ${area} deprecations\n"
 
 
+                for (deprecation in deprecations) { %>
 [[${ deprecation.anchor }]]
 [[${ deprecation.anchor }]]
 .${deprecation.title}
 .${deprecation.title}
 [%collapsible]
 [%collapsible]
 ====
 ====
 *Details* +
 *Details* +
-${deprecation.body.trim()}
+${deprecation.details.trim()}
+
+*Impact* +
+${deprecation.impact.trim()}
 ====
 ====
 <%
 <%
-}
-}
+                }
+            }
+
+            if (isNotable) {
+                print "// end::notable-breaking-changes[]\n"
+            }
+        }
+    }
 } %>
 } %>

+ 11 - 1
build-tools-internal/src/main/resources/templates/release-highlights.asciidoc

@@ -17,7 +17,17 @@ print priorVersions.join("\n| ")
 print "\n"
 print "\n"
 }
 }
 
 
-if (notableHighlights.empty == false) { %>
+/* The `notable-highlights` tag needs to exist, whether or not we actually have any notable highlights. */
+if (notableHighlights.isEmpty()) { %>
+// The notable-highlights tag marks entries that
+// should be featured in the Stack Installation and Upgrade Guide:
+// tag::notable-highlights[]
+// [discrete]
+// === Heading
+//
+// Description.
+// end::notable-highlights[]
+<% } else { %>
 // tag::notable-highlights[]
 // tag::notable-highlights[]
 <% for (highlight in notableHighlights) { %>
 <% for (highlight in notableHighlights) { %>
 [discrete]
 [discrete]

+ 36 - 28
build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.java

@@ -29,40 +29,15 @@ public class BreakingChangesGeneratorTest {
         // given:
         // given:
         final String template = getResource("/templates/breaking-changes.asciidoc");
         final String template = getResource("/templates/breaking-changes.asciidoc");
         final String expectedOutput = getResource(
         final String expectedOutput = getResource(
-            "/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.generateIndexFile.asciidoc"
+            "/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.generateMigrationFile.asciidoc"
         );
         );
 
 
         final List<ChangelogEntry> entries = getEntries();
         final List<ChangelogEntry> entries = getEntries();
 
 
         // when:
         // when:
-        final String actualOutput = BreakingChangesGenerator.generateIndexFile(QualifiedVersion.of("8.4.0-SNAPSHOT"), template, entries);
-
-        // then:
-        assertThat(actualOutput, equalTo(expectedOutput));
-    }
-
-    /**
-     * Check that the breaking changes for a specific area can be correctly generated.
-     */
-    @Test
-    public void generateAreaFile_rendersCorrectMarkup() throws Exception {
-        // given:
-        final String template = getResource("/templates/breaking-changes-area.asciidoc");
-        final String expectedOutput = getResource(
-            "/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.generateAreaFile.asciidoc"
-        );
-        final String breakingArea = "Cluster and node setting";
-
-        final List<ChangelogEntry.Breaking> entries = getEntries().stream()
-            .map(ChangelogEntry::getBreaking)
-            .filter(each -> each.getArea().equals(breakingArea))
-            .toList();
-
-        // when:
-        final String actualOutput = BreakingChangesGenerator.generateBreakingAreaFile(
+        final String actualOutput = BreakingChangesGenerator.generateMigrationFile(
             QualifiedVersion.of("8.4.0-SNAPSHOT"),
             QualifiedVersion.of("8.4.0-SNAPSHOT"),
             template,
             template,
-            breakingArea,
             entries
             entries
         );
         );
 
 
@@ -112,7 +87,40 @@ public class BreakingChangesGeneratorTest {
         breaking4.setImpact("Breaking change impact description 4");
         breaking4.setImpact("Breaking change impact description 4");
         breaking4.setEssSettingChange(true);
         breaking4.setEssSettingChange(true);
 
 
-        return List.of(entry1, entry2, entry3, entry4);
+        ChangelogEntry entry5 = new ChangelogEntry();
+        ChangelogEntry.Deprecation deprecation5 = new ChangelogEntry.Deprecation();
+        entry5.setDeprecation(deprecation5);
+
+        deprecation5.setNotable(true);
+        deprecation5.setTitle("Deprecation change number 5");
+        deprecation5.setArea("Cluster and node setting");
+        deprecation5.setDetails("Deprecation change details 5");
+        deprecation5.setImpact("Deprecation change impact description 5");
+        deprecation5.setEssSettingChange(false);
+
+        ChangelogEntry entry6 = new ChangelogEntry();
+        ChangelogEntry.Deprecation deprecation6 = new ChangelogEntry.Deprecation();
+        entry6.setDeprecation(deprecation6);
+
+        deprecation6.setNotable(true);
+        deprecation6.setTitle("Deprecation change number 6");
+        deprecation6.setArea("Cluster and node setting");
+        deprecation6.setDetails("Deprecation change details 6");
+        deprecation6.setImpact("Deprecation change impact description 6");
+        deprecation6.setEssSettingChange(false);
+
+        ChangelogEntry entry7 = new ChangelogEntry();
+        ChangelogEntry.Deprecation deprecation7 = new ChangelogEntry.Deprecation();
+        entry7.setDeprecation(deprecation7);
+
+        deprecation7.setNotable(false);
+        deprecation7.setTitle("Deprecation change number 7");
+        deprecation7.setArea("Cluster and node setting");
+        deprecation7.setDetails("Deprecation change details 7");
+        deprecation7.setImpact("Deprecation change impact description 7");
+        deprecation7.setEssSettingChange(false);
+
+        return List.of(entry1, entry2, entry3, entry4, entry5, entry6, entry7);
     }
     }
 
 
     private String getResource(String name) throws Exception {
     private String getResource(String name) throws Exception {

+ 6 - 5
build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/GenerateReleaseNotesTaskTest.java

@@ -15,7 +15,6 @@ import org.junit.Test;
 import java.io.File;
 import java.io.File;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.Stream;
 
 
 import static org.hamcrest.Matchers.aMapWithSize;
 import static org.hamcrest.Matchers.aMapWithSize;
@@ -154,12 +153,12 @@ public class GenerateReleaseNotesTaskTest extends GradleUnitTestCase {
     public void getVersions_includesCurrentVersion() {
     public void getVersions_includesCurrentVersion() {
         // given:
         // given:
         when(gitWrapper.listVersions(anyString())).thenReturn(
         when(gitWrapper.listVersions(anyString())).thenReturn(
-            Stream.of("8.0.0-alpha1", "8.0.0-alpha2", "8.0.0-beta1", "8.0.0-beta2", "8.0.0-beta3", "8.0.0-rc1", "8.0.0")
+            Stream.of("8.0.0-alpha1", "8.0.0-alpha2", "8.0.0-beta1", "8.0.0-beta2", "8.0.0-beta3", "8.0.0-rc1", "8.0.0", "8.0.1", "8.1.0")
                 .map(QualifiedVersion::of)
                 .map(QualifiedVersion::of)
         );
         );
 
 
         // when:
         // when:
-        Set<QualifiedVersion> versions = GenerateReleaseNotesTask.getVersions(gitWrapper, "8.0.0-SNAPSHOT");
+        Set<QualifiedVersion> versions = GenerateReleaseNotesTask.getVersions(gitWrapper, "8.2.0-SNAPSHOT");
 
 
         // then:
         // then:
         assertThat(
         assertThat(
@@ -173,8 +172,10 @@ public class GenerateReleaseNotesTaskTest extends GradleUnitTestCase {
                     "8.0.0-beta3",
                     "8.0.0-beta3",
                     "8.0.0-rc1",
                     "8.0.0-rc1",
                     "8.0.0",
                     "8.0.0",
-                    "8.0.0-SNAPSHOT"
-                ).map(QualifiedVersion::of).collect(Collectors.toList()).toArray(new QualifiedVersion[] {})
+                    "8.0.1",
+                    "8.1.0",
+                    "8.2.0-SNAPSHOT"
+                ).map(QualifiedVersion::of).toArray(QualifiedVersion[]::new)
             )
             )
         );
         );
     }
     }

+ 11 - 65
build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/PruneChangelogsTaskTests.java

@@ -55,7 +55,7 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
     @Test
     @Test
     public void findAndDeleteFiles_withNoFiles_doesNothing() {
     public void findAndDeleteFiles_withNoFiles_doesNothing() {
         // when:
         // when:
-        findAndDeleteFiles(gitWrapper, deleteHelper, null, Set.of(), false, Path.of("rootDir"));
+        findAndDeleteFiles(gitWrapper, deleteHelper, null, Set.of(), Path.of("rootDir"));
 
 
         // then:
         // then:
         verify(gitWrapper, never()).listVersions(anyString());
         verify(gitWrapper, never()).listVersions(anyString());
@@ -68,7 +68,7 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
     @Test
     @Test
     public void findAndDeleteFiles_withNoPriorVersions_deletesNothing() {
     public void findAndDeleteFiles_withNoPriorVersions_deletesNothing() {
         // given:
         // given:
-        when(gitWrapper.listVersions(anyString())).thenReturn(Stream.of());
+        when(gitWrapper.listVersions(anyString())).thenAnswer(args -> Stream.of());
 
 
         // when:
         // when:
         findAndDeleteFiles(
         findAndDeleteFiles(
@@ -76,12 +76,10 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
             deleteHelper,
             deleteHelper,
             QualifiedVersion.of("7.16.0"),
             QualifiedVersion.of("7.16.0"),
             Set.of(new File("rootDir/docs/changelog/1234.yml")),
             Set.of(new File("rootDir/docs/changelog/1234.yml")),
-            false,
             Path.of("rootDir")
             Path.of("rootDir")
         );
         );
 
 
         // then:
         // then:
-        verify(gitWrapper).listVersions("v7.*");
         verify(gitWrapper, never()).listFiles(anyString(), anyString());
         verify(gitWrapper, never()).listFiles(anyString(), anyString());
     }
     }
 
 
@@ -92,7 +90,8 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
     @Test
     @Test
     public void findAndDeleteFiles_withFilesButNoHistoricalFiles_deletesNothing() {
     public void findAndDeleteFiles_withFilesButNoHistoricalFiles_deletesNothing() {
         // given:
         // given:
-        when(gitWrapper.listVersions(anyString())).thenReturn(Stream.of(QualifiedVersion.of("7.14.0"), QualifiedVersion.of("7.15.0")));
+        when(gitWrapper.listVersions("v6.*")).thenReturn(Stream.of(QualifiedVersion.of("6.14.0"), QualifiedVersion.of("6.15.0")));
+        when(gitWrapper.listVersions("v7.*")).thenReturn(Stream.of(QualifiedVersion.of("7.14.0"), QualifiedVersion.of("7.15.0")));
         when(gitWrapper.listFiles(anyString(), anyString())).thenAnswer(args -> Stream.of());
         when(gitWrapper.listFiles(anyString(), anyString())).thenAnswer(args -> Stream.of());
 
 
         // when:
         // when:
@@ -101,45 +100,11 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
             deleteHelper,
             deleteHelper,
             QualifiedVersion.of("7.16.0"),
             QualifiedVersion.of("7.16.0"),
             Set.of(new File("rootDir/docs/changelog/1234.yml")),
             Set.of(new File("rootDir/docs/changelog/1234.yml")),
-            false,
             Path.of("rootDir")
             Path.of("rootDir")
         );
         );
 
 
         // then:
         // then:
-        verify(gitWrapper).listVersions("v7.*");
-        verify(gitWrapper, times(2)).listFiles(anyString(), anyString());
-    }
-
-    /**
-     * Check that if there are files to delete, but the user has supplied the `--dry-run` CLI option,
-     * then no files are deleted.
-     */
-    @Test
-    public void findAndDeleteFiles_withFilesToDelete_respectDryRun() {
-        // given:
-        when(gitWrapper.listVersions(anyString())).thenReturn(Stream.of(QualifiedVersion.of("7.14.0"), QualifiedVersion.of("7.15.0")));
-        when(gitWrapper.listFiles(anyString(), anyString())).thenAnswer(
-            args -> Stream.of("docs/changelog/1234.yml", "docs/changelog/5678.yml")
-        );
-
-        // when:
-        findAndDeleteFiles(
-            gitWrapper,
-            deleteHelper,
-            QualifiedVersion.of("7.16.0"),
-            Set.of(
-                new File("rootDir/docs/changelog/1234.yml"),
-                new File("rootDir/docs/changelog/5678.yml"),
-                new File("rootDir/docs/changelog/9123.yml")
-            ),
-            true,
-            Path.of("rootDir")
-        );
-
-        // then:
-        verify(gitWrapper).listVersions("v7.*");
-        verify(gitWrapper, times(2)).listFiles(anyString(), anyString());
-        verify(deleteHelper, never()).deleteFiles(any());
+        verify(gitWrapper, times(4)).listFiles(anyString(), anyString());
     }
     }
 
 
     /**
     /**
@@ -148,7 +113,8 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
     @Test
     @Test
     public void findAndDeleteFiles_withFilesToDelete_deletesFiles() {
     public void findAndDeleteFiles_withFilesToDelete_deletesFiles() {
         // given:
         // given:
-        when(gitWrapper.listVersions(anyString())).thenReturn(Stream.of(QualifiedVersion.of("7.14.0"), QualifiedVersion.of("7.15.0")));
+        when(gitWrapper.listVersions("v6.*")).thenReturn(Stream.of(QualifiedVersion.of("6.14.0"), QualifiedVersion.of("6.15.0")));
+        when(gitWrapper.listVersions("v7.*")).thenReturn(Stream.of(QualifiedVersion.of("7.14.0"), QualifiedVersion.of("7.15.0")));
         when(gitWrapper.listFiles(anyString(), anyString())).thenAnswer(
         when(gitWrapper.listFiles(anyString(), anyString())).thenAnswer(
             args -> Stream.of("docs/changelog/1234.yml", "docs/changelog/5678.yml")
             args -> Stream.of("docs/changelog/1234.yml", "docs/changelog/5678.yml")
         );
         );
@@ -163,13 +129,11 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
                 new File("rootDir/docs/changelog/5678.yml"),
                 new File("rootDir/docs/changelog/5678.yml"),
                 new File("rootDir/docs/changelog/9123.yml")
                 new File("rootDir/docs/changelog/9123.yml")
             ),
             ),
-            false,
             Path.of("rootDir")
             Path.of("rootDir")
         );
         );
 
 
         // then:
         // then:
-        verify(gitWrapper).listVersions("v7.*");
-        verify(gitWrapper, times(2)).listFiles(anyString(), anyString());
+        verify(gitWrapper, times(4)).listFiles(anyString(), anyString());
         verify(deleteHelper).deleteFiles(Set.of(new File("rootDir/docs/changelog/1234.yml"), new File("rootDir/docs/changelog/5678.yml")));
         verify(deleteHelper).deleteFiles(Set.of(new File("rootDir/docs/changelog/1234.yml"), new File("rootDir/docs/changelog/5678.yml")));
     }
     }
 
 
@@ -179,7 +143,8 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
     @Test
     @Test
     public void findAndDeleteFiles_withFilesToDeleteButDeleteFails_throwsException() {
     public void findAndDeleteFiles_withFilesToDeleteButDeleteFails_throwsException() {
         // given:
         // given:
-        when(gitWrapper.listVersions(anyString())).thenReturn(Stream.of(QualifiedVersion.of("7.14.0"), QualifiedVersion.of("7.15.0")));
+        when(gitWrapper.listVersions("v6.*")).thenReturn(Stream.of(QualifiedVersion.of("6.14.0"), QualifiedVersion.of("6.15.0")));
+        when(gitWrapper.listVersions("v7.*")).thenReturn(Stream.of(QualifiedVersion.of("7.14.0"), QualifiedVersion.of("7.15.0")));
         when(gitWrapper.listFiles(anyString(), anyString())).thenAnswer(
         when(gitWrapper.listFiles(anyString(), anyString())).thenAnswer(
             args -> Stream.of("docs/changelog/1234.yml", "docs/changelog/5678.yml")
             args -> Stream.of("docs/changelog/1234.yml", "docs/changelog/5678.yml")
         );
         );
@@ -198,7 +163,6 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
                     new File("rootDir/docs/changelog/5678.yml"),
                     new File("rootDir/docs/changelog/5678.yml"),
                     new File("rootDir/docs/changelog/9123.yml")
                     new File("rootDir/docs/changelog/9123.yml")
                 ),
                 ),
-                false,
                 Path.of("rootDir")
                 Path.of("rootDir")
             )
             )
         );
         );
@@ -208,25 +172,6 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
         assertThat(e.getMessage(), equalTo("Failed to delete some files:\n\n\t" + failedPath + "\n"));
         assertThat(e.getMessage(), equalTo("Failed to delete some files:\n\n\t" + failedPath + "\n"));
     }
     }
 
 
-    /**
-     * Check that when list versions and the current version is at the start of a major series, then the versions for
-     * the previous major series are returned.
-     */
-    @Test
-    public void findPreviousVersion_withStartOfMajorSeries_inspectsPriorMajorSeries() {
-        // given:
-        when(gitWrapper.listVersions("v7.*")).thenReturn(
-            Stream.of(QualifiedVersion.of("v7.0.0"), QualifiedVersion.of("v7.0.1"), QualifiedVersion.of("v7.1.0"))
-        );
-
-        // when:
-        final QualifiedVersion version = QualifiedVersion.of("8.0.0-SNAPSHOT");
-        final List<QualifiedVersion> versions = findPreviousVersion(gitWrapper, version).collect(Collectors.toList());
-
-        // then:
-        assertThat(versions, contains(QualifiedVersion.of("v7.0.0"), QualifiedVersion.of("v7.0.1"), QualifiedVersion.of("v7.1.0")));
-    }
-
     /**
     /**
      * Check that when list versions and the current version is at the start of a major series, then the versions for
      * Check that when list versions and the current version is at the start of a major series, then the versions for
      * the previous major series are returned.
      * the previous major series are returned.
@@ -234,6 +179,7 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
     @Test
     @Test
     public void findPreviousVersion_afterStartOfMajorSeries_inspectsCurrentMajorSeries() {
     public void findPreviousVersion_afterStartOfMajorSeries_inspectsCurrentMajorSeries() {
         // given:
         // given:
+        when(gitWrapper.listVersions("v6.*")).thenReturn(Stream.of());
         when(gitWrapper.listVersions("v7.*")).thenReturn(
         when(gitWrapper.listVersions("v7.*")).thenReturn(
             Stream.of(
             Stream.of(
                 QualifiedVersion.of("v7.0.0"),
                 QualifiedVersion.of("v7.0.0"),

+ 18 - 0
build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/ReleaseHighlightsGeneratorTest.java

@@ -21,6 +21,24 @@ import static org.junit.Assert.assertThat;
 
 
 public class ReleaseHighlightsGeneratorTest {
 public class ReleaseHighlightsGeneratorTest {
 
 
+    /**
+     * Check that the release highlights can be correctly generated when there are no highlights.
+     */
+    @Test
+    public void generateFile_withNoHighlights_rendersCorrectMarkup() throws Exception {
+        // given:
+        final String template = getResource("/templates/release-highlights.asciidoc");
+        final String expectedOutput = getResource(
+            "/org/elasticsearch/gradle/internal/release/ReleaseHighlightsGeneratorTest.noHighlights.generateFile.asciidoc"
+        );
+
+        // when:
+        final String actualOutput = ReleaseHighlightsGenerator.generateFile(QualifiedVersion.of("8.4.0-SNAPSHOT"), template, List.of());
+
+        // then:
+        assertThat(actualOutput, equalTo(expectedOutput));
+    }
+
     /**
     /**
      * Check that the release highlights can be correctly generated.
      * Check that the release highlights can be correctly generated.
      */
      */

+ 0 - 33
build-tools-internal/src/test/resources/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.generateAreaFile.asciidoc

@@ -1,33 +0,0 @@
-[discrete]
-[[breaking_84_cluster_node_setting]]
-==== Cluster and node setting
-
-//NOTE: The notable-breaking-changes tagged regions are re-used in the
-//Installation and Upgrade Guide
-
-TIP: {ess-setting-change}
-
-// tag::notable-breaking-changes[]
-[[breaking_change_number_2]]
-. Breaking change number 2
-[%collapsible]
-====
-*Details* +
-Breaking change details 2
-
-*Impact* +
-Breaking change impact description 2
-====
-
-[[breaking_change_number_4]]
-. Breaking change number 4 {ess-icon}
-[%collapsible]
-====
-*Details* +
-Breaking change details 4
-
-*Impact* +
-Breaking change impact description 4
-====
-// end::notable-breaking-changes[]
-

+ 0 - 36
build-tools-internal/src/test/resources/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.generateIndexFile.asciidoc

@@ -1,36 +0,0 @@
-[[migrating-8.4]]
-== Migrating to 8.4
-++++
-<titleabbrev>8.4</titleabbrev>
-++++
-
-This section discusses the changes that you need to be aware of when migrating
-your application to {es} 8.4.
-
-See also <<release-highlights>> and <<es-release-notes>>.
-
-coming::[8.4.0-SNAPSHOT]
-
-//NOTE: The notable-breaking-changes tagged regions are re-used in the
-//Installation and Upgrade Guide
-
-[discrete]
-[[breaking-changes-8.4]]
-=== Breaking changes
-
-The following changes in {es} 8.4 might affect your applications
-and prevent them from operating normally.
-Before upgrading to 8.4 review these changes and take the described steps
-to mitigate the impact.
-
-NOTE: Breaking changes introduced in minor versions are
-normally limited to security and bug fixes.
-Significant changes in behavior are deprecated in a minor release and
-the old behavior is supported until the next major release.
-To find out if you are using any deprecated functionality,
-enable <<deprecation-logging, deprecation logging>>.
-
-include::migrate_8_4/api.asciidoc[]
-include::migrate_8_4/cluster-node-setting.asciidoc[]
-include::migrate_8_4/transform.asciidoc[]
-

+ 149 - 0
build-tools-internal/src/test/resources/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.generateMigrationFile.asciidoc

@@ -0,0 +1,149 @@
+[[migrating-8.4]]
+== Migrating to 8.4
+++++
+<titleabbrev>8.4</titleabbrev>
+++++
+
+This section discusses the changes that you need to be aware of when migrating
+your application to {es} 8.4.
+
+See also <<release-highlights>> and <<es-release-notes>>.
+
+coming::[8.4.0-SNAPSHOT]
+
+
+[discrete]
+[[breaking-changes-8.4]]
+=== Breaking changes
+
+The following changes in {es} 8.4 might affect your applications
+and prevent them from operating normally.
+Before upgrading to 8.4, review these changes and take the described steps
+to mitigate the impact.
+
+NOTE: Breaking changes introduced in minor versions are
+normally limited to security and bug fixes.
+Significant changes in behavior are deprecated in a minor release and
+the old behavior is supported until the next major release.
+To find out if you are using any deprecated functionality,
+enable <<deprecation-logging, deprecation logging>>.
+
+// NOTE: The notable-breaking-changes tagged regions are re-used in the
+// Installation and Upgrade Guide
+// tag::notable-breaking-changes[]
+[discrete]
+[[breaking_84_api_changes]]
+==== API changes
+
+[[breaking_change_number_1]]
+.Breaking change number 1
+[%collapsible]
+====
+*Details* +
+Breaking change details 1
+
+*Impact* +
+Breaking change impact description 1
+====
+
+[discrete]
+[[breaking_84_cluster_and_node_setting_changes]]
+==== Cluster and node setting changes
+
+[[breaking_change_number_2]]
+.Breaking change number 2
+[%collapsible]
+====
+*Details* +
+Breaking change details 2
+
+*Impact* +
+Breaking change impact description 2
+====
+
+[[breaking_change_number_4]]
+.Breaking change number 4
+[%collapsible]
+====
+*Details* +
+Breaking change details 4
+
+*Impact* +
+Breaking change impact description 4
+====
+// end::notable-breaking-changes[]
+
+[discrete]
+[[breaking_84_transform_changes]]
+==== Transform changes
+
+[[breaking_change_number_3]]
+.Breaking change number 3
+[%collapsible]
+====
+*Details* +
+Breaking change details 3
+
+*Impact* +
+Breaking change impact description 3
+====
+
+
+[discrete]
+[[deprecated-8.4]]
+=== Deprecations
+
+The following functionality has been deprecated in {es} 8.4
+and will be removed in a future version.
+While this won't have an immediate impact on your applications,
+we strongly encourage you take the described steps to update your code
+after upgrading to 8.4.
+
+NOTE: Significant changes in behavior are deprecated in a minor release and
+the old behavior is supported until the next major release.
+To find out if you are using any deprecated functionality,
+enable <<deprecation-logging, deprecation logging>>.
+
+// tag::notable-breaking-changes[]
+[discrete]
+[[deprecations_84_cluster_and_node_setting]]
+==== Cluster and node setting deprecations
+
+[[deprecation_change_number_5]]
+.Deprecation change number 5
+[%collapsible]
+====
+*Details* +
+Deprecation change details 5
+
+*Impact* +
+Deprecation change impact description 5
+====
+
+[[deprecation_change_number_6]]
+.Deprecation change number 6
+[%collapsible]
+====
+*Details* +
+Deprecation change details 6
+
+*Impact* +
+Deprecation change impact description 6
+====
+// end::notable-breaking-changes[]
+
+[discrete]
+[[deprecations_84_cluster_and_node_setting]]
+==== Cluster and node setting deprecations
+
+[[deprecation_change_number_7]]
+.Deprecation change number 7
+[%collapsible]
+====
+*Details* +
+Deprecation change details 7
+
+*Impact* +
+Deprecation change impact description 7
+====
+

+ 1 - 2
build-tools-internal/src/test/resources/org/elasticsearch/gradle/internal/release/ReleaseHighlightsGeneratorTest.generateFile.asciidoc

@@ -12,8 +12,7 @@ endif::[]
 // Add previous release to the list
 // Add previous release to the list
 Other versions:
 Other versions:
 
 
-{ref-bare}/8.4/release-highlights.html[8.4]
-| {ref-bare}/8.3/release-highlights.html[8.3]
+{ref-bare}/8.3/release-highlights.html[8.3]
 | {ref-bare}/8.2/release-highlights.html[8.2]
 | {ref-bare}/8.2/release-highlights.html[8.2]
 | {ref-bare}/8.1/release-highlights.html[8.1]
 | {ref-bare}/8.1/release-highlights.html[8.1]
 | {ref-bare}/8.0/release-highlights.html[8.0]
 | {ref-bare}/8.0/release-highlights.html[8.0]

+ 29 - 0
build-tools-internal/src/test/resources/org/elasticsearch/gradle/internal/release/ReleaseHighlightsGeneratorTest.noHighlights.generateFile.asciidoc

@@ -0,0 +1,29 @@
+[[release-highlights]]
+== What's new in {minor-version}
+
+coming::[{minor-version}]
+
+Here are the highlights of what's new and improved in {es} {minor-version}!
+ifeval::[\{release-state}\"!=\"unreleased\"]
+For detailed information about this release, see the <<es-release-notes>> and
+<<breaking-changes>>.
+endif::[]
+
+// Add previous release to the list
+Other versions:
+
+{ref-bare}/8.3/release-highlights.html[8.3]
+| {ref-bare}/8.2/release-highlights.html[8.2]
+| {ref-bare}/8.1/release-highlights.html[8.1]
+| {ref-bare}/8.0/release-highlights.html[8.0]
+
+// The notable-highlights tag marks entries that
+// should be featured in the Stack Installation and Upgrade Guide:
+// tag::notable-highlights[]
+// [discrete]
+// === Heading
+//
+// Description.
+// end::notable-highlights[]
+
+