Generate a release notes page per patch version (#85279)

Closes #85250. The approach of generating a single asciidoc page for all
releases in a minor series makes it harder to preserve any manual edits
that we have to make. Instead, generate a page per-version. It should
make little difference to the final documentation that users see.
This commit is contained in:
Rory Hunter 2022-03-23 20:19:57 +00:00
parent fa1053e972
commit 4ec75537cd
9 changed files with 75 additions and 159 deletions

View file

@ -82,7 +82,9 @@ public class GenerateReleaseNotesTask extends DefaultTask {
@TaskAction @TaskAction
public void executeTask() throws IOException { public void executeTask() throws IOException {
if (needsGitTags(VersionProperties.getElasticsearch())) { final String currentVersion = VersionProperties.getElasticsearch();
if (needsGitTags(currentVersion)) {
findAndUpdateUpstreamRemote(gitWrapper); findAndUpdateUpstreamRemote(gitWrapper);
} }
@ -90,7 +92,7 @@ public class GenerateReleaseNotesTask extends DefaultTask {
final Map<QualifiedVersion, Set<File>> filesByVersion = partitionFilesByVersion( final Map<QualifiedVersion, Set<File>> filesByVersion = partitionFilesByVersion(
gitWrapper, gitWrapper,
VersionProperties.getElasticsearch(), currentVersion,
this.changelogs.getFiles() this.changelogs.getFiles()
); );
@ -103,7 +105,7 @@ public class GenerateReleaseNotesTask extends DefaultTask {
changelogsByVersion.put(version, entriesForVersion); changelogsByVersion.put(version, entriesForVersion);
}); });
final Set<QualifiedVersion> versions = getVersions(gitWrapper, VersionProperties.getElasticsearch()); final Set<QualifiedVersion> versions = getVersions(gitWrapper, currentVersion);
LOGGER.info("Updating release notes index..."); LOGGER.info("Updating release notes index...");
ReleaseNotesIndexGenerator.update( ReleaseNotesIndexGenerator.update(
@ -113,10 +115,12 @@ public class GenerateReleaseNotesTask extends DefaultTask {
); );
LOGGER.info("Generating release notes..."); LOGGER.info("Generating release notes...");
final QualifiedVersion qualifiedVersion = QualifiedVersion.of(currentVersion);
ReleaseNotesGenerator.update( ReleaseNotesGenerator.update(
this.releaseNotesTemplate.get().getAsFile(), this.releaseNotesTemplate.get().getAsFile(),
this.releaseNotesFile.get().getAsFile(), this.releaseNotesFile.get().getAsFile(),
changelogsByVersion qualifiedVersion,
changelogsByVersion.getOrDefault(qualifiedVersion, Set.of())
); );
LOGGER.info("Generating release highlights..."); LOGGER.info("Generating release highlights...");

View file

@ -11,6 +11,7 @@ package org.elasticsearch.gradle.internal.release;
import org.elasticsearch.gradle.Version; import org.elasticsearch.gradle.Version;
import java.util.Comparator; import java.util.Comparator;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -21,12 +22,7 @@ import java.util.regex.Pattern;
* with how {@link Version} is used in the build. It also retains any qualifier (prerelease) information, and uses * with how {@link Version} is used in the build. It also retains any qualifier (prerelease) information, and uses
* that information when comparing instances. * that information when comparing instances.
*/ */
public record QualifiedVersion( public record QualifiedVersion(int major, int minor, int revision, Qualifier qualifier) implements Comparable<QualifiedVersion> {
int major,
int minor,
int revision,
org.elasticsearch.gradle.internal.release.QualifiedVersion.Qualifier qualifier
) implements Comparable<QualifiedVersion> {
private static final Pattern pattern = Pattern.compile( private static final Pattern pattern = Pattern.compile(
"^v? (\\d+) \\. (\\d+) \\. (\\d+) (?: - (alpha\\d+ | beta\\d+ | rc\\d+ | SNAPSHOT ) )? $", "^v? (\\d+) \\. (\\d+) \\. (\\d+) (?: - (alpha\\d+ | beta\\d+ | rc\\d+ | SNAPSHOT ) )? $",
@ -56,7 +52,7 @@ public record QualifiedVersion(
@Override @Override
public String toString() { public String toString() {
return "%d.%d.%d%s".formatted(major, minor, revision, qualifier == null ? "" : "-" + qualifier); return String.format(Locale.ROOT, "%d.%d.%d%s", major, minor, revision, qualifier == null ? "" : "-" + qualifier);
} }
public boolean hasQualifier() { public boolean hasQualifier() {

View file

@ -14,7 +14,6 @@ import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -31,7 +30,7 @@ import static java.util.stream.Collectors.toList;
*/ */
public class ReleaseNotesGenerator { public class ReleaseNotesGenerator {
/** /**
* These mappings translate change types into the headings as they should appears in the release notes. * These mappings translate change types into the headings as they should appear in the release notes.
*/ */
private static final Map<String, String> TYPE_LABELS = new HashMap<>(); private static final Map<String, String> TYPE_LABELS = new HashMap<>();
@ -47,33 +46,27 @@ public class ReleaseNotesGenerator {
TYPE_LABELS.put("upgrade", "Upgrades"); TYPE_LABELS.put("upgrade", "Upgrades");
} }
static void update(File templateFile, File outputFile, Map<QualifiedVersion, Set<ChangelogEntry>> changelogs) throws IOException { static void update(File templateFile, File outputFile, QualifiedVersion version, Set<ChangelogEntry> changelogs) throws IOException {
final String templateString = Files.readString(templateFile.toPath()); final String templateString = Files.readString(templateFile.toPath());
try (FileWriter output = new FileWriter(outputFile)) { try (FileWriter output = new FileWriter(outputFile)) {
output.write(generateFile(templateString, changelogs)); output.write(generateFile(templateString, version, changelogs));
} }
} }
@VisibleForTesting @VisibleForTesting
static String generateFile(String template, Map<QualifiedVersion, Set<ChangelogEntry>> changelogs) throws IOException { static String generateFile(String template, QualifiedVersion version, Set<ChangelogEntry> changelogs) throws IOException {
final var changelogsByVersionByTypeByArea = buildChangelogBreakdown(changelogs); final var changelogsByTypeByArea = buildChangelogBreakdown(changelogs);
final Map<String, Object> bindings = new HashMap<>(); final Map<String, Object> bindings = new HashMap<>();
bindings.put("changelogsByVersionByTypeByArea", changelogsByVersionByTypeByArea); bindings.put("version", version);
bindings.put("changelogsByTypeByArea", changelogsByTypeByArea);
bindings.put("TYPE_LABELS", TYPE_LABELS); bindings.put("TYPE_LABELS", TYPE_LABELS);
return TemplateUtils.render(template, bindings); return TemplateUtils.render(template, bindings);
} }
private static Map<QualifiedVersion, Map<String, Map<String, List<ChangelogEntry>>>> buildChangelogBreakdown( private static Map<String, Map<String, List<ChangelogEntry>>> buildChangelogBreakdown(Set<ChangelogEntry> changelogs) {
Map<QualifiedVersion, Set<ChangelogEntry>> changelogsByVersion
) {
Map<QualifiedVersion, Map<String, Map<String, List<ChangelogEntry>>>> changelogsByVersionByTypeByArea = new TreeMap<>(
Comparator.reverseOrder()
);
changelogsByVersion.forEach((version, changelogs) -> {
Map<String, Map<String, List<ChangelogEntry>>> changelogsByTypeByArea = changelogs.stream() Map<String, Map<String, List<ChangelogEntry>>> changelogsByTypeByArea = changelogs.stream()
.collect( .collect(
groupingBy( groupingBy(
@ -83,27 +76,18 @@ public class ReleaseNotesGenerator {
// Group changelogs for each type by their team area // Group changelogs for each type by their team area
groupingBy( groupingBy(
// `security` and `known-issue` areas don't need to supply an area // `security` and `known-issue` areas don't need to supply an area
entry -> entry.getType().equals("known-issue") || entry.getType().equals("security") entry -> entry.getType().equals("known-issue") || entry.getType().equals("security") ? "_all_" : entry.getArea(),
? "_all_"
: entry.getArea(),
TreeMap::new, TreeMap::new,
toList() toList()
) )
) )
); );
changelogsByVersionByTypeByArea.put(version, changelogsByTypeByArea);
});
// Sort per-area changelogs by their summary text. Assumes that the underlying list is sortable // Sort per-area changelogs by their summary text. Assumes that the underlying list is sortable
changelogsByVersionByTypeByArea.forEach( changelogsByTypeByArea.forEach(
(_version, byVersion) -> byVersion.forEach( (_type, byTeam) -> byTeam.forEach((_team, changelogsForTeam) -> changelogsForTeam.sort(comparing(ChangelogEntry::getSummary)))
(_type, byTeam) -> byTeam.forEach(
(_team, changelogsForTeam) -> changelogsForTeam.sort(comparing(ChangelogEntry::getSummary))
)
)
); );
return changelogsByVersionByTypeByArea; return changelogsByTypeByArea;
} }
} }

View file

@ -43,7 +43,12 @@ public class ReleaseNotesIndexGenerator {
versionsSet.stream().map(v -> v.isSnapshot() ? v.withoutQualifier() : v).forEach(versions::add); versionsSet.stream().map(v -> v.isSnapshot() ? v.withoutQualifier() : v).forEach(versions::add);
final List<String> includeVersions = versions.stream() final List<String> includeVersions = versions.stream()
.map(v -> v.hasQualifier() ? v.toString() : v.major() + "." + v.minor()) .map(
// We didn't split up the notes for 8.0
version -> version.isBefore(QualifiedVersion.of("8.1.0")) && version.hasQualifier() == false
? version.major() + "." + version.minor()
: version.toString()
)
.distinct() .distinct()
.collect(Collectors.toList()); .collect(Collectors.toList());

View file

@ -77,7 +77,14 @@ public class ReleaseToolsPlugin implements Plugin<Project> {
task.setReleaseNotesTemplate(projectDirectory.file(RESOURCES + "templates/release-notes.asciidoc")); task.setReleaseNotesTemplate(projectDirectory.file(RESOURCES + "templates/release-notes.asciidoc"));
task.setReleaseNotesFile( task.setReleaseNotesFile(
projectDirectory.file(String.format("docs/reference/release-notes/%d.%d.asciidoc", version.getMajor(), version.getMinor())) projectDirectory.file(
String.format(
"docs/reference/release-notes/%d.%d.%d.asciidoc",
version.getMajor(),
version.getMinor(),
version.getRevision()
)
)
); );
task.setReleaseHighlightsTemplate(projectDirectory.file(RESOURCES + "templates/release-highlights.asciidoc")); task.setReleaseHighlightsTemplate(projectDirectory.file(RESOURCES + "templates/release-highlights.asciidoc"));

View file

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

View file

@ -14,10 +14,8 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@ -37,52 +35,36 @@ public class ReleaseNotesGeneratorTest {
"/org/elasticsearch/gradle/internal/release/ReleaseNotesGeneratorTest.generateFile.asciidoc" "/org/elasticsearch/gradle/internal/release/ReleaseNotesGeneratorTest.generateFile.asciidoc"
); );
final Map<QualifiedVersion, Set<ChangelogEntry>> entries = getEntries(); final Set<ChangelogEntry> entries = getEntries();
// when: // when:
final String actualOutput = ReleaseNotesGenerator.generateFile(template, entries); final String actualOutput = ReleaseNotesGenerator.generateFile(template, QualifiedVersion.of("8.2.0-SNAPSHOT"), entries);
// then: // then:
assertThat(actualOutput, equalTo(expectedOutput)); assertThat(actualOutput, equalTo(expectedOutput));
} }
private Map<QualifiedVersion, Set<ChangelogEntry>> getEntries() { private Set<ChangelogEntry> getEntries() {
final Set<ChangelogEntry> entries_8_2_0 = new HashSet<>(); final Set<ChangelogEntry> entries = new HashSet<>();
entries_8_2_0.addAll(buildEntries(1, 2)); entries.addAll(buildEntries(1, 2));
entries_8_2_0.addAll(buildEntries(2, 2)); entries.addAll(buildEntries(2, 2));
entries_8_2_0.addAll(buildEntries(3, 2)); entries.addAll(buildEntries(3, 2));
final Set<ChangelogEntry> entries_8_1_0 = new HashSet<>();
entries_8_1_0.addAll(buildEntries(4, 2));
entries_8_1_0.addAll(buildEntries(5, 2));
entries_8_1_0.addAll(buildEntries(6, 2));
final Set<ChangelogEntry> entries_8_0_0 = new HashSet<>();
entries_8_0_0.addAll(buildEntries(7, 2));
entries_8_0_0.addAll(buildEntries(8, 2));
entries_8_0_0.addAll(buildEntries(9, 2));
// Security issues are presented first in the notes // Security issues are presented first in the notes
final ChangelogEntry securityEntry = new ChangelogEntry(); final ChangelogEntry securityEntry = new ChangelogEntry();
securityEntry.setArea("Security"); securityEntry.setArea("Security");
securityEntry.setType("security"); securityEntry.setType("security");
securityEntry.setSummary("Test security issue"); securityEntry.setSummary("Test security issue");
entries_8_2_0.add(securityEntry); entries.add(securityEntry);
// known issues are presented after security issues // known issues are presented after security issues
final ChangelogEntry knownIssue = new ChangelogEntry(); final ChangelogEntry knownIssue = new ChangelogEntry();
knownIssue.setArea("Search"); knownIssue.setArea("Search");
knownIssue.setType("known-issue"); knownIssue.setType("known-issue");
knownIssue.setSummary("Test known issue"); knownIssue.setSummary("Test known issue");
entries_8_1_0.add(knownIssue); entries.add(knownIssue);
final Map<QualifiedVersion, Set<ChangelogEntry>> result = new HashMap<>(); return entries;
result.put(QualifiedVersion.of("8.2.0-SNAPSHOT"), entries_8_2_0);
result.put(QualifiedVersion.of("8.1.0"), entries_8_1_0);
result.put(QualifiedVersion.of("8.0.0"), entries_8_0_0);
return result;
} }
private List<ChangelogEntry> buildEntries(int seed, int count) { private List<ChangelogEntry> buildEntries(int seed, int count) {

View file

@ -11,6 +11,12 @@ Also see <<breaking-changes-8.2,Breaking changes in 8.2>>.
* Test security issue * Test security issue
[discrete]
[[known-issues-8.2.0]]
=== Known issues
* Test known issue
[[deprecation-8.2.0]] [[deprecation-8.2.0]]
[float] [float]
=== Deprecations === Deprecations
@ -36,70 +42,3 @@ Mappings::
* Test changelog entry 3_1 {es-pull}3002[#3002] (issues: {es-issue}3003[#3003], {es-issue}3004[#3004]) * Test changelog entry 3_1 {es-pull}3002[#3002] (issues: {es-issue}3003[#3003], {es-issue}3004[#3004])
[[release-notes-8.1.0]]
== {es} version 8.1.0
Also see <<breaking-changes-8.1,Breaking changes in 8.1>>.
[discrete]
[[known-issues-8.1.0]]
=== Known issues
* Test known issue
[[new-aggregation-8.1.0]]
[float]
=== New aggregation
Search::
* Test changelog entry 4_0 {es-pull}4000[#4000] (issue: {es-issue}4001[#4001])
* Test changelog entry 4_1 {es-pull}4002[#4002] (issues: {es-issue}4003[#4003], {es-issue}4004[#4004])
[[regression-8.1.0]]
[float]
=== Regressions
Security::
* Test changelog entry 5_0 {es-pull}5000[#5000] (issue: {es-issue}5001[#5001])
* Test changelog entry 5_1 {es-pull}5002[#5002] (issues: {es-issue}5003[#5003], {es-issue}5004[#5004])
[[upgrade-8.1.0]]
[float]
=== Upgrades
Aggregation::
* Test changelog entry 6_0 {es-pull}6000[#6000] (issue: {es-issue}6001[#6001])
* Test changelog entry 6_1 {es-pull}6002[#6002] (issues: {es-issue}6003[#6003], {es-issue}6004[#6004])
[[release-notes-8.0.0]]
== {es} version 8.0.0
Also see <<breaking-changes-8.0,Breaking changes in 8.0>>.
[[bug-8.0.0]]
[float]
=== Bug fixes
Cluster::
* Test changelog entry 7_0 {es-pull}7000[#7000] (issue: {es-issue}7001[#7001])
* Test changelog entry 7_1 {es-pull}7002[#7002] (issues: {es-issue}7003[#7003], {es-issue}7004[#7004])
[[deprecation-8.0.0]]
[float]
=== Deprecations
Indices::
* Test changelog entry 8_0 {es-pull}8000[#8000] (issue: {es-issue}8001[#8001])
* Test changelog entry 8_1 {es-pull}8002[#8002] (issues: {es-issue}8003[#8003], {es-issue}8004[#8004])
[[enhancement-8.0.0]]
[float]
=== Enhancements
Mappings::
* Test changelog entry 9_0 {es-pull}9000[#9000] (issue: {es-issue}9001[#9001])
* Test changelog entry 9_1 {es-pull}9002[#9002] (issues: {es-issue}9003[#9003], {es-issue}9004[#9004])

View file

@ -18,8 +18,9 @@ This section summarizes the changes in each release.
-- --
include::release-notes/8.2.asciidoc[] include::release-notes/8.2.0.asciidoc[]
include::release-notes/8.1.asciidoc[] include::release-notes/8.1.1.asciidoc[]
include::release-notes/8.1.0.asciidoc[]
include::release-notes/8.0.asciidoc[] include::release-notes/8.0.asciidoc[]
include::release-notes/8.0.0-rc3.asciidoc[] include::release-notes/8.0.0-rc3.asciidoc[]
include::release-notes/8.0.0-beta2.asciidoc[] include::release-notes/8.0.0-beta2.asciidoc[]