[release-notes] Update automation to use new markdown format (#124161)

This commit is contained in:
Brian Seeders 2025-05-28 14:53:02 -04:00 committed by GitHub
parent f275b71766
commit c2ad34b97f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 3984 additions and 1489 deletions

View file

@ -1,85 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.release;
import com.google.common.annotations.VisibleForTesting;
import org.elasticsearch.gradle.VersionProperties;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
/**
* Generates the page that contains breaking changes deprecations for a minor release series.
*/
public class BreakingChangesGenerator {
static void update(File migrationTemplateFile, File migrationOutputFile, List<ChangelogEntry> entries) throws IOException {
try (FileWriter output = new FileWriter(migrationOutputFile)) {
output.write(
generateMigrationFile(
QualifiedVersion.of(VersionProperties.getElasticsearch()),
Files.readString(migrationTemplateFile.toPath()),
entries
)
);
}
}
@VisibleForTesting
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::isNotable,
TreeMap::new,
groupingBy(ChangelogEntry.Deprecation::getArea, TreeMap::new, toList())
)
);
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,
TreeMap::new,
groupingBy(ChangelogEntry.Breaking::getArea, TreeMap::new, toList())
)
);
final Map<String, Object> bindings = new HashMap<>();
bindings.put("breakingByNotabilityByArea", breakingByNotabilityByArea);
bindings.put("deprecationsByNotabilityByArea", deprecationsByNotabilityByArea);
bindings.put("isElasticsearchSnapshot", version.isSnapshot());
bindings.put("majorDotMinor", version.major() + "." + version.minor());
bindings.put("majorDotMinorDotRevision", version.major() + "." + version.minor() + "." + version.revision());
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);
}
}

View file

