From bf25f1b28f46da06667ca3160c50c3c267fbb546 Mon Sep 17 00:00:00 2001 From: lcawl Date: Mon, 4 May 2026 15:21:16 -0700 Subject: [PATCH 1/5] Add changelog render --dropdowns --- docs/cli/changelog/render.md | 25 + .../Rendering/ChangelogRenderContext.cs | 1 + .../Rendering/ChangelogRenderingService.cs | 2 + .../BreakingChangesMarkdownRenderer.cs | 64 ++- .../Markdown/DeprecationsMarkdownRenderer.cs | 64 ++- .../Markdown/HighlightsMarkdownRenderer.cs | 33 +- .../Markdown/KnownIssuesMarkdownRenderer.cs | 64 ++- .../docs-builder/Commands/ChangelogCommand.cs | 3 + .../Changelogs/Render/DropdownRenderTests.cs | 429 ++++++++++++++++++ 9 files changed, 638 insertions(+), 47 deletions(-) create mode 100644 tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs diff --git a/docs/cli/changelog/render.md b/docs/cli/changelog/render.md index 9ad7ac343..a2c2d3ce9 100644 --- a/docs/cli/changelog/render.md +++ b/docs/cli/changelog/render.md @@ -55,6 +55,13 @@ The `render` command automatically discovers and merges `.amend-*.yaml` files wi : Defaults to false. : When enabled, entries are grouped by their area within each section. The first area from each entry's areas list is used for grouping. +`--dropdowns` +: Optional: Render separated types (breaking changes, deprecations, known issues, highlights) as MyST dropdowns. +: Defaults to false (flattened bulleted lists). +: When enabled, each entry in separated files is rendered as a collapsible dropdown section using MyST syntax (`::::{dropdown}`). +: When disabled (default), entries are rendered as flattened bulleted lists with PR/issue links inline and Impact/Action sections indented. +: This flag only affects markdown output; AsciiDoc output always uses its standard format. + `--title ` : Optional: The title to use for section headers, directories, and anchors in output files. : Defaults to the version in the first bundle. @@ -141,3 +148,21 @@ docs-builder changelog render \ --input "./public-bundle.yaml|./changelog|elasticsearch|keep-links,./private-bundle.yaml|./private-changelog|internal-repo|hide-links" \ --output ./release-notes ``` + +### Render with dropdown format + +```sh +docs-builder changelog render \ + --input "./bundles/9.3.0.yaml|./changelog|elasticsearch" \ + --dropdowns \ + --output ./release-notes +``` + +### Render with subsections and flattened format (default) + +```sh +docs-builder changelog render \ + --input "./bundles/9.3.0.yaml|./changelog|elasticsearch" \ + --subsections \ + --output ./release-notes +``` diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderContext.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderContext.cs index 4fcdfbe20..19a949aa7 100644 --- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderContext.cs +++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderContext.cs @@ -20,6 +20,7 @@ public record ChangelogRenderContext public required string Owner { get; init; } public required IReadOnlyDictionary> EntriesByType { get; init; } public required bool Subsections { get; init; } + public required bool Dropdowns { get; init; } public required HashSet FeatureIdsToHide { get; init; } public required Dictionary> EntryToBundleProducts { get; init; } public required Dictionary EntryToRepo { get; init; } diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs index 6d394a904..4be056a5d 100644 --- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs +++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs @@ -28,6 +28,7 @@ public record RenderChangelogsArguments public string? Output { get; init; } public string? Title { get; init; } public bool Subsections { get; init; } + public bool Dropdowns { get; init; } public string[]? HideFeatures { get; init; } public string? Config { get; init; } public ChangelogFileType FileType { get; init; } = ChangelogFileType.Markdown; @@ -325,6 +326,7 @@ private static ChangelogRenderContext BuildRenderContext( Owner = ownerForAnchors, EntriesByType = entriesByType, Subsections = input.Subsections, + Dropdowns = input.Dropdowns, FeatureIdsToHide = featureIdsToHide, EntryToBundleProducts = entryToBundleProducts, EntryToRepo = entryToRepo, diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs index 7cace43f7..ff8394208 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs @@ -63,22 +63,58 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(); if (shouldHide) _ = sb.AppendLine(""); } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs index 78dcf5fbd..aa917486f 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs @@ -60,22 +60,58 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(); if (shouldHide) _ = sb.AppendLine(""); } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs index f55a3c19a..bc48a9271 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs @@ -63,11 +63,34 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(); if (shouldHide) _ = sb.AppendLine(""); } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs index a8640f14b..c1bbe5c88 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs @@ -60,22 +60,58 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(); if (shouldHide) _ = sb.AppendLine(""); } diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs index e81b88a2f..74a049567 100644 --- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs +++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs @@ -1152,6 +1152,7 @@ async static (s, collector, state, ctx) => await s.RemoveChangelogs(collector, s /// Filter by feature IDs (comma-separated), or a path to a newline-delimited file containing feature IDs. Can be specified multiple times. Entries with matching feature-id values will be commented out in the output. /// Optional: Output directory for rendered files. Defaults to current directory /// Optional: Group entries by area/component in subsections. For breaking changes with a subtype, groups by subtype instead of area. Defaults to false + /// Optional: Render separated types (breaking changes, deprecations, known issues, highlights) as MyST dropdowns. When false (default), renders as flattened bulleted lists. Defaults to false /// Optional: Title to use for section headers in output files. Defaults to version from first bundle /// [Command("render")] @@ -1162,6 +1163,7 @@ public async Task Render( string[]? hideFeatures = null, string? output = null, bool subsections = false, + bool dropdowns = false, string? title = null, Cancel ctx = default ) @@ -1193,6 +1195,7 @@ public async Task Render( Output = output, Title = title, Subsections = subsections, + Dropdowns = dropdowns, HideFeatures = allFeatureIds.Count > 0 ? allFeatureIds.ToArray() : null, FileType = ft.Value, Config = config diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs new file mode 100644 index 000000000..d4ec84e2a --- /dev/null +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs @@ -0,0 +1,429 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO; +using AwesomeAssertions; +using Elastic.Changelog.Bundling; +using Elastic.Changelog.Rendering; +using Elastic.Documentation.Configuration; + +namespace Elastic.Changelog.Tests.Changelogs.Render; + +public class DropdownRenderTests(ITestOutputHelper output) : RenderChangelogTestBase(output) +{ + [Fact] + public async Task RenderChangelogs_WithDropdownsTrue_RendersDropdownFormat() + { + // Arrange + var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(changelogDir); + + // Create breaking change changelog + // language=yaml + var breakingChange = + """ + title: Breaking API change + type: breaking-change + products: + - product: elasticsearch + target: 9.2.0 + prs: + - "123" + description: API has been changed to improve performance + impact: Existing API calls will fail + action: Update your code to use the new API endpoints + """; + + var changelogFile = FileSystem.Path.Join(changelogDir, "breaking-change.yaml"); + await FileSystem.File.WriteAllTextAsync(changelogFile, breakingChange, TestContext.Current.CancellationToken); + + // Create bundle file + var bundleFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml"); + FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(bundleFile)!); + + // language=yaml + var bundleContent = + $""" + products: + - product: elasticsearch + target: 9.2.0 + entries: + - file: + name: breaking-change.yaml + checksum: {ComputeSha1(breakingChange)} + """; + await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken); + + var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + + var input = new RenderChangelogsArguments + { + Bundles = [new BundleInput { BundleFile = bundleFile, Directory = changelogDir }], + Output = outputDir, + Title = "9.2.0", + Dropdowns = true + }; + + // Act + var result = await Service.RenderChangelogs(Collector, input, TestContext.Current.CancellationToken); + + // Assert + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + var breakingChangesFile = FileSystem.Path.Join(outputDir, "9.2.0", "breaking-changes.md"); + FileSystem.File.Exists(breakingChangesFile).Should().BeTrue(); + + var content = await FileSystem.File.ReadAllTextAsync(breakingChangesFile, TestContext.Current.CancellationToken); + + // Verify dropdown format + content.Should().Contain("::::{dropdown} Breaking API change"); + content.Should().Contain("API has been changed to improve performance"); + content.Should().Contain("**Impact**
Existing API calls will fail"); + content.Should().Contain("**Action**
Update your code to use the new API endpoints"); + content.Should().Contain("::::"); + } + + [Fact] + public async Task RenderChangelogs_WithDropdownsFalse_RendersFlattendFormat() + { + // Arrange + var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(changelogDir); + + // Create deprecation changelog + // language=yaml + var deprecation = + """ + title: Deprecated old API + type: deprecation + products: + - product: elasticsearch + target: 9.2.0 + prs: + - "456" + issues: + - "789" + description: The old API is deprecated + impact: API will be removed in future version + action: Migrate to the new API + """; + + var changelogFile = FileSystem.Path.Join(changelogDir, "deprecation.yaml"); + await FileSystem.File.WriteAllTextAsync(changelogFile, deprecation, TestContext.Current.CancellationToken); + + // Create bundle file + var bundleFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml"); + FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(bundleFile)!); + + // language=yaml + var bundleContent = + $""" + products: + - product: elasticsearch + target: 9.2.0 + entries: + - file: + name: deprecation.yaml + checksum: {ComputeSha1(deprecation)} + """; + await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken); + + var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + + var input = new RenderChangelogsArguments + { + Bundles = [new BundleInput { BundleFile = bundleFile, Directory = changelogDir }], + Output = outputDir, + Title = "9.2.0", + Dropdowns = false // Explicitly set to false for clarity + }; + + // Act + var result = await Service.RenderChangelogs(Collector, input, TestContext.Current.CancellationToken); + + // Assert + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + var deprecationsFile = FileSystem.Path.Join(outputDir, "9.2.0", "deprecations.md"); + FileSystem.File.Exists(deprecationsFile).Should().BeTrue(); + + var content = await FileSystem.File.ReadAllTextAsync(deprecationsFile, TestContext.Current.CancellationToken); + + // Verify flattened format + content.Should().Contain("* Deprecated old API"); + content.Should().Contain("The old API is deprecated"); + content.Should().Contain("For more information, check"); + content.Should().Contain("#456"); + content.Should().Contain("#789"); + content.Should().Contain("**Impact:** API will be removed in future version"); + content.Should().Contain("**Action:** Migrate to the new API"); + + // Should NOT contain dropdown syntax + content.Should().NotContain("::::{dropdown}"); + content.Should().NotContain("::::"); + } + + [Fact] + public async Task RenderChangelogs_DefaultDropdownsFalse_RendersFlattedFormat() + { + // Arrange + var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(changelogDir); + + // Create known issue changelog + // language=yaml + var knownIssue = + """ + title: Known issue with search + type: known-issue + products: + - product: elasticsearch + target: 9.2.0 + prs: + - "999" + description: Search results are incomplete under certain conditions + impact: Some search results may be missing + action: Use the workaround provided in the documentation + """; + + var changelogFile = FileSystem.Path.Join(changelogDir, "known-issue.yaml"); + await FileSystem.File.WriteAllTextAsync(changelogFile, knownIssue, TestContext.Current.CancellationToken); + + // Create bundle file + var bundleFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml"); + FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(bundleFile)!); + + // language=yaml + var bundleContent = + $""" + products: + - product: elasticsearch + target: 9.2.0 + entries: + - file: + name: known-issue.yaml + checksum: {ComputeSha1(knownIssue)} + """; + await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken); + + var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + + var input = new RenderChangelogsArguments + { + Bundles = [new BundleInput { BundleFile = bundleFile, Directory = changelogDir }], + Output = outputDir, + Title = "9.2.0" + // Note: Dropdowns not set, should default to false + }; + + // Act + var result = await Service.RenderChangelogs(Collector, input, TestContext.Current.CancellationToken); + + // Assert + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + var knownIssuesFile = FileSystem.Path.Join(outputDir, "9.2.0", "known-issues.md"); + FileSystem.File.Exists(knownIssuesFile).Should().BeTrue(); + + var content = await FileSystem.File.ReadAllTextAsync(knownIssuesFile, TestContext.Current.CancellationToken); + + // Verify flattened format (default behavior) + content.Should().Contain("* Known issue with search"); + content.Should().Contain("Search results are incomplete under certain conditions"); + content.Should().Contain("For more information, check"); + content.Should().Contain("#999"); + content.Should().Contain("**Impact:** Some search results may be missing"); + content.Should().Contain("**Action:** Use the workaround provided in the documentation"); + + // Should NOT contain dropdown syntax + content.Should().NotContain("::::{dropdown}"); + content.Should().NotContain("::::"); + } + + [Fact] + public async Task RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat() + { + // Arrange + var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(changelogDir); + + // Create highlight feature + // language=yaml + var highlight = + """ + title: Amazing new feature + type: feature + highlight: true + products: + - product: elasticsearch + target: 9.2.0 + prs: + - "555" + description: This feature revolutionizes how you work with data + """; + + var changelogFile = FileSystem.Path.Join(changelogDir, "highlight.yaml"); + await FileSystem.File.WriteAllTextAsync(changelogFile, highlight, TestContext.Current.CancellationToken); + + // Create bundle file + var bundleFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml"); + FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(bundleFile)!); + + // language=yaml + var bundleContent = + $""" + products: + - product: elasticsearch + target: 9.2.0 + entries: + - file: + name: highlight.yaml + checksum: {ComputeSha1(highlight)} + """; + await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken); + + var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + + // Test both dropdown and flattened modes + var testCases = new[] + { + new { Dropdowns = true, ExpectDropdown = true }, + new { Dropdowns = false, ExpectDropdown = false } + }; + + foreach (var testCase in testCases) + { + var subOutputDir = FileSystem.Path.Join(outputDir, testCase.Dropdowns.ToString()); + + var input = new RenderChangelogsArguments + { + Bundles = [new BundleInput { BundleFile = bundleFile, Directory = changelogDir }], + Output = subOutputDir, + Title = "9.2.0", + Dropdowns = testCase.Dropdowns + }; + + // Act + var result = await Service.RenderChangelogs(Collector, input, TestContext.Current.CancellationToken); + + // Assert + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + var highlightsFile = FileSystem.Path.Join(subOutputDir, "9.2.0", "highlights.md"); + FileSystem.File.Exists(highlightsFile).Should().BeTrue(); + + var content = await FileSystem.File.ReadAllTextAsync(highlightsFile, TestContext.Current.CancellationToken); + + if (testCase.ExpectDropdown) + { + // Verify dropdown format + content.Should().Contain("::::{dropdown} Amazing new feature"); + content.Should().Contain("This feature revolutionizes how you work with data"); + content.Should().Contain("::::"); + } + else + { + // Verify flattened format + content.Should().Contain("* Amazing new feature"); + content.Should().Contain("This feature revolutionizes how you work with data"); + content.Should().Contain("For more information, check"); + content.Should().Contain("#555"); + + // Should NOT contain dropdown syntax + content.Should().NotContain("::::{dropdown}"); + content.Should().NotContain("::::"); + } + } + } + + [Fact] + public async Task RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag() + { + // Arrange + var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(changelogDir); + + // Create breaking change + // language=yaml + var breakingChange = + """ + title: Breaking API change + type: breaking-change + products: + - product: elasticsearch + target: 9.2.0 + prs: + - "123" + description: API has been changed + impact: Existing API calls will fail + action: Update your code + """; + + var changelogFile = FileSystem.Path.Join(changelogDir, "breaking-change.yaml"); + await FileSystem.File.WriteAllTextAsync(changelogFile, breakingChange, TestContext.Current.CancellationToken); + + // Create bundle file + var bundleFile = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "bundle.yaml"); + FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(bundleFile)!); + + // language=yaml + var bundleContent = + $""" + products: + - product: elasticsearch + target: 9.2.0 + entries: + - file: + name: breaking-change.yaml + checksum: {ComputeSha1(breakingChange)} + """; + await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken); + + var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + + // Test both dropdown values with AsciiDoc format + var testCases = new[] { true, false }; + + foreach (var dropdowns in testCases) + { + var subOutputDir = FileSystem.Path.Join(outputDir, dropdowns.ToString()); + + var input = new RenderChangelogsArguments + { + Bundles = [new BundleInput { BundleFile = bundleFile, Directory = changelogDir }], + Output = subOutputDir, + Title = "9.2.0", + Dropdowns = dropdowns, + FileType = ChangelogFileType.Asciidoc + }; + + // Act + var result = await Service.RenderChangelogs(Collector, input, TestContext.Current.CancellationToken); + + // Assert + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + // Find the AsciiDoc file (path structure differs from Markdown) + var asciidocFiles = FileSystem.Directory.GetFiles(subOutputDir, "*.asciidoc", SearchOption.AllDirectories); + asciidocFiles.Should().HaveCount(1, "should create exactly one AsciiDoc file"); + + var asciidocFile = asciidocFiles[0]; + var content = await FileSystem.File.ReadAllTextAsync(asciidocFile, TestContext.Current.CancellationToken); + + // AsciiDoc should always use bullet format regardless of dropdowns flag + content.Should().Contain("* Breaking API change"); + content.Should().Contain("**Impact:** Existing API calls will fail"); + content.Should().Contain("**Action:** Update your code"); + + // Should never contain MyST dropdown syntax + content.Should().NotContain("::::{dropdown}"); + content.Should().NotContain("::::"); + } + } +} From ef2849ffa4a8432f64ec6d298385b13e13b9ca85 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 6 May 2026 14:00:36 -0700 Subject: [PATCH 2/5] Update changelog render date handling (#3260) --- docs/cli/changelog/render.md | 2 +- .../Rendering/ChangelogRenderingService.cs | 20 +- .../Changelogs/Render/TitleTargetTests.cs | 198 ++++++++++++++++++ 3 files changed, 216 insertions(+), 4 deletions(-) diff --git a/docs/cli/changelog/render.md b/docs/cli/changelog/render.md index a2c2d3ce9..ef28debd3 100644 --- a/docs/cli/changelog/render.md +++ b/docs/cli/changelog/render.md @@ -64,7 +64,7 @@ The `render` command automatically discovers and merges `.amend-*.yaml` files wi `--title ` : Optional: The title to use for section headers, directories, and anchors in output files. -: Defaults to the version in the first bundle. +: Defaults to the version in the first bundle. When omitted, ISO date targets are formatted for display the same way as the `{changelog}` directive (e.g., `2026-05-04` becomes "May 4, 2026", `2026-05` becomes "May 2026"), while directory names and heading anchors continue to use the raw target slug. : If the string contains spaces, they are replaced with dashes when used in directory names and anchors. The `changelog render` command does **not** use `rules.publish` for filtering. Filtering must be done at bundle time using `rules.bundle`. For more information, refer to [](/contribute/publish-changelogs.md). For how the directive differs, see the [{changelog} directive syntax reference](/syntax/changelog.md). diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs index 4be056a5d..068212a72 100644 --- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs +++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs @@ -227,9 +227,23 @@ private OutputSetup SetupOutput( if (string.IsNullOrWhiteSpace(input.Title) && version == "unknown") collector.EmitWarning(string.Empty, "No --title option provided and bundle files do not contain 'target' values. Output folder and markdown titles will default to 'unknown'. Consider using --title to specify a custom title."); - // Use title from input or default to version - var title = input.Title ?? version; - var titleSlug = ChangelogTextUtilities.TitleToSlug(title); + // Determine title and slug + string title; + string titleSlug; + + if (string.IsNullOrWhiteSpace(input.Title)) + { + // Default title: format dates like the changelog directive + title = VersionOrDate.FormatDisplayVersion(version); + // Slug always uses raw version to maintain consistent paths/anchors + titleSlug = ChangelogTextUtilities.TitleToSlug(version); + } + else + { + // Explicit title provided: use as-is for both title and slug + title = input.Title; + titleSlug = ChangelogTextUtilities.TitleToSlug(input.Title); + } return new OutputSetup(outputDir, title, titleSlug); } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs index b6bf4c576..71b3fa53c 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs @@ -132,4 +132,202 @@ public async Task RenderChangelogs_WithTitleAndNoTargets_NoWarning() d.Severity == Severity.Warning && d.Message.Contains("No --title option provided")); } + + [Fact] + public async Task RenderChangelogs_WithIsoDateTarget_FormatsDateInHeading() + { + // Arrange + var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(changelogDir); + + // Create test changelog file with ISO date target + // language=yaml + var changelog1 = + """ + title: Test feature + type: feature + products: + - product: elasticsearch + target: 2026-05-04 + prs: + - "100" + """; + + var changelogFile = FileSystem.Path.Join(changelogDir, "1755268130-test-feature.yaml"); + await FileSystem.File.WriteAllTextAsync(changelogFile, changelog1, TestContext.Current.CancellationToken); + + // Create bundle file with ISO date target + var bundleDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(bundleDir); + + var bundleFile = FileSystem.Path.Join(bundleDir, "bundle.yaml"); + // language=yaml + var bundleContent = + $""" + products: + - product: elasticsearch + target: 2026-05-04 + entries: + - file: + name: 1755268130-test-feature.yaml + checksum: {ComputeSha1(changelog1)} + """; + await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken); + + var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + + var input = new RenderChangelogsArguments + { + Bundles = [new BundleInput { BundleFile = bundleFile, Directory = changelogDir }], + Output = outputDir + // Note: Title is not set, should default to formatted date + }; + + // Act + var result = await Service.RenderChangelogs(Collector, input, TestContext.Current.CancellationToken); + + // Assert + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + // Check that output directory uses raw date slug + var indexFile = FileSystem.Path.Join(outputDir, "2026-05-04", "index.md"); + FileSystem.File.Exists(indexFile).Should().BeTrue(); + + // Check that heading uses formatted date but anchor uses raw date + var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current.CancellationToken); + indexContent.Should().Contain("## May 4, 2026 [elastic-release-notes-2026-05-04]"); + } + + [Fact] + public async Task RenderChangelogs_WithYearMonthTarget_FormatsDateInHeading() + { + // Arrange + var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(changelogDir); + + // Create test changelog file with year-month target + // language=yaml + var changelog1 = + """ + title: Test feature + type: feature + products: + - product: elasticsearch + target: 2026-05 + prs: + - "100" + """; + + var changelogFile = FileSystem.Path.Join(changelogDir, "1755268130-test-feature.yaml"); + await FileSystem.File.WriteAllTextAsync(changelogFile, changelog1, TestContext.Current.CancellationToken); + + // Create bundle file with year-month target + var bundleDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(bundleDir); + + var bundleFile = FileSystem.Path.Join(bundleDir, "bundle.yaml"); + // language=yaml + var bundleContent = + $""" + products: + - product: elasticsearch + target: 2026-05 + entries: + - file: + name: 1755268130-test-feature.yaml + checksum: {ComputeSha1(changelog1)} + """; + await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken); + + var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + + var input = new RenderChangelogsArguments + { + Bundles = [new BundleInput { BundleFile = bundleFile, Directory = changelogDir }], + Output = outputDir + // Note: Title is not set, should default to formatted date + }; + + // Act + var result = await Service.RenderChangelogs(Collector, input, TestContext.Current.CancellationToken); + + // Assert + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + // Check that output directory uses raw date slug + var indexFile = FileSystem.Path.Join(outputDir, "2026-05", "index.md"); + FileSystem.File.Exists(indexFile).Should().BeTrue(); + + // Check that heading uses formatted date but anchor uses raw date + var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current.CancellationToken); + indexContent.Should().Contain("## May 2026 [elastic-release-notes-2026-05]"); + } + + [Fact] + public async Task RenderChangelogs_WithExplicitDateTitle_DoesNotFormatTitle() + { + // Arrange + var changelogDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(changelogDir); + + // Create test changelog file with ISO date target + // language=yaml + var changelog1 = + """ + title: Test feature + type: feature + products: + - product: elasticsearch + target: 2026-05-04 + prs: + - "100" + """; + + var changelogFile = FileSystem.Path.Join(changelogDir, "1755268130-test-feature.yaml"); + await FileSystem.File.WriteAllTextAsync(changelogFile, changelog1, TestContext.Current.CancellationToken); + + // Create bundle file with ISO date target + var bundleDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + FileSystem.Directory.CreateDirectory(bundleDir); + + var bundleFile = FileSystem.Path.Join(bundleDir, "bundle.yaml"); + // language=yaml + var bundleContent = + $""" + products: + - product: elasticsearch + target: 2026-05-04 + entries: + - file: + name: 1755268130-test-feature.yaml + checksum: {ComputeSha1(changelog1)} + """; + await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken); + + var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); + + var input = new RenderChangelogsArguments + { + Bundles = [new BundleInput { BundleFile = bundleFile, Directory = changelogDir }], + Output = outputDir, + Title = "2026-05-04" // Explicit title provided - should stay literal + }; + + // Act + var result = await Service.RenderChangelogs(Collector, input, TestContext.Current.CancellationToken); + + // Assert + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + // Check that output directory uses title slug + var indexFile = FileSystem.Path.Join(outputDir, "2026-05-04", "index.md"); + FileSystem.File.Exists(indexFile).Should().BeTrue(); + + // Check that heading uses literal title (no formatting applied) + var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current.CancellationToken); + indexContent.Should().Contain("## 2026-05-04 [elastic-release-notes-2026-05-04]"); + } } From c413286733c7e142f9e350f9953218007c81ba9d Mon Sep 17 00:00:00 2001 From: lcawl Date: Wed, 6 May 2026 14:46:43 -0700 Subject: [PATCH 3/5] Fix list indentation --- .../BreakingChangesMarkdownRenderer.cs | 10 ++--- .../Markdown/DeprecationsMarkdownRenderer.cs | 10 ++--- .../Markdown/HighlightsMarkdownRenderer.cs | 4 +- .../Markdown/KnownIssuesMarkdownRenderer.cs | 10 ++--- .../Markdown/MarkdownRendererBase.cs | 42 +++++++++---------- .../Changelogs/Render/DropdownRenderTests.cs | 14 +++---- 6 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs index ff8394208..783df0253 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs @@ -98,19 +98,19 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(); } - // PR/Issue links with "For more information" pattern - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks); + // PR/Issue links with "For more information" pattern - indented for list continuation + RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: true); - // Impact and Action sections + // Impact and Action sections - indented for list continuation if (!string.IsNullOrWhiteSpace(entry.Impact)) { - _ = sb.AppendLine("**Impact:** " + entry.Impact); + _ = sb.AppendLine(ChangelogTextUtilities.Indent("**Impact:** " + entry.Impact)); _ = sb.AppendLine(); } if (!string.IsNullOrWhiteSpace(entry.Action)) { - _ = sb.AppendLine("**Action:** " + entry.Action); + _ = sb.AppendLine(ChangelogTextUtilities.Indent("**Action:** " + entry.Action)); _ = sb.AppendLine(); } } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs index aa917486f..0d47e0a22 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs @@ -95,19 +95,19 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(); } - // PR/Issue links with "For more information" pattern - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks); + // PR/Issue links with "For more information" pattern - indented for list continuation + RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: true); - // Impact and Action sections + // Impact and Action sections - indented for list continuation if (!string.IsNullOrWhiteSpace(entry.Impact)) { - _ = sb.AppendLine("**Impact:** " + entry.Impact); + _ = sb.AppendLine(ChangelogTextUtilities.Indent("**Impact:** " + entry.Impact)); _ = sb.AppendLine(); } if (!string.IsNullOrWhiteSpace(entry.Action)) { - _ = sb.AppendLine("**Action:** " + entry.Action); + _ = sb.AppendLine(ChangelogTextUtilities.Indent("**Action:** " + entry.Action)); _ = sb.AppendLine(); } } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs index bc48a9271..d20a43902 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs @@ -87,8 +87,8 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(); } - // PR/Issue links with "For more information" pattern - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks); + // PR/Issue links with "For more information" pattern - indented for list continuation + RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: true); } if (shouldHide) diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs index c1bbe5c88..46e94fb34 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs @@ -95,19 +95,19 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(); } - // PR/Issue links with "For more information" pattern - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks); + // PR/Issue links with "For more information" pattern - indented for list continuation + RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: true); - // Impact and Action sections + // Impact and Action sections - indented for list continuation if (!string.IsNullOrWhiteSpace(entry.Impact)) { - _ = sb.AppendLine("**Impact:** " + entry.Impact); + _ = sb.AppendLine(ChangelogTextUtilities.Indent("**Impact:** " + entry.Impact)); _ = sb.AppendLine(); } if (!string.IsNullOrWhiteSpace(entry.Action)) { - _ = sb.AppendLine("**Action:** " + entry.Action); + _ = sb.AppendLine(ChangelogTextUtilities.Indent("**Action:** " + entry.Action)); _ = sb.AppendLine(); } } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs b/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs index 6e7cc9447..704394972 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs @@ -40,6 +40,12 @@ protected async Task WriteOutputFileAsync(string outputDir, string titleSlug, st /// Renders PR and issue links for dropdown entries /// protected static void RenderPrIssueLinks(StringBuilder sb, ChangelogEntry entry, string entryRepo, string entryOwner, bool entryHideLinks) + => RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: false); + + /// + /// Renders PR and issue links with optional indentation for flattened list items + /// + protected static void RenderPrIssueLinks(StringBuilder sb, ChangelogEntry entry, string entryRepo, string entryOwner, bool entryHideLinks, bool indentForListItem) { var prParts = new List(); foreach (var pr in entry.Prs ?? []) @@ -62,34 +68,28 @@ protected static void RenderPrIssueLinks(StringBuilder sb, ChangelogEntry entry, if (entryHideLinks) { - foreach (var s in prParts) - _ = sb.AppendLine(s); - foreach (var s in issueParts) - _ = sb.AppendLine(s); - - _ = sb.AppendLine("For more information, check the pull request or issue above."); - } - else - { - _ = sb.Append("For more information, check "); - var first = true; foreach (var s in prParts) { - if (!first) - _ = sb.Append(' '); - _ = sb.Append(s); - first = false; + var line = indentForListItem ? ChangelogTextUtilities.Indent(s) : s; + _ = sb.AppendLine(line); } - foreach (var s in issueParts) { - if (!first) - _ = sb.Append(' '); - _ = sb.Append(s); - first = false; + var line = indentForListItem ? ChangelogTextUtilities.Indent(s) : s; + _ = sb.AppendLine(line); } - _ = sb.AppendLine("."); + var infoLine = "For more information, check the pull request or issue above."; + _ = sb.AppendLine(indentForListItem ? ChangelogTextUtilities.Indent(infoLine) : infoLine); + } + else + { + var lineParts = new List { "For more information, check" }; + lineParts.AddRange(prParts); + lineParts.AddRange(issueParts); + + var fullLine = string.Join(" ", lineParts) + "."; + _ = sb.AppendLine(indentForListItem ? ChangelogTextUtilities.Indent(fullLine) : fullLine); } _ = sb.AppendLine(); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs index d4ec84e2a..af37eb8f7 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs @@ -155,11 +155,11 @@ public async Task RenderChangelogs_WithDropdownsFalse_RendersFlattendFormat() // Verify flattened format content.Should().Contain("* Deprecated old API"); content.Should().Contain("The old API is deprecated"); - content.Should().Contain("For more information, check"); + content.Should().Contain(" For more information, check"); // Indented for list continuation content.Should().Contain("#456"); content.Should().Contain("#789"); - content.Should().Contain("**Impact:** API will be removed in future version"); - content.Should().Contain("**Action:** Migrate to the new API"); + content.Should().Contain(" **Impact:** API will be removed in future version"); // Indented for list continuation + content.Should().Contain(" **Action:** Migrate to the new API"); // Indented for list continuation // Should NOT contain dropdown syntax content.Should().NotContain("::::{dropdown}"); @@ -234,10 +234,10 @@ public async Task RenderChangelogs_DefaultDropdownsFalse_RendersFlattedFormat() // Verify flattened format (default behavior) content.Should().Contain("* Known issue with search"); content.Should().Contain("Search results are incomplete under certain conditions"); - content.Should().Contain("For more information, check"); + content.Should().Contain(" For more information, check"); // Indented for list continuation content.Should().Contain("#999"); - content.Should().Contain("**Impact:** Some search results may be missing"); - content.Should().Contain("**Action:** Use the workaround provided in the documentation"); + content.Should().Contain(" **Impact:** Some search results may be missing"); // Indented for list continuation + content.Should().Contain(" **Action:** Use the workaround provided in the documentation"); // Indented for list continuation // Should NOT contain dropdown syntax content.Should().NotContain("::::{dropdown}"); @@ -331,7 +331,7 @@ public async Task RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat( // Verify flattened format content.Should().Contain("* Amazing new feature"); content.Should().Contain("This feature revolutionizes how you work with data"); - content.Should().Contain("For more information, check"); + content.Should().Contain(" For more information, check"); // Indented for list continuation content.Should().Contain("#555"); // Should NOT contain dropdown syntax From aa32ca116a17b6094f3e65d4e4d30535108da901 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 7 May 2026 08:27:46 -0700 Subject: [PATCH 4/5] Improve changelog render for asciidoc (#3262) * Improve changelog render asciidoc output * Remove definition lists in favour of subsections --- docs/cli/changelog/render.md | 6 +++ .../Asciidoc/AsciidocRendererBase.cs | 34 ++++++++++------ .../BreakingChangesAsciidocRenderer.cs | 14 ++++++- .../Asciidoc/DeprecationsAsciidocRenderer.cs | 36 ++++++++++++----- .../Asciidoc/EntriesByAreaAsciidocRenderer.cs | 36 +++++++++++------ .../Asciidoc/HighlightsAsciidocRenderer.cs | 36 +++++++++++------ .../Asciidoc/KnownIssuesAsciidocRenderer.cs | 36 ++++++++++++----- .../BreakingChangesMarkdownRenderer.cs | 4 +- .../Markdown/DeprecationsMarkdownRenderer.cs | 4 +- .../Markdown/HighlightsMarkdownRenderer.cs | 4 +- .../Markdown/KnownIssuesMarkdownRenderer.cs | 4 +- .../Markdown/MarkdownRendererBase.cs | 39 +++++++++++-------- .../Changelogs/Render/DropdownRenderTests.cs | 4 +- 13 files changed, 175 insertions(+), 82 deletions(-) diff --git a/docs/cli/changelog/render.md b/docs/cli/changelog/render.md index ef28debd3..9ce352db2 100644 --- a/docs/cli/changelog/render.md +++ b/docs/cli/changelog/render.md @@ -98,6 +98,12 @@ When `--file-type asciidoc` is specified, the command generates a single asciido The asciidoc output uses attribute references for links (for example, `{repo-pull}NUMBER[#NUMBER]`). +AsciiDoc output ignores the `--dropdowns` flag and always uses a standardized format with the following characteristics: + +- Multi-block entries (containing description, Impact, and Action sections) use proper list continuation markers (`+`) to maintain list structure +- Strong text formatting uses idiomatic single asterisk syntax (`*Impact:*`, `*Action:*`) following AsciiDoc best practices +- All content blocks are properly attached to their parent list items for correct rendering + ### Multiple PR and issue links Changelog entries can reference multiple pull requests and issues using the `prs` and `issues` array fields. When an entry has multiple links, all of them are rendered inline for that entry: diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/AsciidocRendererBase.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/AsciidocRendererBase.cs index 92a6e23f0..d9aa5fcb6 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/AsciidocRendererBase.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/AsciidocRendererBase.cs @@ -61,40 +61,48 @@ private static void RenderEntryTitleAndLinks(StringBuilder sb, ChangelogEntry en } /// - /// Renders an entry's description with optional comment handling + /// Renders an entry's description with optional comment handling and list continuation /// - private static void RenderEntryDescription(StringBuilder sb, ChangelogEntry entry, bool shouldHide) + private static void RenderEntryDescription(StringBuilder sb, ChangelogEntry entry, bool shouldHide, bool needsContinuation = true) { if (string.IsNullOrWhiteSpace(entry.Description)) return; _ = sb.AppendLine(); - var indented = ChangelogTextUtilities.Indent(entry.Description); + + // Add list continuation marker for multi-block list items + if (needsContinuation) + { + _ = sb.AppendLine("+"); + } + if (shouldHide) { - var indentedLines = indented.Split('\n'); - foreach (var line in indentedLines) + var descriptionLines = entry.Description.Split('\n'); + foreach (var line in descriptionLines) _ = sb.AppendLine(CultureInfo.InvariantCulture, $"// {line}"); } else - _ = sb.AppendLine(indented); + _ = sb.AppendLine(entry.Description); } /// - /// Renders Impact and Action fields for breaking changes, deprecations, and known issues + /// Renders Impact and Action fields for breaking changes, deprecations, and known issues with list continuation /// private static void RenderImpactAndAction(StringBuilder sb, ChangelogEntry entry) { if (!string.IsNullOrWhiteSpace(entry.Impact)) { _ = sb.AppendLine(); - _ = sb.AppendLine(CultureInfo.InvariantCulture, $"**Impact:** {entry.Impact}"); + _ = sb.AppendLine("+"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"*Impact:* {entry.Impact}"); } if (!string.IsNullOrWhiteSpace(entry.Action)) { _ = sb.AppendLine(); - _ = sb.AppendLine(CultureInfo.InvariantCulture, $"**Action:** {entry.Action}"); + _ = sb.AppendLine("+"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"*Action:* {entry.Action}"); } } @@ -105,7 +113,7 @@ protected void RenderBasicEntry(StringBuilder sb, ChangelogEntry entry, Changelo { var (entryRepo, _, hideLinks, shouldHide) = ChangelogRenderUtilities.GetEntryContext(entry, context); RenderEntryTitleAndLinks(sb, entry, entryRepo, hideLinks, shouldHide); - RenderEntryDescription(sb, entry, shouldHide); + RenderEntryDescription(sb, entry, shouldHide, needsContinuation: !string.IsNullOrWhiteSpace(entry.Description)); _ = sb.AppendLine(); } @@ -116,7 +124,11 @@ protected void RenderEntryWithImpactAction(StringBuilder sb, ChangelogEntry entr { var (entryRepo, _, hideLinks, shouldHide) = ChangelogRenderUtilities.GetEntryContext(entry, context); RenderEntryTitleAndLinks(sb, entry, entryRepo, hideLinks, shouldHide); - RenderEntryDescription(sb, entry, shouldHide); + + // Description needs continuation when it exists + var hasDescription = !string.IsNullOrWhiteSpace(entry.Description); + RenderEntryDescription(sb, entry, shouldHide, needsContinuation: hasDescription); + RenderImpactAndAction(sb, entry); _ = sb.AppendLine(); } diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/BreakingChangesAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/BreakingChangesAsciidocRenderer.cs index cdf2a794d..99532acf3 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/BreakingChangesAsciidocRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/BreakingChangesAsciidocRenderer.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Globalization; using System.Text; using Elastic.Documentation; using Elastic.Documentation.ReleaseNotes; @@ -30,8 +31,17 @@ public override void Render(IReadOnlyCollection entries, Changel if (context.Subsections && !string.IsNullOrWhiteSpace(group.Key)) { var header = ChangelogTextUtilities.FormatSubtypeHeader(group.Key); - var headerLine = allEntriesHidden ? $"// **{header}**" : $"**{header}**"; - _ = sb.AppendLine(headerLine); + + if (allEntriesHidden) + { + _ = sb.AppendLine("// [float]"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"// ==== {header}"); + } + else + { + _ = sb.AppendLine("[float]"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"==== {header}"); + } _ = sb.AppendLine(); } diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs index 11d1cf7c0..264730c05 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Globalization; using System.Text; using Elastic.Documentation.ReleaseNotes; @@ -15,22 +16,37 @@ public class DeprecationsAsciidocRenderer(StringBuilder sb) : AsciidocRendererBa /// public override void Render(IReadOnlyCollection entries, ChangelogRenderContext context) { - var groupedByArea = entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList(); + // Group by area if subsections is enabled, otherwise use single group + var groupedEntries = context.Subsections + ? entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList() + : [entries.GroupBy(_ => string.Empty).First()]; - foreach (var areaGroup in groupedByArea) + foreach (var group in groupedEntries) { - // Check if all entries in this area group are hidden - var allEntriesHidden = areaGroup.All(entry => + // Check if all entries in this group are hidden + var allEntriesHidden = group.All(entry => ChangelogRenderUtilities.ShouldHideEntry(entry, context.FeatureIdsToHide, context)); - var componentName = !string.IsNullOrWhiteSpace(areaGroup.Key) ? areaGroup.Key : "General"; - var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName); + // Add nested section header when subsections are enabled and group has a name + if (context.Subsections && !string.IsNullOrWhiteSpace(group.Key)) + { + var componentName = group.Key != string.Empty ? group.Key : "General"; + var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName); - var headerLine = allEntriesHidden ? $"// {formattedComponent}::" : $"{formattedComponent}::"; - _ = sb.AppendLine(headerLine); - _ = sb.AppendLine(); + if (allEntriesHidden) + { + _ = sb.AppendLine("// [float]"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"// ==== {formattedComponent}"); + } + else + { + _ = sb.AppendLine("[float]"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"==== {formattedComponent}"); + } + _ = sb.AppendLine(); + } - foreach (var entry in areaGroup) + foreach (var entry in group) RenderEntryWithImpactAction(sb, entry, context); } } diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/EntriesByAreaAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/EntriesByAreaAsciidocRenderer.cs index 0bd171ad2..513f8901b 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/EntriesByAreaAsciidocRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/EntriesByAreaAsciidocRenderer.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Globalization; using System.Text; using Elastic.Documentation.ReleaseNotes; @@ -15,24 +16,37 @@ public class EntriesByAreaAsciidocRenderer(StringBuilder sb) : AsciidocRendererB /// public override void Render(IReadOnlyCollection entries, ChangelogRenderContext context) { - var groupedByArea = context.Subsections + // Group by area if subsections is enabled, otherwise use single group + var groupedEntries = context.Subsections ? entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList() - : entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).ToList(); + : [entries.GroupBy(_ => string.Empty).First()]; - foreach (var areaGroup in groupedByArea) + foreach (var group in groupedEntries) { - // Check if all entries in this area group are hidden - var allEntriesHidden = areaGroup.All(entry => + // Check if all entries in this group are hidden + var allEntriesHidden = group.All(entry => ChangelogRenderUtilities.ShouldHideEntry(entry, context.FeatureIdsToHide, context)); - var componentName = !string.IsNullOrWhiteSpace(areaGroup.Key) ? areaGroup.Key : "General"; - var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName); + // Add nested section header when subsections are enabled and group has a name + if (context.Subsections && !string.IsNullOrWhiteSpace(group.Key)) + { + var componentName = group.Key != string.Empty ? group.Key : "General"; + var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName); - var headerLine = allEntriesHidden ? $"// {formattedComponent}::" : $"{formattedComponent}::"; - _ = sb.AppendLine(headerLine); - _ = sb.AppendLine(); + if (allEntriesHidden) + { + _ = sb.AppendLine("// [float]"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"// ==== {formattedComponent}"); + } + else + { + _ = sb.AppendLine("[float]"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"==== {formattedComponent}"); + } + _ = sb.AppendLine(); + } - foreach (var entry in areaGroup) + foreach (var entry in group) RenderBasicEntry(sb, entry, context); } } diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/HighlightsAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/HighlightsAsciidocRenderer.cs index eff5542d0..07312f14f 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/HighlightsAsciidocRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/HighlightsAsciidocRenderer.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Globalization; using System.Text; using Elastic.Documentation.ReleaseNotes; @@ -15,24 +16,37 @@ public class HighlightsAsciidocRenderer(StringBuilder sb) : AsciidocRendererBase /// public override void Render(IReadOnlyCollection entries, ChangelogRenderContext context) { - var groupedByArea = context.Subsections + // Group by area if subsections is enabled, otherwise use single group + var groupedEntries = context.Subsections ? entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList() - : entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).ToList(); + : [entries.GroupBy(_ => string.Empty).First()]; - foreach (var areaGroup in groupedByArea) + foreach (var group in groupedEntries) { - // Check if all entries in this area group are hidden - var allEntriesHidden = areaGroup.All(entry => + // Check if all entries in this group are hidden + var allEntriesHidden = group.All(entry => ChangelogRenderUtilities.ShouldHideEntry(entry, context.FeatureIdsToHide, context)); - var componentName = !string.IsNullOrWhiteSpace(areaGroup.Key) ? areaGroup.Key : "General"; - var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName); + // Add nested section header when subsections are enabled and group has a name + if (context.Subsections && !string.IsNullOrWhiteSpace(group.Key)) + { + var componentName = group.Key != string.Empty ? group.Key : "General"; + var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName); - var headerLine = allEntriesHidden ? $"// {formattedComponent}::" : $"{formattedComponent}::"; - _ = sb.AppendLine(headerLine); - _ = sb.AppendLine(); + if (allEntriesHidden) + { + _ = sb.AppendLine("// [float]"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"// ==== {formattedComponent}"); + } + else + { + _ = sb.AppendLine("[float]"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"==== {formattedComponent}"); + } + _ = sb.AppendLine(); + } - foreach (var entry in areaGroup) + foreach (var entry in group) RenderBasicEntry(sb, entry, context); } } diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/KnownIssuesAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/KnownIssuesAsciidocRenderer.cs index f7ff3bc34..74fac4e4f 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/KnownIssuesAsciidocRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/KnownIssuesAsciidocRenderer.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Globalization; using System.Text; using Elastic.Documentation.ReleaseNotes; @@ -15,22 +16,37 @@ public class KnownIssuesAsciidocRenderer(StringBuilder sb) : AsciidocRendererBas /// public override void Render(IReadOnlyCollection entries, ChangelogRenderContext context) { - var groupedByArea = entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList(); + // Group by area if subsections is enabled, otherwise use single group + var groupedEntries = context.Subsections + ? entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList() + : [entries.GroupBy(_ => string.Empty).First()]; - foreach (var areaGroup in groupedByArea) + foreach (var group in groupedEntries) { - // Check if all entries in this area group are hidden - var allEntriesHidden = areaGroup.All(entry => + // Check if all entries in this group are hidden + var allEntriesHidden = group.All(entry => ChangelogRenderUtilities.ShouldHideEntry(entry, context.FeatureIdsToHide, context)); - var componentName = !string.IsNullOrWhiteSpace(areaGroup.Key) ? areaGroup.Key : "General"; - var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName); + // Add nested section header when subsections are enabled and group has a name + if (context.Subsections && !string.IsNullOrWhiteSpace(group.Key)) + { + var componentName = group.Key != string.Empty ? group.Key : "General"; + var formattedComponent = ChangelogTextUtilities.FormatAreaHeader(componentName); - var headerLine = allEntriesHidden ? $"// {formattedComponent}::" : $"{formattedComponent}::"; - _ = sb.AppendLine(headerLine); - _ = sb.AppendLine(); + if (allEntriesHidden) + { + _ = sb.AppendLine("// [float]"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"// ==== {formattedComponent}"); + } + else + { + _ = sb.AppendLine("[float]"); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"==== {formattedComponent}"); + } + _ = sb.AppendLine(); + } - foreach (var entry in areaGroup) + foreach (var entry in group) RenderEntryWithImpactAction(sb, entry, context); } } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs index 783df0253..4cee9acec 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs @@ -70,7 +70,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(InvariantCulture, $"::::{{dropdown}} {ChangelogTextUtilities.Beautify(entry.Title)}"); _ = sb.AppendLine(entry.Description ?? "% Describe the functionality that changed"); _ = sb.AppendLine(); - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks); + RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks)); _ = sb.AppendLine(!string.IsNullOrWhiteSpace(entry.Impact) ? "**Impact**
" + entry.Impact @@ -99,7 +99,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct } // PR/Issue links with "For more information" pattern - indented for list continuation - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: true); + RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks, IndentForListItem: true)); // Impact and Action sections - indented for list continuation if (!string.IsNullOrWhiteSpace(entry.Impact)) diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs index 0d47e0a22..c5b80228b 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs @@ -67,7 +67,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(InvariantCulture, $"::::{{dropdown}} {ChangelogTextUtilities.Beautify(entry.Title)}"); _ = sb.AppendLine(entry.Description ?? "% Describe the functionality that was deprecated"); _ = sb.AppendLine(); - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks); + RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks)); _ = sb.AppendLine(!string.IsNullOrWhiteSpace(entry.Impact) ? "**Impact**
" + entry.Impact @@ -96,7 +96,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct } // PR/Issue links with "For more information" pattern - indented for list continuation - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: true); + RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks, IndentForListItem: true)); // Impact and Action sections - indented for list continuation if (!string.IsNullOrWhiteSpace(entry.Impact)) diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs index d20a43902..5d5cf5a2e 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/HighlightsMarkdownRenderer.cs @@ -70,7 +70,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(InvariantCulture, $"::::{{dropdown}} {ChangelogTextUtilities.Beautify(entry.Title)}"); _ = sb.AppendLine(entry.Description ?? "% Describe the highlight"); _ = sb.AppendLine(); - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks); + RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks)); _ = sb.AppendLine("::::"); } else @@ -88,7 +88,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct } // PR/Issue links with "For more information" pattern - indented for list continuation - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: true); + RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks, IndentForListItem: true)); } if (shouldHide) diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs index 46e94fb34..3f6e3b22d 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs @@ -67,7 +67,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct _ = sb.AppendLine(InvariantCulture, $"::::{{dropdown}} {ChangelogTextUtilities.Beautify(entry.Title)}"); _ = sb.AppendLine(entry.Description ?? "% Describe the known issue"); _ = sb.AppendLine(); - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks); + RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks)); _ = sb.AppendLine(!string.IsNullOrWhiteSpace(entry.Impact) ? "**Impact**
" + entry.Impact @@ -96,7 +96,7 @@ public override async Task RenderAsync(ChangelogRenderContext context, Cancel ct } // PR/Issue links with "For more information" pattern - indented for list continuation - RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: true); + RenderPrIssueLinks(sb, new PrIssueLinkOptions(entry, entryRepo, entryOwner, entryHideLinks, IndentForListItem: true)); // Impact and Action sections - indented for list continuation if (!string.IsNullOrWhiteSpace(entry.Impact)) diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs b/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs index 704394972..d545d6e50 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs @@ -10,6 +10,17 @@ namespace Elastic.Changelog.Rendering.Markdown; +/// +/// Options for rendering PR and issue links +/// +public record PrIssueLinkOptions( + ChangelogEntry Entry, + string Repo, + string Owner, + bool HideLinks, + bool IndentForListItem = false +); + /// /// Abstract base class for changelog markdown renderers /// @@ -37,28 +48,22 @@ protected async Task WriteOutputFileAsync(string outputDir, string titleSlug, st } /// - /// Renders PR and issue links for dropdown entries - /// - protected static void RenderPrIssueLinks(StringBuilder sb, ChangelogEntry entry, string entryRepo, string entryOwner, bool entryHideLinks) - => RenderPrIssueLinks(sb, entry, entryRepo, entryOwner, entryHideLinks, indentForListItem: false); - - /// - /// Renders PR and issue links with optional indentation for flattened list items + /// Renders PR and issue links with configurable formatting options /// - protected static void RenderPrIssueLinks(StringBuilder sb, ChangelogEntry entry, string entryRepo, string entryOwner, bool entryHideLinks, bool indentForListItem) + protected static void RenderPrIssueLinks(StringBuilder sb, PrIssueLinkOptions options) { var prParts = new List(); - foreach (var pr in entry.Prs ?? []) + foreach (var pr in options.Entry.Prs ?? []) { - var s = ChangelogTextUtilities.FormatPrLink(pr, entryRepo, entryHideLinks, entryOwner); + var s = ChangelogTextUtilities.FormatPrLink(pr, options.Repo, options.HideLinks, options.Owner); if (!string.IsNullOrEmpty(s)) prParts.Add(s); } var issueParts = new List(); - foreach (var issue in entry.Issues ?? []) + foreach (var issue in options.Entry.Issues ?? []) { - var s = ChangelogTextUtilities.FormatIssueLink(issue, entryRepo, entryHideLinks, entryOwner); + var s = ChangelogTextUtilities.FormatIssueLink(issue, options.Repo, options.HideLinks, options.Owner); if (!string.IsNullOrEmpty(s)) issueParts.Add(s); } @@ -66,21 +71,21 @@ protected static void RenderPrIssueLinks(StringBuilder sb, ChangelogEntry entry, if (prParts.Count == 0 && issueParts.Count == 0) return; - if (entryHideLinks) + if (options.HideLinks) { foreach (var s in prParts) { - var line = indentForListItem ? ChangelogTextUtilities.Indent(s) : s; + var line = options.IndentForListItem ? ChangelogTextUtilities.Indent(s) : s; _ = sb.AppendLine(line); } foreach (var s in issueParts) { - var line = indentForListItem ? ChangelogTextUtilities.Indent(s) : s; + var line = options.IndentForListItem ? ChangelogTextUtilities.Indent(s) : s; _ = sb.AppendLine(line); } var infoLine = "For more information, check the pull request or issue above."; - _ = sb.AppendLine(indentForListItem ? ChangelogTextUtilities.Indent(infoLine) : infoLine); + _ = sb.AppendLine(options.IndentForListItem ? ChangelogTextUtilities.Indent(infoLine) : infoLine); } else { @@ -89,7 +94,7 @@ protected static void RenderPrIssueLinks(StringBuilder sb, ChangelogEntry entry, lineParts.AddRange(issueParts); var fullLine = string.Join(" ", lineParts) + "."; - _ = sb.AppendLine(indentForListItem ? ChangelogTextUtilities.Indent(fullLine) : fullLine); + _ = sb.AppendLine(options.IndentForListItem ? ChangelogTextUtilities.Indent(fullLine) : fullLine); } _ = sb.AppendLine(); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs index af37eb8f7..204fe7c0d 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs @@ -418,8 +418,8 @@ public async Task RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag() // AsciiDoc should always use bullet format regardless of dropdowns flag content.Should().Contain("* Breaking API change"); - content.Should().Contain("**Impact:** Existing API calls will fail"); - content.Should().Contain("**Action:** Update your code"); + content.Should().Contain("*Impact:* Existing API calls will fail"); + content.Should().Contain("*Action:* Update your code"); // Should never contain MyST dropdown syntax content.Should().NotContain("::::{dropdown}"); From b8d31e3f0a6dac3d6aa9d40ebc30242a7a17e229 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 7 May 2026 09:19:15 -0700 Subject: [PATCH 5/5] Update src/services/Elastic.Changelog/Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs index 264730c05..2800e9507 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/DeprecationsAsciidocRenderer.cs @@ -17,6 +17,8 @@ public class DeprecationsAsciidocRenderer(StringBuilder sb) : AsciidocRendererBa public override void Render(IReadOnlyCollection entries, ChangelogRenderContext context) { // Group by area if subsections is enabled, otherwise use single group + if (entries.Count == 0) + return; var groupedEntries = context.Subsections ? entries.GroupBy(e => ChangelogRenderUtilities.GetComponent(e, context)).OrderBy(g => g.Key).ToList() : [entries.GroupBy(_ => string.Empty).First()];