瀏覽代碼

Make improvements to the release notes generator (#83525)

Forward-port of the `build-tools-internal` parts of #83341.
Rory Hunter 3 年之前
父節點
當前提交
bafdcbd2f1
共有 21 個文件被更改,包括 412 次插入509 次删除
  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;
 
 /**
- * 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 {
     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;
 
 /**
- * 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 {
     @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 org.elasticsearch.gradle.VersionProperties;
-import org.gradle.api.GradleException;
 
 import java.io.File;
 import java.io.FileWriter;
@@ -19,140 +18,65 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
 
 import static java.util.Comparator.comparing;
 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 {
 
-    // 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(
-                generateIndexFile(
+                generateMigrationFile(
                     QualifiedVersion.of(VersionProperties.getElasticsearch()),
-                    Files.readString(indexTemplateFile.toPath()),
+                    Files.readString(migrationTemplateFile.toPath()),
                     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
-    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)
             .filter(Objects::nonNull)
             .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(
                 groupingBy(
                     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<>();
-        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("nextMajor", (version.major() + 1) + ".0");
+        bindings.put("version", version);
 
         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());
 
+    /**
+     * 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) {
         try {
             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 title;
         private String details;
@@ -277,13 +286,13 @@ public class ChangelogEntry {
             if (o == null || getClass() != o.getClass()) {
                 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
@@ -294,7 +303,8 @@ public class ChangelogEntry {
         @Override
         public String toString() {
             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,
                 title,
                 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) {
-        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)
+            .replaceAll("'", "")
             .replaceAll("[^\\w]+", "_")
             .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.GradleException;
 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.RegularFile;
 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.tasks.InputFile;
 import org.gradle.api.tasks.InputFiles;
-import org.gradle.api.tasks.OutputDirectory;
 import org.gradle.api.tasks.OutputFile;
 import org.gradle.api.tasks.TaskAction;
 import org.gradle.process.ExecOperations;
@@ -37,13 +34,13 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
 import javax.inject.Inject;
 
 import static java.util.Comparator.naturalOrder;
-import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
 /**
@@ -58,13 +55,11 @@ public class GenerateReleaseNotesTask extends DefaultTask {
     private final RegularFileProperty releaseNotesTemplate;
     private final RegularFileProperty releaseHighlightsTemplate;
     private final RegularFileProperty breakingChangesTemplate;
-    private final RegularFileProperty breakingChangesAreaTemplate;
 
     private final RegularFileProperty releaseNotesIndexFile;
     private final RegularFileProperty releaseNotesFile;
     private final RegularFileProperty releaseHighlightsFile;
-    private final RegularFileProperty breakingChangesIndexFile;
-    private final DirectoryProperty breakingChangesDirectory;
+    private final RegularFileProperty breakingChangesMigrationFile;
 
     private final GitWrapper gitWrapper;
 
@@ -76,13 +71,11 @@ public class GenerateReleaseNotesTask extends DefaultTask {
         releaseNotesTemplate = objectFactory.fileProperty();
         releaseHighlightsTemplate = objectFactory.fileProperty();
         breakingChangesTemplate = objectFactory.fileProperty();
-        breakingChangesAreaTemplate = objectFactory.fileProperty();
 
         releaseNotesIndexFile = objectFactory.fileProperty();
         releaseNotesFile = objectFactory.fileProperty();
         releaseHighlightsFile = objectFactory.fileProperty();
-        breakingChangesIndexFile = objectFactory.fileProperty();
-        breakingChangesDirectory = objectFactory.directoryProperty();
+        breakingChangesMigrationFile = objectFactory.fileProperty();
 
         gitWrapper = new GitWrapper(execOperations);
     }
@@ -136,15 +129,13 @@ public class GenerateReleaseNotesTask extends DefaultTask {
         LOGGER.info("Generating breaking changes / deprecations notes...");
         BreakingChangesGenerator.update(
             this.breakingChangesTemplate.get().getAsFile(),
-            this.breakingChangesIndexFile.get().getAsFile(),
-            this.breakingChangesDirectory.get().getAsFile(),
-            this.breakingChangesAreaTemplate.get().getAsFile(),
+            this.breakingChangesMigrationFile.get().getAsFile(),
             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 currentVersion the version to base the query upon
      * @return all versions in the series
@@ -152,7 +143,8 @@ public class GenerateReleaseNotesTask extends DefaultTask {
     @VisibleForTesting
     static Set<QualifiedVersion> getVersions(GitWrapper gitWrapper, String 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);
         return versions;
     }
@@ -183,13 +175,13 @@ public class GenerateReleaseNotesTask extends DefaultTask {
         QualifiedVersion currentVersion = QualifiedVersion.of(versionString);
 
         // 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)
             // Only keep earlier versions, and if `currentVersion` is a prerelease, then only prereleases too.
             .filter(each -> each.isBefore(currentVersion) && (currentVersion.hasQualifier() == each.hasQualifier()))
             .sorted(naturalOrder())
-            .collect(toList());
+            .toList();
 
         if (earlierVersions.isEmpty()) {
             throw new GradleException("Failed to find git tags prior to [v" + currentVersion + "]");
@@ -348,29 +340,11 @@ public class GenerateReleaseNotesTask extends DefaultTask {
     }
 
     @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.tasks.Internal;
 import org.gradle.api.tasks.TaskAction;
-import org.gradle.api.tasks.options.Option;
 import org.gradle.process.ExecOperations;
 
 import java.io.File;
@@ -47,8 +46,6 @@ public class PruneChangelogsTask extends DefaultTask {
     private final GitWrapper gitWrapper;
     private final Path rootDir;
 
-    private boolean dryRun;
-
     @Inject
     public PruneChangelogsTask(Project project, ObjectFactory objectFactory, ExecOperations execOperations) {
         changelogs = objectFactory.fileCollection();
@@ -65,16 +62,6 @@ public class PruneChangelogsTask extends DefaultTask {
         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
     public void executeTask() {
         findAndDeleteFiles(
@@ -82,7 +69,6 @@ public class PruneChangelogsTask extends DefaultTask {
             files -> files.stream().filter(each -> each.delete() == false).collect(Collectors.toSet()),
             QualifiedVersion.of(VersionProperties.getElasticsearch()),
             this.getChangelogs().getFiles(),
-            this.dryRun,
             this.rootDir
         );
     }
@@ -93,7 +79,6 @@ public class PruneChangelogsTask extends DefaultTask {
         DeleteHelper deleteHelper,
         QualifiedVersion version,
         Set<File> allFilesInCheckout,
-        boolean dryRun,
         Path rootDir
     ) {
         if (allFilesInCheckout.isEmpty()) {
@@ -117,20 +102,18 @@ public class PruneChangelogsTask extends DefaultTask {
             return;
         }
 
-        LOGGER.warn("The following changelog files {} be deleted:", dryRun ? "can" : "will");
+        LOGGER.warn("The following changelog files will be deleted:");
         LOGGER.warn("");
         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 version the git tags are inspected relative to this version
@@ -162,10 +145,11 @@ public class PruneChangelogsTask extends DefaultTask {
      */
     @VisibleForTesting
     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) {
             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 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.setBreakingChangesTemplate(projectDirectory.file(RESOURCES + "templates/breaking-changes.asciidoc"));
-            task.setBreakingChangesIndexFile(
+            task.setBreakingChangesMigrationFile(
                 projectDirectory.file(
                     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);
         });

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

@@ -78,8 +78,8 @@
             "Stats",
             "Store",
             "Suggesters",
-            "TLS",
             "Task Management",
+            "TLS",
             "Transform",
             "TSDB",
             "Watcher"
@@ -109,10 +109,10 @@
           "$ref": "#/definitions/Highlight"
         },
         "breaking": {
-          "$ref": "#/definitions/Breaking"
+          "$ref": "#/definitions/CompatibilityChange"
         },
         "deprecation": {
-          "$ref": "#/definitions/Deprecation"
+          "$ref": "#/definitions/CompatibilityChange"
         }
       },
       "required": [
@@ -140,10 +140,10 @@
       ],
       "additionalProperties": false
     },