@ -0,0 +1,252 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.release;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import org.gradle.api.DefaultTask;
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;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.options.Option;
import org.gradle.process.ExecOperations;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static java.util.stream.Collectors.toList;
public class BundleChangelogsTask extends DefaultTask {
private static final Logger LOGGER = Logging.getLogger(BundleChangelogsTask.class);
private final ConfigurableFileCollection changelogs;
private final RegularFileProperty bundleFile;
private final DirectoryProperty changelogDirectory;
private final DirectoryProperty changelogBundlesDirectory;
private final GitWrapper gitWrapper;
@Nullable
private String branch;
@Nullable
private String bcRef;
private boolean finalize;
@Option(option = "branch", description = "Branch (or other ref) to use for generating the changelog bundle.")
public void setBranch(String branch) {
this.branch = branch;
}
@Option(
option = "bc-ref",
description = "A source ref, typically the sha of a BC, that should be used to source PRs for changelog entries. "
+ "The actual content of the changelogs will come from the 'branch' ref. "
+ "You should generally always use bc-ref."
)
public void setBcRef(String ref) {
this.bcRef = ref;
}
@Option(option = "finalize", description = "Specify that the bundle is finalized, i.e. that the version has been released.")
public void setFinalize(boolean finalize) {
this.finalize = finalize;
}
private static final ObjectMapper yamlMapper = new ObjectMapper(
new YAMLFactory().enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)
.disable(YAMLGenerator.Feature.SPLIT_LINES)
.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR)
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
.enable(YAMLGenerator.Feature.LITERAL_BLOCK_STYLE)
).setSerializationInclusion(JsonInclude.Include.NON_NULL);
@Inject
public BundleChangelogsTask(ObjectFactory objectFactory, ExecOperations execOperations) {
changelogs = objectFactory.fileCollection();
bundleFile = objectFactory.fileProperty();
changelogDirectory = objectFactory.directoryProperty();
changelogBundlesDirectory = objectFactory.directoryProperty();
gitWrapper = new GitWrapper(execOperations);
}
/*
Given a branch, and possibly a build candidate commit sha
Check out the changelog yaml files from the branch/BC sha
Then, bundle them all up into one file and write it to disk, along with a timestamp and whether the release is considered released
When using a branch without a BC sha:
- Check out the changelog yaml files from the HEAD of the branch
When using a BC sha:
- Check out the changelog yaml files from the BC commit
- Update those files with any updates from the HEAD of the branch (in case the changelogs get modified later)
- Check for any changelog yaml files that were added AFTER the BC,
but whose PR was merged before the BC (in case someone adds a forgotten changelog after the fact)
*/
@TaskAction
public void executeTask() throws IOException {
if (branch == null) {
throw new IllegalArgumentException("'branch' not specified.");
}
final String upstreamRemote = gitWrapper.getUpstream();
Set<String> entriesFromBc = Set.of();
var didCheckoutChangelogs = false;
try {
var usingBcRef = bcRef != null && bcRef.isEmpty() == false;
if (usingBcRef) {
// Check out all the changelogs that existed at the time of the BC
checkoutChangelogs(gitWrapper, upstreamRemote, bcRef);
entriesFromBc = changelogDirectory.getAsFileTree().getFiles().stream().map(File::getName).collect(Collectors.toSet());
// Then add/update changelogs from the HEAD of the branch
// We do an "add" here, rather than checking out the entire directory, in case changelogs have been removed for some reason
addChangelogsFromRef(gitWrapper, upstreamRemote, branch);
} else {
checkoutChangelogs(gitWrapper, upstreamRemote, branch);
}
didCheckoutChangelogs = true;
Properties props = new Properties();
props.load(
new StringReader(
gitWrapper.runCommand("git", "show", upstreamRemote + "/" + branch + ":build-tools-internal/version.properties")
)
);
String version = props.getProperty("elasticsearch");
LOGGER.info("Finding changelog files for " + version + "...");
Set<String> finalEntriesFromBc = entriesFromBc;
List<ChangelogEntry> entries = changelogDirectory.getAsFileTree().getFiles().stream().filter(f -> {
// When not using a bc ref, we just take everything from the branch/sha passed in
if (usingBcRef == false) {
return true;
}
// If the changelog was present in the BC sha, always use it
if (finalEntriesFromBc.contains(f.getName())) {
return true;
}
// Otherwise, let's check to see if a reference to the PR exists in the commit log for the sha
// This specifically covers the case of a PR being merged into the BC with a missing changelog file, and the file added
// later.
var prNumber = f.getName().replace(".yaml", "");
var output = gitWrapper.runCommand("git", "log", bcRef, "--grep", "(#" + prNumber + ")");
return output.trim().isEmpty() == false;
}).map(ChangelogEntry::parse).sorted(Comparator.comparing(ChangelogEntry::getPr)).collect(toList());
ChangelogBundle bundle = new ChangelogBundle(version, finalize, Instant.now().toString(), entries);
yamlMapper.writeValue(new File("docs/release-notes/changelog-bundles/" + version + ".yml"), bundle);
} finally {
if (didCheckoutChangelogs) {
gitWrapper.runCommand("git", "restore", "-s@", "-SW", "--", changelogDirectory.get().toString());
}
}
}
private void checkoutChangelogs(GitWrapper gitWrapper, String upstream, String ref) {
gitWrapper.updateRemote(upstream);
// If the changelog directory contains modified/new files, we should error out instead of wiping them out silently
var output = gitWrapper.runCommand("git", "status", "--porcelain", changelogDirectory.get().toString()).trim();
if (output.isEmpty() == false) {
throw new IllegalStateException(
"Changelog directory contains changes that will be wiped out by this task:\n" + changelogDirectory.get() + "\n" + output
);
}
gitWrapper.runCommand("rm", "-rf", changelogDirectory.get().toString());
var refSpec = upstream + "/" + ref;
if (ref.contains("upstream/")) {
refSpec = ref.replace("upstream/", upstream + "/");
} else if (ref.matches("^[0-9a-f]+$")) {
refSpec = ref;
}
gitWrapper.runCommand("git", "checkout", refSpec, "--", changelogDirectory.get().toString());
}
private void addChangelogsFromRef(GitWrapper gitWrapper, String upstream, String ref) {
var refSpec = upstream + "/" + ref;
if (ref.contains("upstream/")) {
refSpec = ref.replace("upstream/", upstream + "/");
} else if (ref.matches("^[0-9a-f]+$")) {
refSpec = ref;
}
gitWrapper.runCommand("git", "checkout", refSpec, "--", changelogDirectory.get() + "/*.yaml");
}
@InputDirectory
public DirectoryProperty getChangelogDirectory() {
return changelogDirectory;
}
public void setChangelogDirectory(Directory dir) {
this.changelogDirectory.set(dir);
}
@InputDirectory
public DirectoryProperty getChangelogBundlesDirectory() {
return changelogBundlesDirectory;
}
public void setChangelogBundlesDirectory(Directory dir) {
this.changelogBundlesDirectory.set(dir);
}
@InputFiles
public FileCollection getChangelogs() {
return changelogs;
}
public void setChangelogs(FileCollection files) {
this.changelogs.setFrom(files);
}
@OutputFile
public RegularFileProperty getBundleFile() {
return bundleFile;
}
public void setBundleFile(RegularFile file) {
this.bundleFile.set(file);
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.release;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
public record ChangelogBundle(String version, boolean released, String generated, List<ChangelogEntry> changelogs) {
private static final Logger LOGGER = Logging.getLogger(GenerateReleaseNotesTask.class);
private static final ObjectMapper yamlMapper = new ObjectMapper(
new YAMLFactory().enable(YAMLGenerator.Feature.MINIMIZE_QUOTES).disable(YAMLGenerator.Feature.SPLIT_LINES)
);
public ChangelogBundle(String version, String generated, List<ChangelogEntry> changelogs) {
this(version, false, generated, changelogs);
}
public static ChangelogBundle parse(File file) {
try {
return yamlMapper.readValue(file, ChangelogBundle.class);
} catch (IOException e) {
LOGGER.error("Failed to parse changelog bundle from " + file.getAbsolutePath(), e);
throw new UncheckedIOException(e);
}
}
public static ChangelogBundle copy(ChangelogBundle bundle) {
List<ChangelogEntry> changelogs = bundle.changelogs().stream().toList();
return new ChangelogBundle(bundle.version(), bundle.released(), bundle.generated(), changelogs);
}
public ChangelogBundle withChangelogs(List<ChangelogEntry> changelogs) {
return new ChangelogBundle(version, released, generated, changelogs);
}
}

View file

@ -9,6 +9,7 @@
package org.elasticsearch.gradle.internal.release;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
@ -35,12 +36,12 @@ public class ChangelogEntry {
private static final Logger LOGGER = Logging.getLogger(GenerateReleaseNotesTask.class);
private Integer pr;
private List<Integer> issues;
private String summary;
private String area;
private String type;
private String summary;
private Highlight highlight;
private List<Integer> issues;
private Breaking breaking;
private Highlight highlight;
private Deprecation deprecation;
private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
@ -193,6 +194,7 @@ public class ChangelogEntry {
this.body = body;
}
@JsonIgnore
public String getAnchor() {
return generatedAnchor(this.title);
}
@ -278,6 +280,7 @@ public class ChangelogEntry {
this.notable = notable;
}
@JsonIgnore
public String getAnchor() {
return generatedAnchor(this.title);
}

View file

@ -13,75 +13,60 @@ import com.google.common.annotations.VisibleForTesting;
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.FileCollection;
import org.gradle.api.file.Directory;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFile;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.process.ExecOperations;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Comparator;
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 java.util.stream.Stream;
import javax.inject.Inject;
import static java.util.Comparator.naturalOrder;
import static java.util.stream.Collectors.toSet;
/**
* Orchestrates the steps required to generate or update various release notes files.
*/
public class GenerateReleaseNotesTask extends DefaultTask {
private static final Logger LOGGER = Logging.getLogger(GenerateReleaseNotesTask.class);
private final ConfigurableFileCollection changelogs;
private final RegularFileProperty releaseNotesIndexTemplate;
private final RegularFileProperty releaseNotesTemplate;
private final RegularFileProperty releaseHighlightsTemplate;
private final RegularFileProperty breakingChangesTemplate;
private final RegularFileProperty migrationIndexTemplate;
private final RegularFileProperty deprecationsTemplate;
private final RegularFileProperty releaseNotesIndexFile;
private final RegularFileProperty releaseNotesFile;
private final RegularFileProperty releaseHighlightsFile;
private final RegularFileProperty breakingChangesMigrationFile;
private final RegularFileProperty migrationIndexFile;
private final RegularFileProperty breakingChangesFile;
private final RegularFileProperty deprecationsFile;
private final DirectoryProperty changelogBundleDirectory;
private final GitWrapper gitWrapper;
@Inject
public GenerateReleaseNotesTask(ObjectFactory objectFactory, ExecOperations execOperations) {
changelogs = objectFactory.fileCollection();
releaseNotesIndexTemplate = objectFactory.fileProperty();
releaseNotesTemplate = objectFactory.fileProperty();
releaseHighlightsTemplate = objectFactory.fileProperty();
breakingChangesTemplate = objectFactory.fileProperty();
migrationIndexTemplate = objectFactory.fileProperty();
deprecationsTemplate = objectFactory.fileProperty();
releaseNotesIndexFile = objectFactory.fileProperty();
releaseNotesFile = objectFactory.fileProperty();
releaseHighlightsFile = objectFactory.fileProperty();
breakingChangesMigrationFile = objectFactory.fileProperty();
migrationIndexFile = objectFactory.fileProperty();
breakingChangesFile = objectFactory.fileProperty();
deprecationsFile = objectFactory.fileProperty();
changelogBundleDirectory = objectFactory.directoryProperty();
gitWrapper = new GitWrapper(execOperations);
}
@ -94,170 +79,42 @@ public class GenerateReleaseNotesTask extends DefaultTask {
findAndUpdateUpstreamRemote(gitWrapper);
}
LOGGER.info("Finding changelog files...");
final Map<QualifiedVersion, Set<File>> filesByVersion = partitionFilesByVersion(
gitWrapper,
currentVersion,
this.changelogs.getFiles()
);
final List<ChangelogEntry> entries = new ArrayList<>();
final Map<QualifiedVersion, Set<ChangelogEntry>> changelogsByVersion = new HashMap<>();
filesByVersion.forEach((version, files) -> {
Set<ChangelogEntry> entriesForVersion = files.stream().map(ChangelogEntry::parse).collect(toSet());
entries.addAll(entriesForVersion);
changelogsByVersion.put(version, entriesForVersion);
});
final Set<QualifiedVersion> versions = getVersions(gitWrapper, currentVersion);
LOGGER.info("Updating release notes index...");
ReleaseNotesIndexGenerator.update(
versions,
this.releaseNotesIndexTemplate.get().getAsFile(),
this.releaseNotesIndexFile.get().getAsFile()
);
LOGGER.info("Generating release notes...");
final QualifiedVersion qualifiedVersion = QualifiedVersion.of(currentVersion);
ReleaseNotesGenerator.update(
this.releaseNotesTemplate.get().getAsFile(),
this.releaseNotesFile.get().getAsFile(),
qualifiedVersion,
changelogsByVersion.getOrDefault(qualifiedVersion, Set.of())
);
// Only update breaking changes and migration guide for new minors
if (qualifiedVersion.revision() == 0) {
LOGGER.info("Generating release highlights...");
ReleaseHighlightsGenerator.update(
this.releaseHighlightsTemplate.get().getAsFile(),
this.releaseHighlightsFile.get().getAsFile(),
entries
);
LOGGER.info("Generating breaking changes / deprecations notes...");
BreakingChangesGenerator.update(
this.breakingChangesTemplate.get().getAsFile(),
this.breakingChangesMigrationFile.get().getAsFile(),
entries
);
LOGGER.info("Updating migration/index...");
MigrationIndexGenerator.update(
getMinorVersions(versions),
this.migrationIndexTemplate.get().getAsFile(),
this.migrationIndexFile.get().getAsFile()
);
}
}
/**
* 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
*/
@VisibleForTesting
static Set<QualifiedVersion> getVersions(GitWrapper gitWrapper, String currentVersion) {
QualifiedVersion qualifiedVersion = QualifiedVersion.of(currentVersion);
final String pattern = "v" + qualifiedVersion.major() + ".*";
// We may be generating notes for a minor version prior to the latest minor, so we need to filter out versions that are too new.
Set<QualifiedVersion> versions = Stream.concat(
gitWrapper.listVersions(pattern).filter(v -> v.isBefore(qualifiedVersion)),
Stream.of(qualifiedVersion)
).collect(toSet());
// If this is a new minor ensure we include the previous minor, which may not have been released
if (qualifiedVersion.minor() > 0 && qualifiedVersion.revision() == 0) {
QualifiedVersion previousMinor = new QualifiedVersion(qualifiedVersion.major(), qualifiedVersion.minor() - 1, 0, null);
versions.add(previousMinor);
}
return versions;
}
/**
* Convert set of QualifiedVersion to MinorVersion by deleting all but the major and minor components.
*/
@VisibleForTesting
static Set<MinorVersion> getMinorVersions(Set<QualifiedVersion> versions) {
return versions.stream().map(MinorVersion::of).collect(toSet());
}
/**
* Group a set of files by the version in which they first appeared, up until the supplied version. Any files not
* present in an earlier version are assumed to have been introduced in the specified version.
*
* <p>This method works by finding all git tags prior to {@param versionString} in the same minor series, and
* examining the git tree for that tag. By doing this over each tag, it is possible to see how the contents
* of the changelog directory changed over time.
*
* @param gitWrapper used to call `git`
* @param versionString the "current" version. Does not require a tag in git.
* @param allFilesInCheckout the files to partition
* @return a mapping from version to the files added in that version.
*/
@VisibleForTesting
static Map<QualifiedVersion, Set<File>> partitionFilesByVersion(
GitWrapper gitWrapper,
String versionString,
Set<File> allFilesInCheckout
) {
if (needsGitTags(versionString) == false) {
return Map.of(QualifiedVersion.of(versionString), allFilesInCheckout);
}
QualifiedVersion currentVersion = QualifiedVersion.of(versionString);
// Find all tags for this minor series, using a wildcard tag pattern.
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.isSnapshot() || (currentVersion.hasQualifier() == each.hasQualifier()))
)
.sorted(naturalOrder())
LOGGER.info("Finding changelog bundles...");
List<ChangelogBundle> allBundles = this.changelogBundleDirectory.getAsFileTree()
.getFiles()
.stream()
.map(ChangelogBundle::parse)
.toList();
if (earlierVersions.isEmpty()) {
throw new GradleException("Failed to find git tags prior to [v" + currentVersion + "]");
var bundles = getSortedBundlesWithUniqueChangelogs(allBundles);
LOGGER.info("Generating release notes...");
ReleaseNotesGenerator.update(this.releaseNotesTemplate.get().getAsFile(), this.releaseNotesFile.get().getAsFile(), bundles);
ReleaseNotesGenerator.update(this.breakingChangesTemplate.get().getAsFile(), this.breakingChangesFile.get().getAsFile(), bundles);
ReleaseNotesGenerator.update(this.deprecationsTemplate.get().getAsFile(), this.deprecationsFile.get().getAsFile(), bundles);
}
@VisibleForTesting
static List<ChangelogBundle> getSortedBundlesWithUniqueChangelogs(List<ChangelogBundle> bundles) {
List<ChangelogBundle> sorted = bundles.stream()
.sorted(Comparator.comparing(ChangelogBundle::released).reversed().thenComparing(ChangelogBundle::generated))
.toList();
// Ensure that each changelog/PR only shows up once, in its earliest release
var uniquePrs = new HashSet<Integer>();
List<ChangelogBundle> modifiedBundles = new ArrayList<>();
for (int i = sorted.size() - 1; i >= 0; i--) {
var bundle = sorted.get(i);
if (bundle.released() == false) {
List<ChangelogEntry> entries = bundle.changelogs().stream().filter(c -> false == uniquePrs.contains(c.getPr())).toList();
modifiedBundles.add(bundle.withChangelogs(entries));
} else {
modifiedBundles.add(bundle);
}
uniquePrs.addAll(bundle.changelogs().stream().map(ChangelogEntry::getPr).toList());
}
Map<QualifiedVersion, Set<File>> partitionedFiles = new HashMap<>();
Set<File> mutableAllFilesInCheckout = new HashSet<>(allFilesInCheckout);
// 1. For each earlier version
earlierVersions.forEach(earlierVersion -> {
// 2. Find all the changelog files it contained
Set<String> filesInTreeForVersion = gitWrapper.listFiles("v" + earlierVersion, "docs/changelog")
.map(line -> Path.of(line).getFileName().toString())
.collect(toSet());
Set<File> filesForVersion = new HashSet<>();
partitionedFiles.put(earlierVersion, filesForVersion);
// 3. Find the `File` object for each one
final Iterator<File> filesIterator = mutableAllFilesInCheckout.iterator();
while (filesIterator.hasNext()) {
File nextFile = filesIterator.next();
if (filesInTreeForVersion.contains(nextFile.getName())) {
// 4. And remove it so that it is associated with the earlier version
filesForVersion.add(nextFile);
filesIterator.remove();
}
}
});
// 5. Associate whatever is left with the current version.
partitionedFiles.put(currentVersion, mutableAllFilesInCheckout);
return partitionedFiles;
return modifiedBundles;
}
/**
@ -266,18 +123,7 @@ public class GenerateReleaseNotesTask extends DefaultTask {
*/
private static void findAndUpdateUpstreamRemote(GitWrapper gitWrapper) {
LOGGER.info("Finding upstream git remote");
// We need to ensure the tags are up-to-date. Find the correct remote to use
String upstream = gitWrapper.listRemotes()
.entrySet()
.stream()
.filter(entry -> entry.getValue().contains("elastic/elasticsearch"))
.findFirst()
.map(Map.Entry::getKey)
.orElseThrow(
() -> new GradleException(
"I need to ensure the git tags are up-to-date, but I couldn't find a git remote for [elastic/elasticsearch]"
)
);
String upstream = gitWrapper.getUpstream();
LOGGER.info("Updating remote [{}]", upstream);
// Now update the remote, and make sure we update the tags too
@ -308,22 +154,13 @@ public class GenerateReleaseNotesTask extends DefaultTask {
return true;
}
@InputFiles
public FileCollection getChangelogs() {
return changelogs;
@InputDirectory
public DirectoryProperty getChangelogBundleDirectory() {
return changelogBundleDirectory;
}
public void setChangelogs(FileCollection files) {
this.changelogs.setFrom(files);
}
@InputFile
public RegularFileProperty getReleaseNotesIndexTemplate() {
return releaseNotesIndexTemplate;
}
public void setReleaseNotesIndexTemplate(RegularFile file) {
this.releaseNotesIndexTemplate.set(file);
public void setChangelogBundleDirectory(Directory dir) {
this.changelogBundleDirectory.set(dir);
}
@InputFile
@ -354,21 +191,12 @@ public class GenerateReleaseNotesTask extends DefaultTask {
}
@InputFile
public RegularFileProperty getMigrationIndexTemplate() {
return migrationIndexTemplate;
public RegularFileProperty getDeprecationsTemplate() {
return deprecationsTemplate;
}
public void setMigrationIndexTemplate(RegularFile file) {
this.migrationIndexTemplate.set(file);
}
@OutputFile
public RegularFileProperty getReleaseNotesIndexFile() {
return releaseNotesIndexFile;
}
public void setReleaseNotesIndexFile(RegularFile file) {
this.releaseNotesIndexFile.set(file);
public void setDeprecationsTemplate(RegularFile file) {
this.deprecationsTemplate.set(file);
}
@OutputFile
@ -390,20 +218,20 @@ public class GenerateReleaseNotesTask extends DefaultTask {
}
@OutputFile
public RegularFileProperty getBreakingChangesMigrationFile() {
return breakingChangesMigrationFile;
public RegularFileProperty getBreakingChangesFile() {
return breakingChangesFile;
}
public void setBreakingChangesMigrationFile(RegularFile file) {
this.breakingChangesMigrationFile.set(file);
public void setBreakingChangesFile(RegularFile file) {
this.breakingChangesFile.set(file);
}
@OutputFile
public RegularFileProperty getMigrationIndexFile() {
return migrationIndexFile;
public RegularFileProperty getDeprecationsFile() {
return deprecationsFile;
}
public void setMigrationIndexFile(RegularFile file) {
this.migrationIndexFile.set(file);
public void setDeprecationsFile(RegularFile file) {
this.deprecationsFile.set(file);
}
}

View file

@ -9,6 +9,7 @@
package org.elasticsearch.gradle.internal.release;
import org.gradle.api.GradleException;
import org.gradle.process.ExecOperations;
import java.io.ByteArrayOutputStream;
@ -87,4 +88,14 @@ public class GitWrapper {
public Stream<String> listFiles(String ref, String path) {
return runCommand("git", "ls-tree", "--name-only", "-r", ref, path).lines();
}
public String getUpstream() {
String upstream = listRemotes().entrySet()
.stream()
.filter(entry -> entry.getValue().contains("elastic/elasticsearch"))
.findFirst()
.map(Map.Entry::getKey)
.orElseThrow(() -> new GradleException("Couldn't find a git remote for [elastic/elasticsearch]"));
return upstream;
}
}

View file

@ -1,51 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.release;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import static java.util.Comparator.reverseOrder;
/**
* This class ensures that the migrate/index page has the appropriate anchors and include directives
* for the current repository version.
*/
public class MigrationIndexGenerator {
static void update(Set<MinorVersion> versions, File indexTemplate, File indexFile) throws IOException {
try (FileWriter indexFileWriter = new FileWriter(indexFile)) {
indexFileWriter.write(generateFile(versions, Files.readString(indexTemplate.toPath())));
}
}
@VisibleForTesting
static String generateFile(Set<MinorVersion> versionsSet, String template) throws IOException {
final Set<MinorVersion> versions = new TreeSet<>(reverseOrder());
versions.addAll(versionsSet);
final List<String> includeVersions = versions.stream().map(MinorVersion::underscore).collect(Collectors.toList());
final Map<String, Object> bindings = new HashMap<>();
bindings.put("versions", versions);
bindings.put("includeVersions", includeVersions);
return TemplateUtils.render(template, bindings);
}
}

View file

@ -1,68 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.release;
import com.google.common.annotations.VisibleForTesting;
import org.elasticsearch.gradle.VersionProperties;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Generates the release highlights notes, for changelog files that contain the <code>highlight</code> field.
*/
public class ReleaseHighlightsGenerator {
static void update(File templateFile, File outputFile, List<ChangelogEntry> entries) throws IOException {
try (FileWriter output = new FileWriter(outputFile)) {
output.write(
generateFile(QualifiedVersion.of(VersionProperties.getElasticsearch()), Files.readString(templateFile.toPath()), entries)
);
}
}
@VisibleForTesting
static String generateFile(QualifiedVersion version, String template, List<ChangelogEntry> entries) throws IOException {
final List<String> priorVersions = new ArrayList<>();
if (version.minor() > 0) {
final int major = version.major();
for (int minor = version.minor() - 1; minor >= 0; minor--) {
String majorMinor = major + "." + minor;
priorVersions.add("{ref-bare}/" + majorMinor + "/release-highlights.html[" + majorMinor + "]");
}
}
final Map<Boolean, List<ChangelogEntry.Highlight>> groupedHighlights = entries.stream()
.map(ChangelogEntry::getHighlight)
.filter(Objects::nonNull)
.sorted(Comparator.comparingInt(ChangelogEntry.Highlight::getPr))
.collect(Collectors.groupingBy(ChangelogEntry.Highlight::isNotable, Collectors.toList()));
final List<ChangelogEntry.Highlight> notableHighlights = groupedHighlights.getOrDefault(true, List.of());
final List<ChangelogEntry.Highlight> nonNotableHighlights = groupedHighlights.getOrDefault(false, List.of());
final Map<String, Object> bindings = new HashMap<>();
bindings.put("priorVersions", priorVersions);
bindings.put("notableHighlights", notableHighlights);
bindings.put("nonNotableHighlights", nonNotableHighlights);
return TemplateUtils.render(template, bindings);
}
}

View file

@ -15,13 +15,16 @@ import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Objects;
import java.util.TreeMap;
import static java.util.Comparator.comparing;
import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
@ -30,6 +33,17 @@ import static java.util.stream.Collectors.toList;
* type of change, then by team area.
*/
public class ReleaseNotesGenerator {
private record ChangelogsBundleWrapper(
QualifiedVersion version,
ChangelogBundle bundle,
Map<String, Map<String, List<ChangelogEntry>>> changelogsByTypeByArea,
QualifiedVersion unqualifiedVersion,
String versionWithoutSeparator,
List<ChangelogEntry.Highlight> notableHighlights,
List<ChangelogEntry.Highlight> nonNotableHighlights
) {}
/**
* These mappings translate change types into the headings as they should appear in the release notes.
*/
@ -39,40 +53,95 @@ public class ReleaseNotesGenerator {
TYPE_LABELS.put("breaking", "Breaking changes");
TYPE_LABELS.put("breaking-java", "Breaking Java changes");
TYPE_LABELS.put("bug", "Bug fixes");
TYPE_LABELS.put("fixes", "Fixes");
TYPE_LABELS.put("deprecation", "Deprecations");
TYPE_LABELS.put("enhancement", "Enhancements");
TYPE_LABELS.put("feature", "New features");
TYPE_LABELS.put("features-enhancements", "Features and enhancements");
TYPE_LABELS.put("new-aggregation", "New aggregation");
TYPE_LABELS.put("regression", "Regressions");
TYPE_LABELS.put("upgrade", "Upgrades");
}
static void update(File templateFile, File outputFile, QualifiedVersion version, Set<ChangelogEntry> changelogs) throws IOException {
/**
* These are the types of changes that are considered "Features and Enhancements" in the release notes.
*/
private static final List<String> FEATURE_ENHANCEMENT_TYPES = List.of("feature", "new-aggregation", "enhancement", "upgrade");
static void update(File templateFile, File outputFile, List<ChangelogBundle> bundles) throws IOException {
final String templateString = Files.readString(templateFile.toPath());
try (FileWriter output = new FileWriter(outputFile)) {
output.write(generateFile(templateString, version, changelogs));
output.write(generateFile(templateString, bundles));
}
}
@VisibleForTesting
static String generateFile(String template, QualifiedVersion version, Set<ChangelogEntry> changelogs) throws IOException {
final var changelogsByTypeByArea = buildChangelogBreakdown(changelogs);
static String generateFile(String template, List<ChangelogBundle> bundles) throws IOException {
var bundlesWrapped = new ArrayList<ChangelogsBundleWrapper>();
for (var bundle : bundles) {
var changelogs = bundle.changelogs();
final var changelogsByTypeByArea = buildChangelogBreakdown(changelogs);
final Map<Boolean, List<ChangelogEntry.Highlight>> groupedHighlights = changelogs.stream()
.map(ChangelogEntry::getHighlight)
.filter(Objects::nonNull)
.sorted(comparingInt(ChangelogEntry.Highlight::getPr))
.collect(groupingBy(ChangelogEntry.Highlight::isNotable, toList()));
final var notableHighlights = groupedHighlights.getOrDefault(true, List.of());
final var nonNotableHighlights = groupedHighlights.getOrDefault(false, List.of());
final var version = QualifiedVersion.of(bundle.version());
final var versionWithoutSeparator = version.withoutQualifier().toString().replaceAll("\\.", "");
final var wrapped = new ChangelogsBundleWrapper(
version,
bundle,
changelogsByTypeByArea,
version.withoutQualifier(),
versionWithoutSeparator,
notableHighlights,
nonNotableHighlights
);
bundlesWrapped.add(wrapped);
}
final Map<String, Object> bindings = new HashMap<>();
bindings.put("version", version);
bindings.put("changelogsByTypeByArea", changelogsByTypeByArea);
bindings.put("TYPE_LABELS", TYPE_LABELS);
bindings.put("changelogBundles", bundlesWrapped);
return TemplateUtils.render(template, bindings);
}
private static Map<String, Map<String, List<ChangelogEntry>>> buildChangelogBreakdown(Set<ChangelogEntry> changelogs) {
/**
* The new markdown release notes are grouping several of the old change types together.
* This method maps the change type that developers use in the changelogs to the new type that the release notes cares about.
*/
private static String getTypeFromEntry(ChangelogEntry entry) {
if (entry.getBreaking() != null) {
return "breaking";
}
if (FEATURE_ENHANCEMENT_TYPES.contains(entry.getType())) {
return "features-enhancements";
}
if (entry.getType().equals("bug")) {
return "fixes";
}
return entry.getType();
}
private static Map<String, Map<String, List<ChangelogEntry>>> buildChangelogBreakdown(Collection<ChangelogEntry> changelogs) {
Map<String, Map<String, List<ChangelogEntry>>> changelogsByTypeByArea = changelogs.stream()
.collect(
groupingBy(
// Entries with breaking info are always put in the breaking section
entry -> entry.getBreaking() == null ? entry.getType() : "breaking",
entry -> getTypeFromEntry(entry),
TreeMap::new,
// Group changelogs for each type by their team area
groupingBy(

View file

@ -1,54 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.release;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import static java.util.Comparator.reverseOrder;
/**
* This class ensures that the release notes index page has the appropriate anchors and include directives
* for the current repository version.
*/
public class ReleaseNotesIndexGenerator {
static void update(Set<QualifiedVersion> versions, File indexTemplate, File indexFile) throws IOException {
try (FileWriter indexFileWriter = new FileWriter(indexFile)) {
indexFileWriter.write(generateFile(versions, Files.readString(indexTemplate.toPath())));
}
}
@VisibleForTesting
static String generateFile(Set<QualifiedVersion> versionsSet, String template) throws IOException {
final Set<QualifiedVersion> versions = new TreeSet<>(reverseOrder());
// For the purpose of generating the index, snapshot versions are the same as released versions. Prerelease versions are not.
versionsSet.stream().map(v -> v.isSnapshot() ? v.withoutQualifier() : v).forEach(versions::add);
final List<String> includeVersions = versions.stream().map(QualifiedVersion::toString).collect(Collectors.toList());
final Map<String, Object> bindings = new HashMap<>();
bindings.put("versions", versions);
bindings.put("includeVersions", includeVersions);
return TemplateUtils.render(template, bindings);
}
}

View file

@ -23,7 +23,6 @@ import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.util.PatternSet;
import java.io.File;
import java.util.function.Function;
import javax.inject.Inject;
@ -55,9 +54,9 @@ public class ReleaseToolsPlugin implements Plugin<Project> {
project.getTasks().register("tagVersions", TagVersionsTask.class);
project.getTasks().register("setCompatibleVersions", SetCompatibleVersionsTask.class, t -> t.setThisVersion(version));
final FileTree yamlFiles = projectDirectory.dir("docs/changelog")
.getAsFileTree()
.matching(new PatternSet().include("**/*.yml", "**/*.yaml"));
final Directory changeLogDirectory = projectDirectory.dir("docs/changelog");
final Directory changeLogBundlesDirectory = projectDirectory.dir("docs/release-notes/changelog-bundles");
final FileTree yamlFiles = changeLogDirectory.getAsFileTree().matching(new PatternSet().include("**/*.yml", "**/*.yaml"));
final Provider<ValidateYamlAgainstSchemaTask> validateChangelogsTask = project.getTasks()
.register("validateChangelogs", ValidateYamlAgainstSchemaTask.class, task -> {
@ -68,49 +67,41 @@ public class ReleaseToolsPlugin implements Plugin<Project> {
task.setReport(new File(project.getBuildDir(), "reports/validateYaml.txt"));
});
final Function<Boolean, Action<GenerateReleaseNotesTask>> configureGenerateTask = shouldConfigureYamlFiles -> task -> {
final Action<BundleChangelogsTask> configureBundleTask = task -> {
task.setGroup("Documentation");
if (shouldConfigureYamlFiles) {
task.setChangelogs(yamlFiles);
task.setDescription("Generates release notes from changelog files held in this checkout");
} else {
task.setDescription("Generates stub release notes e.g. after feature freeze");
}
task.setDescription("Generates release notes from changelog files held in this checkout");
task.setChangelogs(yamlFiles);
task.setChangelogDirectory(changeLogDirectory);
task.setChangelogBundlesDirectory(changeLogBundlesDirectory);
task.setBundleFile(projectDirectory.file("docs/release-notes/changelogs-" + version.toString() + ".yml"));
task.getOutputs().upToDateWhen(o -> false);
};
task.setReleaseNotesIndexTemplate(projectDirectory.file(RESOURCES + "templates/release-notes-index.asciidoc"));
task.setReleaseNotesIndexFile(projectDirectory.file("docs/reference/release-notes.asciidoc"));
final Action<GenerateReleaseNotesTask> configureGenerateTask = task -> {
task.setGroup("Documentation");
task.setDescription("Generates release notes for all versions/branches using changelog bundles in this checkout");
task.setReleaseNotesTemplate(projectDirectory.file(RESOURCES + "templates/release-notes.asciidoc"));
task.setReleaseNotesFile(
projectDirectory.file(
String.format(
"docs/reference/release-notes/%d.%d.%d.asciidoc",
version.getMajor(),
version.getMinor(),
version.getRevision()
)
)
);
task.setReleaseNotesTemplate(projectDirectory.file(RESOURCES + "templates/index.md"));
task.setReleaseNotesFile(projectDirectory.file("docs/release-notes/index.md"));
task.setReleaseHighlightsTemplate(projectDirectory.file(RESOURCES + "templates/release-highlights.asciidoc"));
task.setReleaseHighlightsFile(projectDirectory.file("docs/reference/release-notes/highlights.asciidoc"));
task.setBreakingChangesTemplate(projectDirectory.file(RESOURCES + "templates/breaking-changes.asciidoc"));
task.setBreakingChangesMigrationFile(
projectDirectory.file(
String.format("docs/reference/migration/migrate_%d_%d.asciidoc", version.getMajor(), version.getMinor())
)
);
task.setMigrationIndexTemplate(projectDirectory.file(RESOURCES + "templates/migration-index.asciidoc"));
task.setMigrationIndexFile(projectDirectory.file("docs/reference/migration/index.asciidoc"));
task.setBreakingChangesTemplate(projectDirectory.file(RESOURCES + "templates/breaking-changes.md"));
task.setBreakingChangesFile(projectDirectory.file("docs/release-notes/breaking-changes.md"));
task.setDeprecationsTemplate(projectDirectory.file(RESOURCES + "templates/deprecations.md"));
task.setDeprecationsFile(projectDirectory.file("docs/release-notes/deprecations.md"));
task.setChangelogBundleDirectory(changeLogBundlesDirectory);
task.getOutputs().upToDateWhen(o -> false);
task.dependsOn(validateChangelogsTask);
};
project.getTasks().register("generateReleaseNotes", GenerateReleaseNotesTask.class).configure(configureGenerateTask.apply(true));
project.getTasks()
.register("generateStubReleaseNotes", GenerateReleaseNotesTask.class)
.configure(configureGenerateTask.apply(false));
project.getTasks().register("bundleChangelogs", BundleChangelogsTask.class).configure(configureBundleTask);
project.getTasks().register("generateReleaseNotes", GenerateReleaseNotesTask.class).configure(configureGenerateTask);
project.getTasks().register("pruneChangelogs", PruneChangelogsTask.class).configure(task -> {
task.setGroup("Documentation");

View file

@ -1,98 +0,0 @@
[[migrating-${majorDotMinor}]]
== Migrating to ${majorDotMinor}
++++
<titleabbrev>${majorDotMinor}</titleabbrev>
++++
This section discusses the changes that you need to be aware of when migrating
your application to {es} ${majorDotMinor}.
See also <<release-highlights>> and <<es-release-notes>>.
<% if (isElasticsearchSnapshot) { %>
coming::[${majorDotMinorDotRevision}]
<% } %>
[discrete]
[[breaking-changes-${majorDotMinor}]]
=== Breaking changes
<% if (breakingByNotabilityByArea.isEmpty()) { %>
There are no breaking changes in {es} ${majorDotMinor}.
<% } else { %>
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
to mitigate the impact.
<%
if (breakingByNotabilityByArea.getOrDefault(true, []).isEmpty()) { %>
There are no notable breaking changes in {es} ${majorDotMinor}.
But there are some less critical breaking changes.
<% }
[true, false].each { isNotable ->
def breakingByArea = breakingByNotabilityByArea.getOrDefault(isNotable, [])
if (breakingByArea.isEmpty() == false) {
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 (deprecationsByNotabilityByArea.isEmpty() == false) { %>
[discrete]
[[deprecated-${majorDotMinor}]]
=== Deprecations
The following functionality has been deprecated in {es} ${majorDotMinor}
and will be removed in a future version.
While this won't have an immediate impact on your applications,
we strongly encourage you to take the described steps to update your code
after upgrading to ${majorDotMinor}.
To find out if you are using any deprecated functionality,
enable <<deprecation-logging, deprecation logging>>.
<%
[true, false].each { isNotable ->
def deprecationsByArea = deprecationsByNotabilityByArea.getOrDefault(isNotable, [])
if (deprecationsByArea.isEmpty() == false) {
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.details.trim()}
*Impact* +
${deprecation.impact.trim()}
====
<%
}
}
}
}
} %>

View file

@ -0,0 +1,50 @@
---
navigation_title: "Breaking changes"
mapped_pages:
- https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking-changes.html
---
# Elasticsearch breaking changes [elasticsearch-breaking-changes]
Breaking changes can impact your Elastic applications, potentially disrupting normal operations. Before you upgrade, carefully review the Elasticsearch breaking changes and take the necessary steps to mitigate any issues.
If you are migrating from a version prior to version 9.0, you must first upgrade to the last 8.x version available. To learn how to upgrade, check out [Upgrade](docs-content://deploy-manage/upgrade.md).
% ## Next version [elasticsearch-nextversion-breaking-changes]
<%
for(bundle in changelogBundles) {
def version = bundle.version
def versionForIds = bundle.version.toString().equals('9.0.0') ? bundle.versionWithoutSeparator : bundle.version
def changelogsByTypeByArea = bundle.changelogsByTypeByArea
def unqualifiedVersion = bundle.unqualifiedVersion
def coming = !bundle.bundle.released
if (coming) {
print "\n"
print "```{applies_to}\n"
print "stack: coming ${version}\n"
print "```"
}
%>
## ${unqualifiedVersion} [elasticsearch-${versionForIds}-breaking-changes]
<%
if (!changelogsByTypeByArea['breaking']) {
print "\nNo breaking changes in this version.\n"
} else {
for (team in (changelogsByTypeByArea['breaking'] ?: [:]).keySet()) {
print "\n${team}:\n";
for (change in changelogsByTypeByArea['breaking'][team]) {
print "* ${change.summary} [#${change.pr}](https://github.com/elastic/elasticsearch/pull/${change.pr})"
if (change.issues != null && change.issues.empty == false) {
print change.issues.size() == 1 ? " (issue: " : " (issues: "
print change.issues.collect { "[#${it}](https://github.com/elastic/elasticsearch/issues/${it})" }.join(", ")
print ")"
}
print "\n"
}
}
print "\n\n"
}
}

View file

@ -0,0 +1,53 @@
---
navigation_title: "Deprecations"
---
# {{es}} deprecations [elasticsearch-deprecations]
Over time, certain Elastic functionality becomes outdated and is replaced or removed. To help with the transition, Elastic deprecates functionality for a period before removal, giving you time to update your applications.
Review the deprecated functionality for Elasticsearch. While deprecations have no immediate impact, we strongly encourage you update your implementation after you upgrade. To learn how to upgrade, check out [Upgrade](docs-content://deploy-manage/upgrade.md).
To give you insight into what deprecated features youre using, {{es}}:
* Returns a `Warn` HTTP header whenever you submit a request that uses deprecated functionality.
* [Logs deprecation warnings](docs-content://deploy-manage/monitor/logging-configuration/update-elasticsearch-logging-levels.md#deprecation-logging) when deprecated functionality is used.
* [Provides a deprecation info API](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-migration-deprecations) that scans a clusters configuration and mappings for deprecated functionality.
% ## Next version [elasticsearch-nextversion-deprecations]
<%
for(bundle in changelogBundles) {
def version = bundle.version
def versionForIds = bundle.version.toString().equals('9.0.0') ? bundle.versionWithoutSeparator : bundle.version
def changelogsByTypeByArea = bundle.changelogsByTypeByArea
def unqualifiedVersion = bundle.unqualifiedVersion
def coming = !bundle.bundle.released
if (coming) {
print "\n"
print "```{applies_to}\n"
print "stack: coming ${version}\n"
print "```"
}
%>
## ${unqualifiedVersion} [elasticsearch-${versionForIds}-deprecations]
<%
if (!changelogsByTypeByArea['deprecation']) {
print "\nNo deprecations in this version.\n"
} else {
for (team in (changelogsByTypeByArea['deprecation'] ?: [:]).keySet()) {
print "\n${team}:\n";
for (change in changelogsByTypeByArea['deprecation'][team]) {
print "* ${change.summary} [#${change.pr}](https://github.com/elastic/elasticsearch/pull/${change.pr})"
if (change.issues != null && change.issues.empty == false) {
print change.issues.size() == 1 ? " (issue: " : " (issues: "
print change.issues.collect { "[#${it}](https://github.com/elastic/elasticsearch/issues/${it})" }.join(", ")
print ")"
}
print "\n"
}
}
print "\n\n"
}
}

View file

@ -0,0 +1,76 @@
---
navigation_title: "Elasticsearch"
mapped_pages:
- https://www.elastic.co/guide/en/elasticsearch/reference/current/es-release-notes.html
---
# Elasticsearch release notes [elasticsearch-release-notes]
Review the changes, fixes, and more in each version of Elasticsearch.
To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
% ## version.next [elasticsearch-next-release-notes]
% ### Features and enhancements [elasticsearch-next-features-enhancements]
% *
% ### Fixes [elasticsearch-next-fixes]
% *
<%
for(bundle in changelogBundles) {
def version = bundle.version
def versionForIds = bundle.version.toString().equals('9.0.0') ? bundle.versionWithoutSeparator : bundle.version
def changelogsByTypeByArea = bundle.changelogsByTypeByArea
def notableHighlights = bundle.notableHighlights
def nonNotableHighlights = bundle.nonNotableHighlights
def unqualifiedVersion = bundle.unqualifiedVersion
def coming = !bundle.bundle.released
if (coming) {
print "\n"
print "```{applies_to}\n"
print "stack: coming ${version}\n"
print "```"
}
%>
## ${unqualifiedVersion} [elasticsearch-${versionForIds}-release-notes]
<%
if (!notableHighlights.isEmpty() || !nonNotableHighlights.isEmpty()) {
print "\n### Highlights [elasticsearch-${versionForIds}-highlights]\n"
}
for (highlights in [notableHighlights, nonNotableHighlights]) {
if (!highlights.isEmpty()) {
for (highlight in highlights) { %>
::::{dropdown} ${highlight.title}
${highlight.body.trim()}
::::
<% }
}
}
for (changeType in ['features-enhancements', 'fixes', 'regression']) {
if (changelogsByTypeByArea[changeType] == null || changelogsByTypeByArea[changeType].empty) {
continue;
}
%>
### ${ TYPE_LABELS.getOrDefault(changeType, 'No mapping for TYPE_LABELS[' + changeType + ']') } [elasticsearch-${versionForIds}-${changeType}]
<% for (team in changelogsByTypeByArea[changeType].keySet()) {
print "\n${team}:\n";
for (change in changelogsByTypeByArea[changeType][team]) {
print "* ${change.summary} [#${change.pr}](https://github.com/elastic/elasticsearch/pull/${change.pr})"
if (change.issues != null && change.issues.empty == false) {
print change.issues.size() == 1 ? " (issue: " : " (issues: "
print change.issues.collect { "[#${it}](https://github.com/elastic/elasticsearch/issues/${it})" }.join(", ")
print ")"
}
print "\n"
}
}
}
print "\n"
}

View file

@ -1,12 +0,0 @@
[[es-release-notes]]
= Release notes
[partintro]
--
This section summarizes the changes in each release.
<% versions.each { print "* <<release-notes-${ it }>>\n" } %>
--
<% includeVersions.each { print "include::release-notes/${ it }.asciidoc[]\n" } %>

View file

@ -1,45 +0,0 @@
<%
def unqualifiedVersion = version.withoutQualifier()
%>[[release-notes-$unqualifiedVersion]]
== {es} version ${unqualifiedVersion}
<% if (version.isSnapshot()) { %>
coming[$unqualifiedVersion]
<% } %>
Also see <<breaking-changes-${ version.major }.${ version.minor },Breaking changes in ${ version.major }.${ version.minor }>>.
<% if (changelogsByTypeByArea["security"] != null) { %>
[discrete]
[[security-updates-${unqualifiedVersion}]]
=== Security updates
<% for (change in changelogsByTypeByArea.remove("security").remove("_all_")) {
print "* ${change.summary}\n"
}
}
if (changelogsByTypeByArea["known-issue"] != null) { %>
[discrete]
[[known-issues-${unqualifiedVersion}]]
=== Known issues
<% for (change in changelogsByTypeByArea.remove("known-issue").remove("_all_")) {
print "* ${change.summary}\n"
}
}
for (changeType in changelogsByTypeByArea.keySet()) { %>
[[${ changeType }-${ unqualifiedVersion }]]
[float]
=== ${ TYPE_LABELS.getOrDefault(changeType, 'No mapping for TYPE_LABELS[' + changeType + ']') }
<% for (team in changelogsByTypeByArea[changeType].keySet()) {
print "\n${team}::\n";
for (change in changelogsByTypeByArea[changeType][team]) {
print "* ${change.summary} {es-pull}${change.pr}[#${change.pr}]"
if (change.issues != null && change.issues.empty == false) {
print change.issues.size() == 1 ? " (issue: " : " (issues: "
print change.issues.collect { "{es-issue}${it}[#${it}]" }.join(", ")
print ")"
}
print "\n"
}
}
}
print "\n\n"

View file

@ -1,130 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.release;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
public class BreakingChangesGeneratorTest {
/**
* Check that the breaking changes can be correctly generated.
*/
@Test
public void generateIndexFile_rendersCorrectMarkup() throws Exception {
// given:
final String template = getResource("/templates/breaking-changes.asciidoc");
final String expectedOutput = getResource(
"/org/elasticsearch/gradle/internal/release/BreakingChangesGeneratorTest.generateMigrationFile.asciidoc"
);
final List<ChangelogEntry> entries = getEntries();
// when:
final String actualOutput = BreakingChangesGenerator.generateMigrationFile(
QualifiedVersion.of("8.4.0-SNAPSHOT"),
template,
entries
);
// then:
assertThat(actualOutput, equalTo(expectedOutput));
}
private List<ChangelogEntry> getEntries() {
ChangelogEntry entry1 = new ChangelogEntry();
ChangelogEntry.Breaking breaking1 = new ChangelogEntry.Breaking();
entry1.setBreaking(breaking1);
breaking1.setNotable(true);
breaking1.setTitle("Breaking change number 1");
breaking1.setArea("API");
breaking1.setDetails("Breaking change details 1");
breaking1.setImpact("Breaking change impact description 1");
ChangelogEntry entry2 = new ChangelogEntry();
ChangelogEntry.Breaking breaking2 = new ChangelogEntry.Breaking();
entry2.setBreaking(breaking2);
breaking2.setNotable(true);
breaking2.setTitle("Breaking change number 2");
breaking2.setArea("Cluster and node setting");
breaking2.setDetails("Breaking change details 2");
breaking2.setImpact("Breaking change impact description 2");
ChangelogEntry entry3 = new ChangelogEntry();
ChangelogEntry.Breaking breaking3 = new ChangelogEntry.Breaking();
entry3.setBreaking(breaking3);
breaking3.setNotable(false);
breaking3.setTitle("Breaking change number 3");
breaking3.setArea("Transform");
breaking3.setDetails("Breaking change details 3");
breaking3.setImpact("Breaking change impact description 3");
ChangelogEntry entry4 = new ChangelogEntry();
ChangelogEntry.Breaking breaking4 = new ChangelogEntry.Breaking();
entry4.setBreaking(breaking4);
breaking4.setNotable(true);
breaking4.setTitle("Breaking change number 4");
breaking4.setArea("Cluster and node setting");
breaking4.setDetails("Breaking change details 4");
breaking4.setImpact("Breaking change impact description 4");
breaking4.setEssSettingChange(true);
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 {
return Files.readString(Paths.get(Objects.requireNonNull(this.getClass().getResource(name)).toURI()), StandardCharsets.UTF_8);
}
}

View file

@ -9,38 +9,12 @@
package org.elasticsearch.gradle.internal.release;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.is;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class GenerateReleaseNotesTaskTest {
private GitWrapper gitWrapper;
@Before
public void setup() {
this.gitWrapper = mock(GitWrapper.class);
}
/**
* Check that the task does not update git tags if the current version is a snapshot of the first patch release.
*/
@ -88,250 +62,4 @@ public class GenerateReleaseNotesTaskTest {
public void needsGitTags_withLaterAlphaRelease_returnsFalse() {
assertThat(GenerateReleaseNotesTask.needsGitTags("8.0.0-alpha2"), is(true));
}
/**
* Check that partitioning changelog files when the current version is a snapshot returns a map with a single entry.
*/
@Test
public void partitionFiles_withSnapshot_returnsSingleMapping() {
// when:
Map<QualifiedVersion, Set<File>> partitionedFiles = GenerateReleaseNotesTask.partitionFilesByVersion(
gitWrapper,
"8.0.0-SNAPSHOT",
Set.of(new File("docs/changelog/1234.yaml"))
);
// then:
assertThat(partitionedFiles, aMapWithSize(1));
assertThat(
partitionedFiles,
hasEntry(equalTo(QualifiedVersion.of("8.0.0-SNAPSHOT")), hasItem(new File("docs/changelog/1234.yaml")))
);
verifyNoMoreInteractions(gitWrapper);
}
/**
* Check that partitioning changelog files when the current version is the first release
* in a minor series returns a map with a single entry.
*/
@Test
public void partitionFiles_withFirstRevision_returnsSingleMapping() {
// when:
Map<QualifiedVersion, Set<File>> partitionedFiles = GenerateReleaseNotesTask.partitionFilesByVersion(
gitWrapper,
"8.5.0",
Set.of(new File("docs/changelog/1234.yaml"))
);
// then:
assertThat(partitionedFiles, aMapWithSize(1));
assertThat(partitionedFiles, hasEntry(equalTo(QualifiedVersion.of("8.5.0")), hasItem(new File("docs/changelog/1234.yaml"))));
verifyNoMoreInteractions(gitWrapper);
}
/**
* Check that partitioning changelog files when the current version is the first alpha prerelease returns a map with a single entry.
*/
@Test
public void partitionFiles_withFirstAlpha_returnsSingleMapping() {
// when:
Map<QualifiedVersion, Set<File>> partitionedFiles = GenerateReleaseNotesTask.partitionFilesByVersion(
gitWrapper,
"8.0.0-alpha1",
Set.of(new File("docs/changelog/1234.yaml"))
);
// then:
assertThat(partitionedFiles, aMapWithSize(1));
assertThat(partitionedFiles, hasEntry(equalTo(QualifiedVersion.of("8.0.0-alpha1")), hasItem(new File("docs/changelog/1234.yaml"))));
verifyNoMoreInteractions(gitWrapper);
}
/**
* Check that when deriving a lit of versions from git tags, the current unreleased version is included.
*/
@Test
public void getVersions_includesCurrentAndPreviousVersion() {
// 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", "8.0.1", "8.1.0")
.map(QualifiedVersion::of)
);
// when:
Set<QualifiedVersion> versions = GenerateReleaseNotesTask.getVersions(gitWrapper, "8.3.0-SNAPSHOT");
// then:
assertThat(
versions,
containsInAnyOrder(
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",
"8.2.0",
"8.3.0-SNAPSHOT"
).map(QualifiedVersion::of).toArray(QualifiedVersion[]::new)
)
);
}
/**
* Check that when deriving a list of major.minor versions from git tags, the current unreleased version is included,
* but any higher version numbers are not.
*/
@Test
public void getMinorVersions_includesCurrentButNotFutureVersions() {
// given:
when(gitWrapper.listVersions(anyString())).thenReturn(
Stream.of("8.0.0-alpha1", "8.0.0-alpha2", "8.0.0", "8.0.1", "8.1.0", "8.2.0", "8.2.1", "8.3.0", "8.3.1", "8.4.0")
.map(QualifiedVersion::of)
);
// when:
Set<QualifiedVersion> versions = GenerateReleaseNotesTask.getVersions(gitWrapper, "8.3.0-SNAPSHOT");
Set<MinorVersion> minorVersions = GenerateReleaseNotesTask.getMinorVersions(versions);
// then:
assertThat(
minorVersions,
containsInAnyOrder(new MinorVersion(8, 0), new MinorVersion(8, 1), new MinorVersion(8, 2), new MinorVersion(8, 3))
);
}
/**
* Check that the task partitions the list of files correctly by version for a prerelease.
*/
@Test
public void partitionFiles_withPrerelease_correctlyGroupsByPrereleaseVersion() {
// 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")
.map(QualifiedVersion::of)
);
when(gitWrapper.listFiles(eq("v8.0.0-alpha1"), anyString())).thenReturn(
Stream.of("docs/changelog/1_1234.yaml", "docs/changelog/1_5678.yaml")
);
when(gitWrapper.listFiles(eq("v8.0.0-alpha2"), anyString())).thenReturn(
Stream.of("docs/changelog/2_1234.yaml", "docs/changelog/2_5678.yaml")
);
Set<File> allFiles = Set.of(
new File("docs/changelog/1_1234.yaml"),
new File("docs/changelog/1_5678.yaml"),
new File("docs/changelog/2_1234.yaml"),
new File("docs/changelog/2_5678.yaml"),
new File("docs/changelog/3_1234.yaml"),
new File("docs/changelog/3_5678.yaml")
);
// when:
Map<QualifiedVersion, Set<File>> partitionedFiles = GenerateReleaseNotesTask.partitionFilesByVersion(
gitWrapper,
"8.0.0-beta1",
allFiles
);
// then:
verify(gitWrapper).listVersions("v8.0*");
verify(gitWrapper).listFiles("v8.0.0-alpha1", "docs/changelog");
verify(gitWrapper).listFiles("v8.0.0-alpha2", "docs/changelog");
assertThat(
partitionedFiles,
allOf(
aMapWithSize(3),
hasKey(QualifiedVersion.of("8.0.0-alpha1")),
hasKey(QualifiedVersion.of("8.0.0-alpha2")),
hasKey(QualifiedVersion.of("8.0.0-beta1"))
)
);
assertThat(
partitionedFiles,
allOf(
hasEntry(
equalTo(QualifiedVersion.of("8.0.0-alpha1")),
containsInAnyOrder(new File("docs/changelog/1_1234.yaml"), new File("docs/changelog/1_5678.yaml"))
),
hasEntry(
equalTo(QualifiedVersion.of("8.0.0-alpha2")),
containsInAnyOrder(new File("docs/changelog/2_1234.yaml"), new File("docs/changelog/2_5678.yaml"))
),
hasEntry(
equalTo(QualifiedVersion.of("8.0.0-beta1")),
containsInAnyOrder(new File("docs/changelog/3_1234.yaml"), new File("docs/changelog/3_5678.yaml"))
)
)
);
}
/**
* Check that the task partitions the list of files correctly by version for a patch release.
*/
@Test
public void partitionFiles_withPatchRelease_correctlyGroupsByPatchVersion() {
// given:
when(gitWrapper.listVersions(anyString())).thenReturn(
Stream.of("8.0.0-alpha1", "8.0.0-alpha2", "8.0.0-beta1", "8.0.0-rc1", "8.0.0", "8.0.1", "8.0.2", "8.1.0")
.map(QualifiedVersion::of)
);
when(gitWrapper.listFiles(eq("v8.0.0"), anyString())).thenReturn(
Stream.of("docs/changelog/1_1234.yaml", "docs/changelog/1_5678.yaml")
);
when(gitWrapper.listFiles(eq("v8.0.1"), anyString())).thenReturn(
Stream.of("docs/changelog/2_1234.yaml", "docs/changelog/2_5678.yaml")
);
Set<File> allFiles = Set.of(
new File("docs/changelog/1_1234.yaml"),
new File("docs/changelog/1_5678.yaml"),
new File("docs/changelog/2_1234.yaml"),
new File("docs/changelog/2_5678.yaml"),
new File("docs/changelog/3_1234.yaml"),
new File("docs/changelog/3_5678.yaml")
);
// when:
Map<QualifiedVersion, Set<File>> partitionedFiles = GenerateReleaseNotesTask.partitionFilesByVersion(gitWrapper, "8.0.2", allFiles);
// then:
verify(gitWrapper).listVersions("v8.0*");
verify(gitWrapper).listFiles("v8.0.0", "docs/changelog");
verify(gitWrapper).listFiles("v8.0.1", "docs/changelog");
assertThat(
partitionedFiles,
allOf(
aMapWithSize(3),
hasKey(QualifiedVersion.of("8.0.0")),
hasKey(QualifiedVersion.of("8.0.1")),
hasKey(QualifiedVersion.of("8.0.2"))
)
);
assertThat(
partitionedFiles,
allOf(
hasEntry(
equalTo(QualifiedVersion.of("8.0.0")),
containsInAnyOrder(new File("docs/changelog/1_1234.yaml"), new File("docs/changelog/1_5678.yaml"))
),
hasEntry(
equalTo(QualifiedVersion.of("8.0.1")),
containsInAnyOrder(new File("docs/changelog/2_1234.yaml"), new File("docs/changelog/2_5678.yaml"))
),
hasEntry(
equalTo(QualifiedVersion.of("8.0.2")),
containsInAnyOrder(new File("docs/changelog/3_1234.yaml"), new File("docs/changelog/3_5678.yaml"))
)
)
);
}
}

View file

@ -1,87 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.release;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import static org.hamcrest.Matchers.equalTo;
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.
*/
@Test
public void generateFile_rendersCorrectMarkup() throws Exception {
// given:
final String template = getResource("/templates/release-highlights.asciidoc");
final String expectedOutput = getResource(
"/org/elasticsearch/gradle/internal/release/ReleaseHighlightsGeneratorTest.generateFile.asciidoc"
);
final List<ChangelogEntry> entries = getEntries();
// when:
final String actualOutput = ReleaseHighlightsGenerator.generateFile(QualifiedVersion.of("8.4.0-SNAPSHOT"), template, entries);
// then:
assertThat(actualOutput, equalTo(expectedOutput));
}
private List<ChangelogEntry> getEntries() {
ChangelogEntry entry123 = makeChangelogEntry(123, true);
ChangelogEntry entry456 = makeChangelogEntry(456, true);
ChangelogEntry entry789 = makeChangelogEntry(789, false);
// Return unordered list, to test correct re-ordering
return List.of(entry456, entry123, entry789);
}
private ChangelogEntry makeChangelogEntry(int pr, boolean notable) {
ChangelogEntry entry = new ChangelogEntry();
entry.setPr(pr);
ChangelogEntry.Highlight highlight = new ChangelogEntry.Highlight();
entry.setHighlight(highlight);
highlight.setNotable(notable);
highlight.setTitle("Notable release highlight number " + pr);
highlight.setBody("Notable release body number " + pr);
return entry;
}
private String getResource(String name) throws Exception {
return Files.readString(Paths.get(Objects.requireNonNull(this.getClass().getResource(name)).toURI()), StandardCharsets.UTF_8);
}
}

View file

@ -15,67 +15,115 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import static org.elasticsearch.gradle.internal.release.GenerateReleaseNotesTask.getSortedBundlesWithUniqueChangelogs;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
public class ReleaseNotesGeneratorTest {
/**
* Check that the release notes can be correctly generated.
*/
@Test
public void generateFile_rendersCorrectMarkup() throws Exception {
// given:
final String template = getResource("/templates/release-notes.asciidoc");
final String expectedOutput = getResource(
"/org/elasticsearch/gradle/internal/release/ReleaseNotesGeneratorTest.generateFile.asciidoc"
);
private static final List<String> CHANGE_TYPES = List.of(
"breaking",
"breaking-java",
"bug",
"fixes",
"deprecation",
"enhancement",
"feature",
"features-enhancements",
"new-aggregation",
"regression",
"upgrade"
);
final Set<ChangelogEntry> entries = getEntries();
@Test
public void generateFile_index_rendersCorrectMarkup() throws Exception {
testTemplate("index.md");
}
@Test
public void generateFile_index_noHighlights_rendersCorrectMarkup() throws Exception {
var bundles = getBundles();
bundles = bundles.stream().filter(b -> false == b.version().equals("9.1.0")).toList();
testTemplate("index.md", "index.no-highlights.md", bundles);
}
@Test
public void generateFile_index_noChanges_rendersCorrectMarkup() throws Exception {
var bundles = new ArrayList<ChangelogBundle>();
testTemplate("index.md", "index.no-changes.md", bundles);
}
@Test
public void generateFile_breakingChanges_rendersCorrectMarkup() throws Exception {
testTemplate("breaking-changes.md");
}
@Test
public void generateFile_deprecations_rendersCorrectMarkup() throws Exception {
testTemplate("deprecations.md");
}
public void testTemplate(String templateFilename) throws Exception {
testTemplate(templateFilename, templateFilename, null);
}
public void testTemplate(String templateFilename, String outputFilename) throws Exception {
testTemplate(templateFilename, outputFilename, null);
}
public void testTemplate(String templateFilename, String outputFilename, List<ChangelogBundle> bundles) throws Exception {
// given:
final String template = getResource("/templates/" + templateFilename);
final String expectedOutput = getResource("/org/elasticsearch/gradle/internal/release/ReleaseNotesGeneratorTest." + outputFilename);
if (bundles == null) {
bundles = getBundles();
}
bundles = getSortedBundlesWithUniqueChangelogs(bundles);
// when:
final String actualOutput = ReleaseNotesGenerator.generateFile(template, QualifiedVersion.of("8.2.0-SNAPSHOT"), entries);
final String actualOutput = ReleaseNotesGenerator.generateFile(template, bundles);
// then:
assertThat(actualOutput, equalTo(expectedOutput));
}
private Set<ChangelogEntry> getEntries() {
final Set<ChangelogEntry> entries = new HashSet<>();
entries.addAll(buildEntries(1, 2));
entries.addAll(buildEntries(2, 2));
entries.addAll(buildEntries(3, 2));
private List<ChangelogBundle> getBundles() {
List<ChangelogBundle> bundles = new ArrayList<>();
// Security issues are presented first in the notes
final ChangelogEntry securityEntry = new ChangelogEntry();
securityEntry.setArea("Security");
securityEntry.setType("security");
securityEntry.setSummary("Test security issue");
entries.add(securityEntry);
for (int i = 0; i < CHANGE_TYPES.size(); i++) {
bundles.add(
new ChangelogBundle(
"9.0." + i,
i != CHANGE_TYPES.size() - 1,
"2025-05-16T00:00:" + String.format("%d", 10 + i),
buildEntries(i, 2)
)
);
}
// known issues are presented after security issues
final ChangelogEntry knownIssue = new ChangelogEntry();
knownIssue.setArea("Search");
knownIssue.setType("known-issue");
knownIssue.setSummary("Test known issue");
entries.add(knownIssue);
final List<ChangelogEntry> entries = new ArrayList<>();
entries.add(makeHighlightsEntry(51, false));
entries.add(makeHighlightsEntry(50, true));
entries.add(makeHighlightsEntry(52, true));
return entries;
bundles.add(new ChangelogBundle("9.1.0", false, "2025-05-17T00:00:00", entries));
return bundles;
}
private List<ChangelogEntry> buildEntries(int seed, int count) {
// Sample of possible areas from `changelog-schema.json`
final List<String> areas = List.of("Aggregation", "Cluster", "Indices", "Mappings", "Search", "Security");
// Possible change types, with `breaking`, `breaking-java`, `known-issue` and `security` removed.
final List<String> types = List.of("bug", "deprecation", "enhancement", "feature", "new-aggregation", "regression", "upgrade");
final String area = areas.get(seed % areas.size());
final String type = types.get(seed % types.size());
final String type = CHANGE_TYPES.get(seed % CHANGE_TYPES.size());
final List<ChangelogEntry> entries = new ArrayList<>(count);
@ -101,6 +149,30 @@ public class ReleaseNotesGeneratorTest {
return entries;
}
private List<ChangelogEntry> getHighlightsEntries() {
ChangelogEntry entry123 = makeHighlightsEntry(123, true);
ChangelogEntry entry456 = makeHighlightsEntry(456, true);
ChangelogEntry entry789 = makeHighlightsEntry(789, false);
// Return unordered list, to test correct re-ordering
return List.of(entry456, entry123, entry789);
}
private ChangelogEntry makeHighlightsEntry(int pr, boolean notable) {
ChangelogEntry entry = new ChangelogEntry();
entry.setPr(pr);
ChangelogEntry.Highlight highlight = new ChangelogEntry.Highlight();
entry.setHighlight(highlight);
highlight.setNotable(notable);
highlight.setTitle((notable ? "[Notable] " : "") + "Release highlight number " + pr);
highlight.setBody("Release highlight body number " + pr);
entry.setType("feature");
entry.setArea("Search");
entry.setSummary("");
return entry;
}
private String getResource(String name) throws Exception {
return Files.readString(Paths.get(Objects.requireNonNull(this.getClass().getResource(name)).toURI()), StandardCharsets.UTF_8);
}

View file

@ -1,60 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
package org.elasticsearch.gradle.internal.release;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
public class ReleaseNotesIndexGeneratorTest {
/**
* Check that a release notes index can be generated.
*/
@Test
public void generateFile_rendersCorrectMarkup() throws Exception {
// given:
final Set<QualifiedVersion> versions = Stream.of(
"8.0.0-alpha1",
"8.0.0-beta2",
"8.0.0-rc3",
"8.0.0",
"8.0.1",
"8.0.2",
"8.1.0",
"8.1.1",
"8.2.0-SNAPSHOT"
).map(QualifiedVersion::of).collect(Collectors.toSet());
final String template = getResource("/templates/release-notes-index.asciidoc");
final String expectedOutput = getResource(
"/org/elasticsearch/gradle/internal/release/ReleaseNotesIndexGeneratorTest.generateFile.asciidoc"
);
// when:
final String actualOutput = ReleaseNotesIndexGenerator.generateFile(versions, template);
// then:
assertThat(actualOutput, equalTo(expectedOutput));
}
private String getResource(String name) throws Exception {
return Files.readString(Paths.get(Objects.requireNonNull(this.getClass().getResource(name)).toURI()), StandardCharsets.UTF_8);
}
}

View file

@ -1,134 +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]
[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.
[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
====
[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 to take the described steps to update your code
after upgrading to 8.4.
To find out if you are using any deprecated functionality,
enable <<deprecation-logging, deprecation logging>>.
[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
====
[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
====

View file

@ -0,0 +1,71 @@
---
navigation_title: "Breaking changes"
mapped_pages:
- https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking-changes.html
---
# Elasticsearch breaking changes [elasticsearch-breaking-changes]
Breaking changes can impact your Elastic applications, potentially disrupting normal operations. Before you upgrade, carefully review the Elasticsearch breaking changes and take the necessary steps to mitigate any issues.
If you are migrating from a version prior to version 9.0, you must first upgrade to the last 8.x version available. To learn how to upgrade, check out [Upgrade](docs-content://deploy-manage/upgrade.md).
% ## Next version [elasticsearch-nextversion-breaking-changes]
```{applies_to}
stack: coming 9.1.0
```
## 9.1.0 [elasticsearch-9.1.0-breaking-changes]
No breaking changes in this version.
```{applies_to}
stack: coming 9.0.10
```
## 9.0.10 [elasticsearch-9.0.10-breaking-changes]
No breaking changes in this version.
## 9.0.9 [elasticsearch-9.0.9-breaking-changes]
No breaking changes in this version.
## 9.0.8 [elasticsearch-9.0.8-breaking-changes]
No breaking changes in this version.
## 9.0.7 [elasticsearch-9.0.7-breaking-changes]
No breaking changes in this version.
## 9.0.6 [elasticsearch-9.0.6-breaking-changes]
No breaking changes in this version.
## 9.0.5 [elasticsearch-9.0.5-breaking-changes]
No breaking changes in this version.
## 9.0.4 [elasticsearch-9.0.4-breaking-changes]
No breaking changes in this version.
## 9.0.3 [elasticsearch-9.0.3-breaking-changes]
No breaking changes in this version.
## 9.0.2 [elasticsearch-9.0.2-breaking-changes]
No breaking changes in this version.
## 9.0.1 [elasticsearch-9.0.1-breaking-changes]
No breaking changes in this version.
## 9.0.0 [elasticsearch-900-breaking-changes]
Aggregation:
* Test changelog entry 0_0 [#0](https://github.com/elastic/elasticsearch/pull/0) (issue: [#1](https://github.com/elastic/elasticsearch/issues/1))
* Test changelog entry 0_1 [#2](https://github.com/elastic/elasticsearch/pull/2) (issues: [#3](https://github.com/elastic/elasticsearch/issues/3), [#4](https://github.com/elastic/elasticsearch/issues/4))

View file

@ -0,0 +1,75 @@
---
navigation_title: "Deprecations"
---
# {{es}} deprecations [elasticsearch-deprecations]
Over time, certain Elastic functionality becomes outdated and is replaced or removed. To help with the transition, Elastic deprecates functionality for a period before removal, giving you time to update your applications.
Review the deprecated functionality for Elasticsearch. While deprecations have no immediate impact, we strongly encourage you update your implementation after you upgrade. To learn how to upgrade, check out [Upgrade](docs-content://deploy-manage/upgrade.md).
To give you insight into what deprecated features youre using, {{es}}:
* Returns a `Warn` HTTP header whenever you submit a request that uses deprecated functionality.
* [Logs deprecation warnings](docs-content://deploy-manage/monitor/logging-configuration/update-elasticsearch-logging-levels.md#deprecation-logging) when deprecated functionality is used.
* [Provides a deprecation info API](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-migration-deprecations) that scans a clusters configuration and mappings for deprecated functionality.
% ## Next version [elasticsearch-nextversion-deprecations]
```{applies_to}
stack: coming 9.1.0
```
## 9.1.0 [elasticsearch-9.1.0-deprecations]
No deprecations in this version.
```{applies_to}
stack: coming 9.0.10
```
## 9.0.10 [elasticsearch-9.0.10-deprecations]
No deprecations in this version.
## 9.0.9 [elasticsearch-9.0.9-deprecations]
No deprecations in this version.
## 9.0.8 [elasticsearch-9.0.8-deprecations]
No deprecations in this version.
## 9.0.7 [elasticsearch-9.0.7-deprecations]
No deprecations in this version.
## 9.0.6 [elasticsearch-9.0.6-deprecations]
No deprecations in this version.
## 9.0.5 [elasticsearch-9.0.5-deprecations]
No deprecations in this version.
## 9.0.4 [elasticsearch-9.0.4-deprecations]
Search:
* Test changelog entry 4_0 [#4000](https://github.com/elastic/elasticsearch/pull/4000) (issue: [#4001](https://github.com/elastic/elasticsearch/issues/4001))
* Test changelog entry 4_1 [#4002](https://github.com/elastic/elasticsearch/pull/4002) (issues: [#4003](https://github.com/elastic/elasticsearch/issues/4003), [#4004](https://github.com/elastic/elasticsearch/issues/4004))
## 9.0.3 [elasticsearch-9.0.3-deprecations]
No deprecations in this version.
## 9.0.2 [elasticsearch-9.0.2-deprecations]
No deprecations in this version.
## 9.0.1 [elasticsearch-9.0.1-deprecations]
No deprecations in this version.
## 9.0.0 [elasticsearch-900-deprecations]
No deprecations in this version.

View file

@ -1,44 +0,0 @@
[[release-notes-8.2.0]]
== {es} version 8.2.0
coming[8.2.0]
Also see <<breaking-changes-8.2,Breaking changes in 8.2>>.
[discrete]
[[security-updates-8.2.0]]
=== Security updates
* Test security issue
[discrete]
[[known-issues-8.2.0]]
=== Known issues
* Test known issue
[[deprecation-8.2.0]]
[float]
=== Deprecations
Cluster::
* Test changelog entry 1_0 {es-pull}1000[#1000] (issue: {es-issue}1001[#1001])
* Test changelog entry 1_1 {es-pull}1002[#1002] (issues: {es-issue}1003[#1003], {es-issue}1004[#1004])
[[enhancement-8.2.0]]
[float]
=== Enhancements
Indices::
* Test changelog entry 2_0 {es-pull}2000[#2000] (issue: {es-issue}2001[#2001])
* Test changelog entry 2_1 {es-pull}2002[#2002] (issues: {es-issue}2003[#2003], {es-issue}2004[#2004])
[[feature-8.2.0]]
[float]
=== New features
Mappings::
* Test changelog entry 3_0 {es-pull}3000[#3000] (issue: {es-issue}3001[#3001])
* Test changelog entry 3_1 {es-pull}3002[#3002] (issues: {es-issue}3003[#3003], {es-issue}3004[#3004])

View file

@ -0,0 +1,132 @@
---
navigation_title: "Elasticsearch"
mapped_pages:
- https://www.elastic.co/guide/en/elasticsearch/reference/current/es-release-notes.html
---
# Elasticsearch release notes [elasticsearch-release-notes]
Review the changes, fixes, and more in each version of Elasticsearch.
To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
% ## version.next [elasticsearch-next-release-notes]
% ### Features and enhancements [elasticsearch-next-features-enhancements]
% *
% ### Fixes [elasticsearch-next-fixes]
% *
```{applies_to}
stack: coming 9.1.0
```
## 9.1.0 [elasticsearch-9.1.0-release-notes]
### Highlights [elasticsearch-9.1.0-highlights]
::::{dropdown} [Notable] Release highlight number 50
Release highlight body number 50
::::
::::{dropdown} [Notable] Release highlight number 52
Release highlight body number 52
::::
::::{dropdown} Release highlight number 51
Release highlight body number 51
::::
### Features and enhancements [elasticsearch-9.1.0-features-enhancements]
Search:
* [#51](https://github.com/elastic/elasticsearch/pull/51)
* [#50](https://github.com/elastic/elasticsearch/pull/50)
* [#52](https://github.com/elastic/elasticsearch/pull/52)
```{applies_to}
stack: coming 9.0.10
```
## 9.0.10 [elasticsearch-9.0.10-release-notes]
### Features and enhancements [elasticsearch-9.0.10-features-enhancements]
Search:
* Test changelog entry 10_0 [#10000](https://github.com/elastic/elasticsearch/pull/10000) (issue: [#10001](https://github.com/elastic/elasticsearch/issues/10001))
* Test changelog entry 10_1 [#10002](https://github.com/elastic/elasticsearch/pull/10002) (issues: [#10003](https://github.com/elastic/elasticsearch/issues/10003), [#10004](https://github.com/elastic/elasticsearch/issues/10004))
## 9.0.9 [elasticsearch-9.0.9-release-notes]
### Regressions [elasticsearch-9.0.9-regression]
Mappings:
* Test changelog entry 9_0 [#9000](https://github.com/elastic/elasticsearch/pull/9000) (issue: [#9001](https://github.com/elastic/elasticsearch/issues/9001))
* Test changelog entry 9_1 [#9002](https://github.com/elastic/elasticsearch/pull/9002) (issues: [#9003](https://github.com/elastic/elasticsearch/issues/9003), [#9004](https://github.com/elastic/elasticsearch/issues/9004))
## 9.0.8 [elasticsearch-9.0.8-release-notes]
### Features and enhancements [elasticsearch-9.0.8-features-enhancements]
Indices:
* Test changelog entry 8_0 [#8000](https://github.com/elastic/elasticsearch/pull/8000) (issue: [#8001](https://github.com/elastic/elasticsearch/issues/8001))
* Test changelog entry 8_1 [#8002](https://github.com/elastic/elasticsearch/pull/8002) (issues: [#8003](https://github.com/elastic/elasticsearch/issues/8003), [#8004](https://github.com/elastic/elasticsearch/issues/8004))
## 9.0.7 [elasticsearch-9.0.7-release-notes]
### Features and enhancements [elasticsearch-9.0.7-features-enhancements]
Cluster:
* Test changelog entry 7_0 [#7000](https://github.com/elastic/elasticsearch/pull/7000) (issue: [#7001](https://github.com/elastic/elasticsearch/issues/7001))
* Test changelog entry 7_1 [#7002](https://github.com/elastic/elasticsearch/pull/7002) (issues: [#7003](https://github.com/elastic/elasticsearch/issues/7003), [#7004](https://github.com/elastic/elasticsearch/issues/7004))
## 9.0.6 [elasticsearch-9.0.6-release-notes]
### Features and enhancements [elasticsearch-9.0.6-features-enhancements]
Aggregation:
* Test changelog entry 6_0 [#6000](https://github.com/elastic/elasticsearch/pull/6000) (issue: [#6001](https://github.com/elastic/elasticsearch/issues/6001))
* Test changelog entry 6_1 [#6002](https://github.com/elastic/elasticsearch/pull/6002) (issues: [#6003](https://github.com/elastic/elasticsearch/issues/6003), [#6004](https://github.com/elastic/elasticsearch/issues/6004))
## 9.0.5 [elasticsearch-9.0.5-release-notes]
### Features and enhancements [elasticsearch-9.0.5-features-enhancements]
Security:
* Test changelog entry 5_0 [#5000](https://github.com/elastic/elasticsearch/pull/5000) (issue: [#5001](https://github.com/elastic/elasticsearch/issues/5001))
* Test changelog entry 5_1 [#5002](https://github.com/elastic/elasticsearch/pull/5002) (issues: [#5003](https://github.com/elastic/elasticsearch/issues/5003), [#5004](https://github.com/elastic/elasticsearch/issues/5004))
## 9.0.4 [elasticsearch-9.0.4-release-notes]
## 9.0.3 [elasticsearch-9.0.3-release-notes]
### Fixes [elasticsearch-9.0.3-fixes]
Mappings:
* Test changelog entry 3_0 [#3000](https://github.com/elastic/elasticsearch/pull/3000) (issue: [#3001](https://github.com/elastic/elasticsearch/issues/3001))
* Test changelog entry 3_1 [#3002](https://github.com/elastic/elasticsearch/pull/3002) (issues: [#3003](https://github.com/elastic/elasticsearch/issues/3003), [#3004](https://github.com/elastic/elasticsearch/issues/3004))
## 9.0.2 [elasticsearch-9.0.2-release-notes]
### Fixes [elasticsearch-9.0.2-fixes]
Indices:
* Test changelog entry 2_0 [#2000](https://github.com/elastic/elasticsearch/pull/2000) (issue: [#2001](https://github.com/elastic/elasticsearch/issues/2001))
* Test changelog entry 2_1 [#2002](https://github.com/elastic/elasticsearch/pull/2002) (issues: [#2003](https://github.com/elastic/elasticsearch/issues/2003), [#2004](https://github.com/elastic/elasticsearch/issues/2004))
## 9.0.1 [elasticsearch-9.0.1-release-notes]
## 9.0.0 [elasticsearch-900-release-notes]

View file

@ -0,0 +1,21 @@
---
navigation_title: "Elasticsearch"
mapped_pages:
- https://www.elastic.co/guide/en/elasticsearch/reference/current/es-release-notes.html
---
# Elasticsearch release notes [elasticsearch-release-notes]
Review the changes, fixes, and more in each version of Elasticsearch.
To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
% ## version.next [elasticsearch-next-release-notes]
% ### Features and enhancements [elasticsearch-next-features-enhancements]
% *
% ### Fixes [elasticsearch-next-fixes]
% *

View file

@ -0,0 +1,105 @@
---
navigation_title: "Elasticsearch"
mapped_pages:
- https://www.elastic.co/guide/en/elasticsearch/reference/current/es-release-notes.html
---
# Elasticsearch release notes [elasticsearch-release-notes]
Review the changes, fixes, and more in each version of Elasticsearch.
To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
% ## version.next [elasticsearch-next-release-notes]
% ### Features and enhancements [elasticsearch-next-features-enhancements]
% *
% ### Fixes [elasticsearch-next-fixes]
% *
```{applies_to}
stack: coming 9.0.10
```
## 9.0.10 [elasticsearch-9.0.10-release-notes]
### Features and enhancements [elasticsearch-9.0.10-features-enhancements]
Search:
* Test changelog entry 10_0 [#10000](https://github.com/elastic/elasticsearch/pull/10000) (issue: [#10001](https://github.com/elastic/elasticsearch/issues/10001))
* Test changelog entry 10_1 [#10002](https://github.com/elastic/elasticsearch/pull/10002) (issues: [#10003](https://github.com/elastic/elasticsearch/issues/10003), [#10004](https://github.com/elastic/elasticsearch/issues/10004))
## 9.0.9 [elasticsearch-9.0.9-release-notes]
### Regressions [elasticsearch-9.0.9-regression]
Mappings:
* Test changelog entry 9_0 [#9000](https://github.com/elastic/elasticsearch/pull/9000) (issue: [#9001](https://github.com/elastic/elasticsearch/issues/9001))
* Test changelog entry 9_1 [#9002](https://github.com/elastic/elasticsearch/pull/9002) (issues: [#9003](https://github.com/elastic/elasticsearch/issues/9003), [#9004](https://github.com/elastic/elasticsearch/issues/9004))
## 9.0.8 [elasticsearch-9.0.8-release-notes]
### Features and enhancements [elasticsearch-9.0.8-features-enhancements]
Indices:
* Test changelog entry 8_0 [#8000](https://github.com/elastic/elasticsearch/pull/8000) (issue: [#8001](https://github.com/elastic/elasticsearch/issues/8001))
* Test changelog entry 8_1 [#8002](https://github.com/elastic/elasticsearch/pull/8002) (issues: [#8003](https://github.com/elastic/elasticsearch/issues/8003), [#8004](https://github.com/elastic/elasticsearch/issues/8004))
## 9.0.7 [elasticsearch-9.0.7-release-notes]
### Features and enhancements [elasticsearch-9.0.7-features-enhancements]
Cluster:
* Test changelog entry 7_0 [#7000](https://github.com/elastic/elasticsearch/pull/7000) (issue: [#7001](https://github.com/elastic/elasticsearch/issues/7001))
* Test changelog entry 7_1 [#7002](https://github.com/elastic/elasticsearch/pull/7002) (issues: [#7003](https://github.com/elastic/elasticsearch/issues/7003), [#7004](https://github.com/elastic/elasticsearch/issues/7004))
## 9.0.6 [elasticsearch-9.0.6-release-notes]
### Features and enhancements [elasticsearch-9.0.6-features-enhancements]
Aggregation:
* Test changelog entry 6_0 [#6000](https://github.com/elastic/elasticsearch/pull/6000) (issue: [#6001](https://github.com/elastic/elasticsearch/issues/6001))
* Test changelog entry 6_1 [#6002](https://github.com/elastic/elasticsearch/pull/6002) (issues: [#6003](https://github.com/elastic/elasticsearch/issues/6003), [#6004](https://github.com/elastic/elasticsearch/issues/6004))
## 9.0.5 [elasticsearch-9.0.5-release-notes]
### Features and enhancements [elasticsearch-9.0.5-features-enhancements]
Security:
* Test changelog entry 5_0 [#5000](https://github.com/elastic/elasticsearch/pull/5000) (issue: [#5001](https://github.com/elastic/elasticsearch/issues/5001))
* Test changelog entry 5_1 [#5002](https://github.com/elastic/elasticsearch/pull/5002) (issues: [#5003](https://github.com/elastic/elasticsearch/issues/5003), [#5004](https://github.com/elastic/elasticsearch/issues/5004))
## 9.0.4 [elasticsearch-9.0.4-release-notes]
## 9.0.3 [elasticsearch-9.0.3-release-notes]
### Fixes [elasticsearch-9.0.3-fixes]
Mappings:
* Test changelog entry 3_0 [#3000](https://github.com/elastic/elasticsearch/pull/3000) (issue: [#3001](https://github.com/elastic/elasticsearch/issues/3001))
* Test changelog entry 3_1 [#3002](https://github.com/elastic/elasticsearch/pull/3002) (issues: [#3003](https://github.com/elastic/elasticsearch/issues/3003), [#3004](https://github.com/elastic/elasticsearch/issues/3004))
## 9.0.2 [elasticsearch-9.0.2-release-notes]
### Fixes [elasticsearch-9.0.2-fixes]
Indices:
* Test changelog entry 2_0 [#2000](https://github.com/elastic/elasticsearch/pull/2000) (issue: [#2001](https://github.com/elastic/elasticsearch/issues/2001))
* Test changelog entry 2_1 [#2002](https://github.com/elastic/elasticsearch/pull/2002) (issues: [#2003](https://github.com/elastic/elasticsearch/issues/2003), [#2004](https://github.com/elastic/elasticsearch/issues/2004))
## 9.0.1 [elasticsearch-9.0.1-release-notes]
## 9.0.0 [elasticsearch-900-release-notes]

View file

@ -1,30 +0,0 @@
[[es-release-notes]]
= Release notes
[partintro]
--
This section summarizes the changes in each release.
* <<release-notes-8.2.0>>
* <<release-notes-8.1.1>>
* <<release-notes-8.1.0>>
* <<release-notes-8.0.2>>
* <<release-notes-8.0.1>>
* <<release-notes-8.0.0>>
* <<release-notes-8.0.0-rc3>>
* <<release-notes-8.0.0-beta2>>
* <<release-notes-8.0.0-alpha1>>
--
include::release-notes/8.2.0.asciidoc[]
include::release-notes/8.1.1.asciidoc[]
include::release-notes/8.1.0.asciidoc[]
include::release-notes/8.0.2.asciidoc[]
include::release-notes/8.0.1.asciidoc[]
include::release-notes/8.0.0.asciidoc[]
include::release-notes/8.0.0-rc3.asciidoc[]
include::release-notes/8.0.0-beta2.asciidoc[]
include::release-notes/8.0.0-alpha1.asciidoc[]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,148 @@
version: 9.0.1
released: true
generated: 2025-05-06T19:39:27.474912Z
changelogs:
- pr: 125694
summary: LTR score bounding
area: Ranking
type: bug
issues: []
- pr: 125922
summary: Fix text structure NPE when fields in list have null value
area: Machine Learning
type: bug
issues: []
- pr: 126273
summary: Fix LTR rescorer with model alias
area: Ranking
type: bug
issues: []
- pr: 126310
summary: Add Issuer to failed SAML Signature validation logs when available
area: Security
type: enhancement
issues:
- 111022
- pr: 126342
summary: Enable sort optimization on float and `half_float`
area: Search
type: enhancement
issues: []
- pr: 126583
summary: Cancel expired async search task when a remote returns its results
area: CCS
type: bug
issues: []
- pr: 126605
summary: Fix equality bug in `WaitForIndexColorStep`
area: ILM+SLM
type: bug
issues: []
- pr: 126614
summary: Fix join masking eval
area: ES|QL
type: bug
issues: []
- pr: 126637
summary: Improve resiliency of `UpdateTimeSeriesRangeService`
area: TSDB
type: bug
issues: []
- pr: 126686
summary: Fix race condition in `RestCancellableNodeClient`
area: Task Management
type: bug
issues:
- 88201
- pr: 126729
summary: Use terminal reader in keystore add command
area: Infra/CLI
type: bug
issues:
- 98115
- pr: 126778
summary: Fix bbq quantization algorithm but for differently distributed components
area: Vector Search
type: bug
issues: []
- pr: 126783
summary: Fix shard size of initializing restored shard
area: Allocation
type: bug
issues:
- 105331
- pr: 126806
summary: Workaround max name limit imposed by Jackson 2.17
area: Infra/Core
type: bug
issues: []
- pr: 126850
summary: "[otel-data] Bump plugin version to release _metric_names_hash changes"
area: Data streams
type: bug
issues: []
- pr: 126852
summary: "Validation checks on paths allowed for 'files' entitlements. Restrict the paths we allow access to, forbidding plugins to specify/request entitlements for reading or writing to specific protected directories."
area: Infra/Core
type: enhancement
issues: []
- pr: 126858
summary: Leverage threadpool schedule for inference api to avoid long running thread
area: Machine Learning
type: bug
issues:
- 126853
- pr: 126884
summary: Rare terms aggregation false **positive** fix
area: Aggregations
type: bug
issues: []
- pr: 126889
summary: Rework uniquify to not use iterators
area: Infra/Core
type: bug
issues:
- 126883
- pr: 126911
summary: Fix `vec_caps` to test for OS support too (on x64)
area: Vector Search
type: bug
issues:
- 126809
- pr: 126930
summary: Adding missing `onFailure` call for Inference API start model request
area: Machine Learning
type: bug
issues: []
- pr: 126990
summary: "Fix: consider case sensitiveness differences in Windows/Unix-like filesystems for files entitlements"
area: Infra/Core
type: bug
issues:
- 127047
- pr: 127146
summary: Fix sneaky bug in single value query
area: ES|QL
type: bug
issues: []
- pr: 127225
summary: Fix count optimization with pushable union types
area: ES|QL
type: bug
issues:
- 127200
- pr: 127353
summary: Updating tika to 2.9.3
area: Ingest Node
type: upgrade
issues: []
- pr: 127414
summary: Fix npe when using source confirmed text query against missing field
area: Search
type: bug
issues: []
- pr: 127527
summary: "No, line noise isn't a valid ip"
area: ES|QL
type: bug
issues: []

View file

@ -0,0 +1,135 @@
version: 9.0.2
released: false
generated: 2025-05-22T15:14:00.768080Z
changelogs:
- pr: 126992
summary: Add missing `outbound_network` entitlement to x-pack-core
area: Infra/Core
type: bug
issues:
- 127003
- pr: 127009
summary: "ESQL: Keep `DROP` attributes when resolving field names"
area: ES|QL
type: bug
issues:
- 126418
- pr: 127337
summary: Http proxy support in JWT realm
area: Authentication
type: enhancement
issues:
- 114956
- pr: 127383
summary: Don't push down filters on the right hand side of an inlinejoin
area: ES|QL
type: bug
issues: []
- pr: 127475
summary: Remove dangling spaces wherever found
area: Security
type: bug
issues: []
- pr: 127563
summary: "ESQL: Avoid unintended attribute removal"
area: ES|QL
type: bug
issues:
- 127468
- pr: 127658
summary: Append all data to Chat Completion buffer
area: Machine Learning
type: bug
issues: []
- pr: 127687
summary: "ESQL: Fix alias removal in regex extraction with JOIN"
area: ES|QL
type: bug
issues:
- 127467
- pr: 127752
summary: Downsampling does not consider passthrough fields as dimensions
area: Downsampling
type: bug
issues:
- 125156
- pr: 127798
summary: Handle streaming request body in audit log
area: Audit
type: bug
issues: []
- pr: 127824
summary: Skip the validation when retrieving the index mode during reindexing a time series data stream
area: TSDB
type: bug
issues: []
- pr: 127856
summary: Fix services API Google Vertex AI Rerank location field requirement
area: Machine Learning
type: bug
issues: []
- pr: 127877
summary: Check hidden frames in entitlements
area: Infra/Core
type: bug
issues: []
- pr: 127921
summary: "[9.x] Revert \"Enable madvise by default for all builds\""
area: Vector Search
type: bug
issues: []
- pr: 127924
summary: Limit Replace function memory usage
area: ES|QL
type: enhancement
issues: []
- pr: 127949
summary: Ensure ordinal builder emit ordinal blocks
area: ES|QL
type: bug
issues: []
- pr: 127975
summary: Fix a bug in `significant_terms`
area: Aggregations
type: bug
issues: []
- pr: 127991
summary: Avoid nested docs in painless execute api
area: Infra/Scripting
type: bug
issues:
- 41004
- pr: 128043
summary: Make S3 custom query parameter optional
area: Snapshot/Restore
type: breaking
issues: []
breaking:
area: Cluster and node setting
title: Make S3 custom query parameter optional
details: "Earlier versions of Elasticsearch would record the purpose of each S3 API call using the `?x-purpose=` custom query parameter. This isn't believed to be necessary outside of the ECH/ECE/ECK/... managed services, and it adds rather a lot to the request logs, so with this change we make the feature optional and disabled by default."
impact: "If you wish to reinstate the old behaviour on a S3 repository, set `s3.client.${CLIENT_NAME}.add_purpose_custom_query_parameter` to `true` for the relevant client."
notable: false
essSettingChange: false
- pr: 128047
summary: Add missing entitlement to `repository-azure`
area: Snapshot/Restore
type: bug
issues:
- 128046
- pr: 128111
summary: Fix union types in CCS
area: ES|QL
type: bug
issues: []
- pr: 128153
summary: "Fix: Add `NamedWriteable` for `RuleQueryRankDoc`"
area: Relevance
type: bug
issues:
- 126071
- pr: 128161
summary: Fix system data streams incorrectly showing up in the list of template validation problems
area: Data streams
type: bug
issues: []