-    "Breaking": {
+    "CompatibilityChange": {
       "properties": {
         "area": {
-          "$ref": "#/definitions/breakingArea"
+          "$ref": "#/definitions/compatibilityChangeArea"
         },
         "title": {
           "type": "string",
@@ -172,35 +172,12 @@
       ],
       "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",
       "enum": [
         "Cluster and node setting",
         "Command line tool",
+        "CRUD",
         "Index setting",
         "JVM option",
         "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) { %>
 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]
 [[breaking-changes-${majorDotMinor}]]
 === Breaking changes
 
 The following changes in {es} ${majorDotMinor} might affect your applications
 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.
 
 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>>.
 
 <%
-  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]
 [[deprecated-${majorDotMinor}]]
 === Deprecations
 
 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,
 we strongly encourage you take the described steps to update your code
 after upgrading to ${majorDotMinor}.
@@ -52,24 +81,40 @@ after upgrading to ${majorDotMinor}.
 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>>."
+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.title}
 [%collapsible]
 ====
 *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"
 }
 
-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[]
 <% for (highlight in notableHighlights) { %>
 [discrete]

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

@@ -29,40 +29,15 @@ public class BreakingChangesGeneratorTest {
         // given:
         final String template = getResource("/templates/breaking-changes.asciidoc");
         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();
 
         // 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"),
             template,
-            breakingArea,
             entries
         );
 
@@ -112,7 +87,40 @@ public class BreakingChangesGeneratorTest {
         breaking4.setImpact("Breaking change impact description 4");
         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 {

+ 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.util.Map;
 import java.util.Set;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import static org.hamcrest.Matchers.aMapWithSize;
@@ -154,12 +153,12 @@ public class GenerateReleaseNotesTaskTest extends GradleUnitTestCase {
     public void getVersions_includesCurrentVersion() {
         // given:
         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)
         );
 
         // when:
-        Set<QualifiedVersion> versions = GenerateReleaseNotesTask.getVersions(gitWrapper, "8.0.0-SNAPSHOT");
+        Set<QualifiedVersion> versions = GenerateReleaseNotesTask.getVersions(gitWrapper, "8.2.0-SNAPSHOT");
 
         // then:
         assertThat(
@@ -173,8 +172,10 @@ public class GenerateReleaseNotesTaskTest extends GradleUnitTestCase {
                     "8.0.0-beta3",
                     "8.0.0-rc1",
                     "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
     public void findAndDeleteFiles_withNoFiles_doesNothing() {
         // when:
-        findAndDeleteFiles(gitWrapper, deleteHelper, null, Set.of(), false, Path.of("rootDir"));
+        findAndDeleteFiles(gitWrapper, deleteHelper, null, Set.of(), Path.of("rootDir"));
 
         // then:
         verify(gitWrapper, never()).listVersions(anyString());
@@ -68,7 +68,7 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
     @Test
     public void findAndDeleteFiles_withNoPriorVersions_deletesNothing() {
         // given:
-        when(gitWrapper.listVersions(anyString())).thenReturn(Stream.of());
+        when(gitWrapper.listVersions(anyString())).thenAnswer(args -> Stream.of());
 
         // when:
         findAndDeleteFiles(
@@ -76,12 +76,10 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
             deleteHelper,
             QualifiedVersion.of("7.16.0"),
             Set.of(new File("rootDir/docs/changelog/1234.yml")),
-            false,
             Path.of("rootDir")
         );
 
         // then:
-        verify(gitWrapper).listVersions("v7.*");
         verify(gitWrapper, never()).listFiles(anyString(), anyString());
     }
 
@@ -92,7 +90,8 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
     @Test
     public void findAndDeleteFiles_withFilesButNoHistoricalFiles_deletesNothing() {
         // 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:
@@ -101,45 +100,11 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
             deleteHelper,
             QualifiedVersion.of("7.16.0"),
             Set.of(new File("rootDir/docs/changelog/1234.yml")),
-            false,
             Path.of("rootDir")
         );
 
         // 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
     public void findAndDeleteFiles_withFilesToDelete_deletesFiles() {
         // 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("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/9123.yml")
             ),
-            false,
             Path.of("rootDir")
         );
 
         // 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")));
     }
 
@@ -179,7 +143,8 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
     @Test
     public void findAndDeleteFiles_withFilesToDeleteButDeleteFails_throwsException() {
         // 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("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/9123.yml")
                 ),
-                false,
                 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"));
     }
 
-    /**
-     * 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
      * the previous major series are returned.
@@ -234,6 +179,7 @@ public class PruneChangelogsTaskTests extends GradleUnitTestCase {
     @Test
     public void findPreviousVersion_afterStartOfMajorSeries_inspectsCurrentMajorSeries() {
         // given:
+        when(gitWrapper.listVersions("v6.*")).thenReturn(Stream.of());
         when(gitWrapper.listVersions("v7.*")).thenReturn(
             Stream.of(
                 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 {
 
+    /**
+     * 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.
      */

+ 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
 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.1/release-highlights.html[8.1]
 | {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[]
+
+