diff --git a/docs/changelog/bundles/0.100.0.yaml b/docs/changelog/bundles/0.100.0.yaml index 98a9b225f7..2281c7d9a6 100644 --- a/docs/changelog/bundles/0.100.0.yaml +++ b/docs/changelog/bundles/0.100.0.yaml @@ -2,7 +2,7 @@ products: - product: docs-builder target: 0.100.0 entries: - # Features and enhancements + # Features - type: feature title: Add PR label blockers to changelog creation products: diff --git a/docs/syntax/changelog.md b/docs/syntax/changelog.md index f06ff051c9..f10b8c5af8 100644 --- a/docs/syntax/changelog.md +++ b/docs/syntax/changelog.md @@ -25,7 +25,7 @@ The directive supports the following options: | `:type: value` | Filter entries by type | Excludes separated types | | `:subsections:` | Group entries by area/component | false | | `:link-visibility: value` | Visibility of pull request (PR) and issue links | `auto` | -| `:description-visibility: value` | Visibility of changelog **record** descriptions (YAML `description` on each entry) | `auto` | +| `:description-visibility: value[, value…]` | Visibility of changelog descriptions (YAML `description` on each entry). Comma-separated tokens are combined. | `auto` | | `:dropdowns:` | Render breaking changes, deprecations, known issues, and highlights as expandable dropdowns instead of flattened bulleted lists | false | | `:highlights:` | Emit a dedicated highlights section for entries with `highlight: true` (entries still appear under their type sections) | false | | `:release-dates:` | Render the bundle `release-date` field as _Released: …_ after the version heading | false | @@ -124,27 +124,45 @@ This aligns with the `changelog render` command's link visibility controls. #### `:description-visibility:` -Controls whether the **`description`** text on each **changelog record** appears in output (bullet body text under each item, or the first paragraph inside a breaking-change, deprecation, known-issue, or highlight entry when [`:dropdowns:`](#dropdowns) is enabled). This is **different** from the optional **bundle** `description` field (release intro prose after `_Released:_`), which is always shown when present. See [Rendered output](#rendered-output). +Controls whether the `description` text for each changelog appears in the output (bullet body text under each item, or the first paragraph inside a breaking-change, deprecation, known-issue, or highlight entry when [`:dropdowns:`](#dropdowns) is enabled). +This is different from the optional bundle `description` field (release intro prose after `_Released:_`), which is always shown when present. +See [Rendered output](#rendered-output). + +Pass one or more comma-separated tokens. +There is at most one _base token_ (`auto`, `keep-descriptions`, or `hide-descriptions`). +_Overlay tokens_ (`keep-highlight-descriptions`, `keep-feature-descriptions`) combine with each other and with a base. | Value | Behavior | |-------|----------| -| `auto` | When **every** constituent repository in the bundle’s resolved repo identity is **public** (same private-repo detection as `:link-visibility:` from `assembler.yml`, including `repo1+repo2` merged bundles), **omit** record `description` bodies. When **any** constituent is marked **private**, **show** those bodies. In standalone builds without `assembler.yml`, every repo is treated as public ⇒ changelog descriptions are omitted under `auto`. | +| `auto` | When every constituent repository in the bundle’s resolved repo identity is public (same private-repo detection as `:link-visibility:` from `assembler.yml`, including `repo1+repo2` merged bundles), omit record `description` bodies. When any constituent is marked private, show those bodies. In standalone builds without `assembler.yml`, every repo is treated as public ⇒ changelog descriptions are omitted under `auto`. Overlay tokens still show descriptions in their sections. | | `keep-descriptions` | Always render changelog descriptions when present in the bundle source. Use this on pages such as deprecations or breaking changes when you still want full release-note prose alongside public repos. | -| `keep-highlight-descriptions` | Show changelog descriptions **only** in the [`:highlights:`](#highlights) section. Hide them in every other section, including the type-section copy of a highlighted entry. When `:highlights:` is omitted, descriptions are hidden everywhere. | -| `hide-descriptions` | Always omit changelog descriptions (titles, PR/issue links, impact, and action sections and bundle-level intros are unaffected). | +| `keep-highlight-descriptions` | Overlay: always show changelog descriptions in the [Highlights](#highlights) section. When used alone, hide them in every other section, including the type-section copy of a highlighted entry. When `:highlights:` is omitted, the overlay has no visible effect. | +| `keep-feature-descriptions` | Overlay: always show changelog descriptions in the "Features" section. When used alone, hide them in every other section. | +| `hide-descriptions` | Omit changelog descriptions in unmarked sections (titles, PR/issue links, impact, and action sections and bundle-level intros are unaffected). Overlay tokens still show descriptions in their sections. | + +```markdown +:::{changelog} +:highlights: +:description-visibility: keep-feature-descriptions, keep-highlight-descriptions +::: +``` -**Contrast with `:link-visibility:`:** `:link-visibility: auto` hides **links** when a repo is **private**. `:description-visibility: auto` **shows** richer record **description** prose when **any** source repo is **private**, and hides that prose for bundles that resolve to **only public** repositories. +The example shows prose in Features and Highlights, and hides it in Enhancements, Fixes, and other type sections. + +**Contrast with `:link-visibility:`:** `:link-visibility: auto` hides **links** when a repo is **private**. `:description-visibility: auto` **shows** richer record **description** prose when **any** source repo is **private**, and hides that prose for bundles that resolve to **only public** repositories (except where an overlay token applies). #### `:dropdowns:` [dropdowns] -Controls how the "separated" entry types (`breaking-change`, `deprecation`, `known-issue`, and entries flagged `highlight: true`) are rendered. This option only affects these types; features, enhancements, security, bug fixes, documentation, regressions, and other changes are always rendered as flat bulleted lists. +Controls how breaking changes, deprecations, features, highlights, and known issues are rendered. Enhancements, security, bug fixes, documentation, regressions, and other changes stay flat bulleted lists. | Mode | Behavior | |------|----------| | (omitted, default) | Flattened: each entry renders as a bullet with its title, links, and (when present) `Impact:` / `Action:` lines as indented continuation. | | `:dropdowns:` | Dropdowns: each entry renders as an expandable `{dropdown}` with the title as the summary and description, links, `**Impact**`, and `**Action**` inside. | -Use dropdowns when breaking-change and deprecation entries have long `description`, `impact`, or `action` prose that benefits from being collapsed by default. Use the flattened default for compact release notes where the list itself is the primary content. +Use dropdowns when breaking changes, deprecations, features, or highlights have long `description`, `impact`, or `action` prose that benefits from being collapsed by default. +Use the flattened default for compact release notes where the list itself is the primary content. +`:dropdowns:` does not group entries by area; use [`:subsections:`](#subsections) for that. Entry titles may contain inline markdown markers from changelog YAML (for example, `` `setting.name` ``). Dropdown titles are plain text; see [Plain-text titles](/syntax/dropdowns.md#plain-text-titles). @@ -154,12 +172,12 @@ Controls whether entries with `highlight: true` get a dedicated **Highlights** s | Mode | Behavior | |------|----------| -| (omitted, default) | Inline only: highlighted entries appear under their normal type sections (for example Features and enhancements). No `### Highlights` section. | +| (omitted, default) | Inline only: highlighted entries appear under their normal type sections (for example Features). No `### Highlights` section. | | `:highlights:` | Section: emit a `### Highlights` section and keep those entries under their type sections. | Use `:highlights:` on general release notes when you want a highlights section alongside features, enhancements, and bug fixes — without requiring `:type: all` (which also pulls in breaking changes, deprecations, and known issues). -Changelog descriptions follow [`:description-visibility:`](#description-visibility). Use `keep-descriptions` to show prose in every section, or `keep-highlight-descriptions` to show prose only in the highlights section. +Changelog descriptions follow [`:description-visibility:`](#description-visibility). Use `keep-descriptions` to show prose in every section, `keep-highlight-descriptions` for Highlights only, `keep-feature-descriptions` for Features only, or combine overlays: `keep-feature-descriptions, keep-highlight-descriptions`. #### `:release-dates:` [release-dates] @@ -174,11 +192,12 @@ Use this option for semver or agent releases where an explicit release date adds This is **render-time** control only. To include or omit `release-date` in bundle YAML at build time, use `bundle.release_dates` in `changelog.yml` or the `--release-date` / `--no-release-date` flags on [`changelog bundle`](/cli/changelog/bundle.md) (option-based mode). The `changelog render` command does not provide an equivalent flag; it always renders release dates when present in the bundle. -#### `:subsections:` +#### `:subsections:` [subsections] When enabled, entries are grouped by "area" within each section. By default, entries are listed without area grouping. If a changelog has multiple area values, only the first one is used. +This option is independent of [`:dropdowns:`](#dropdowns). #### `:config:` @@ -379,13 +398,15 @@ This release includes new features and bug fixes. Download the release binaries: https://github.com/elastic/elasticsearch/releases/tag/v0.100.0 -### Features and enhancements +### Features +... +### Enhancements ... ### Fixes ... ## 2025-08-05 -### Features and enhancements +### Features ... ``` @@ -403,25 +424,34 @@ Each changelog entry may have its own `description` field in YAML (shown as body | Section | Entry type | Rendering | |---------|------------|-----------| -| Features and enhancements | `feature`, `enhancement` | Grouped by area | -| Fixes | `bug-fix`, `security` | Grouped by area | -| Documentation | `docs` | Grouped by area | -| Regressions | `regression` | Grouped by area | -| Other changes | `other` | Grouped by area | +| Features | `feature` | Flattened bullets by default; expandable dropdowns with [`:dropdowns:`](#dropdowns) | +| Enhancements | `enhancement` | Flattened bullets | +| Fixes | `bug-fix` | Flattened bullets | +| Security | `security` | Flattened bullets | +| Documentation | `docs` | Flattened bullets | +| Regressions | `regression` | Flattened bullets | +| Other changes | `other` | Flattened bullets | | Breaking changes | `breaking-change` | Flattened bullets by default; expandable dropdowns with [`:dropdowns:`](#dropdowns) | | Highlights | Entries with `highlight: true` | Dedicated section only when [`:highlights:`](#highlights) is set; flattened bullets by default; expandable dropdowns with [`:dropdowns:`](#dropdowns) | | Deprecations | `deprecation` | Flattened bullets by default; expandable dropdowns with [`:dropdowns:`](#dropdowns) | | Known issues | `known-issue` | Flattened bullets by default; expandable dropdowns with [`:dropdowns:`](#dropdowns) | -**Note about highlights:** +Use [`:subsections:`](#subsections) to group entries by area within a section. Area grouping is off by default and is independent of `:dropdowns:`. + +:::{admonition} Highlights - The highlights section appears only when [`:highlights:`](#highlights) is set and at least one entry has `highlight: true` -- When the section is shown, highlighted entries appear in **both** the highlights section and their original type section (for example, both the "highlights" and "features and enhancements" sections) +- When the section is shown, highlighted entries appear in both the highlights section and their original type section (for example, both the "highlights" and "features" sections) - When `:highlights:` is omitted, flagged entries still appear under their type sections (inline only) - You can combine `:highlights:` with the default type filter (no `:type:`) to show highlights alongside features and fixes without including breaking changes, deprecations, or known issues +::: + Sections with no entries of that type are omitted from the output. Releases with no entries after the `:type:` filter are omitted entirely, except on general release notes (`:type: all` or default) when the bundle has a `description`. +Section heading anchors use `-features` and `-enhancements` suffixes. +Pages that previously linked to `-features-enhancements` need those links updated. + ## Error behavior for invalid entries [changelog-missing-files] Bundles are self-contained: every entry must embed its content (`title`, `type`, and so on) inline. diff --git a/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs index 32ce2ae362..c624e11a1b 100644 --- a/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs +++ b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs @@ -17,7 +17,7 @@ public enum ChangelogEntryType [Display(Name = "invalid")] Invalid = 0, - /// A new feature or enhancement. + /// A new capability that did not exist before. [Display(Name = "feature")] Feature, diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs index 5caa9a86cf..ca073d7936 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs @@ -284,23 +284,74 @@ private ChangelogDescriptionVisibility ParseDescriptionVisibility() if (string.IsNullOrWhiteSpace(value)) return ChangelogDescriptionVisibility.Auto; - return value.ToLowerInvariant() switch + var result = ChangelogDescriptionVisibility.None; + var sawBase = false; + var hadValid = false; + + foreach (var raw in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + hadValid |= TryAccumulateDescriptionVisibilityToken(raw, ref result, ref sawBase); + + return hadValid ? result : ChangelogDescriptionVisibility.Auto; + } + + private bool TryAccumulateDescriptionVisibilityToken(string raw, ref ChangelogDescriptionVisibility result, ref bool sawBase) + { + if (!TryMapDescriptionVisibilityToken(raw, out var token, out var isBase)) { - "auto" => ChangelogDescriptionVisibility.Auto, - "keep-descriptions" => ChangelogDescriptionVisibility.KeepDescriptions, - "keep-highlight-descriptions" => ChangelogDescriptionVisibility.KeepHighlightDescriptions, - "hide-descriptions" => ChangelogDescriptionVisibility.HideDescriptions, - _ => EmitInvalidDescriptionVisibilityWarning(value) - }; + EmitInvalidDescriptionVisibilityWarning(raw); + return false; + } + + if (isBase && sawBase) + { + this.EmitWarning( + $"Multiple :description-visibility: base values; ignoring '{raw}'. Valid base values are: auto, keep-descriptions, hide-descriptions." + ); + return true; + } + + if (isBase) + sawBase = true; + + result |= token; + return true; } - private ChangelogDescriptionVisibility EmitInvalidDescriptionVisibilityWarning(string value) + private static bool TryMapDescriptionVisibilityToken(string raw, out ChangelogDescriptionVisibility token, out bool isBase) { + switch (raw.ToLowerInvariant()) + { + case "auto": + token = ChangelogDescriptionVisibility.Auto; + isBase = true; + return true; + case "keep-descriptions": + token = ChangelogDescriptionVisibility.KeepDescriptions; + isBase = true; + return true; + case "hide-descriptions": + token = ChangelogDescriptionVisibility.HideDescriptions; + isBase = true; + return true; + case "keep-highlight-descriptions": + token = ChangelogDescriptionVisibility.KeepHighlightDescriptions; + isBase = false; + return true; + case "keep-feature-descriptions": + token = ChangelogDescriptionVisibility.KeepFeatureDescriptions; + isBase = false; + return true; + default: + token = ChangelogDescriptionVisibility.None; + isBase = false; + return false; + } + } + + private void EmitInvalidDescriptionVisibilityWarning(string value) => this.EmitWarning( - $"Invalid :description-visibility: value '{value}'. Valid values are: auto, keep-descriptions, keep-highlight-descriptions, hide-descriptions. Using auto." + $"Invalid :description-visibility: value '{value}'. Valid values are: auto, keep-descriptions, keep-highlight-descriptions, keep-feature-descriptions, hide-descriptions. Using auto when no valid tokens remain." ); - return ChangelogDescriptionVisibility.Auto; - } /// /// Parses and validates the :type: option. @@ -663,12 +714,13 @@ private IEnumerable ComputeGeneratedAnchors() ) yield return $"{repo}-{anchorSlug}-deprecations"; + if (!dedicatedPage && shouldInclude(ChangelogEntryType.Feature) && entriesByType.ContainsKey(ChangelogEntryType.Feature)) + yield return $"{repo}-{anchorSlug}-features"; + if ( - !dedicatedPage - && shouldInclude(ChangelogEntryType.Feature) - && (entriesByType.ContainsKey(ChangelogEntryType.Feature) || entriesByType.ContainsKey(ChangelogEntryType.Enhancement)) + !dedicatedPage && shouldInclude(ChangelogEntryType.Enhancement) && entriesByType.ContainsKey(ChangelogEntryType.Enhancement) ) - yield return $"{repo}-{anchorSlug}-features-enhancements"; + yield return $"{repo}-{anchorSlug}-enhancements"; if (!dedicatedPage && shouldInclude(ChangelogEntryType.BugFix) && entriesByType.ContainsKey(ChangelogEntryType.BugFix)) yield return $"{repo}-{anchorSlug}-fixes"; @@ -755,16 +807,11 @@ private IEnumerable ComputeTableOfContent() if (shouldInclude(ChangelogEntryType.Deprecation) && entriesByType.ContainsKey(ChangelogEntryType.Deprecation)) yield return new PageTocItem { Heading = "Deprecations", Slug = SectionSlug("deprecations"), Level = 3 }; - if ( - shouldInclude(ChangelogEntryType.Feature) - && (entriesByType.ContainsKey(ChangelogEntryType.Feature) || entriesByType.ContainsKey(ChangelogEntryType.Enhancement)) - ) - yield return new PageTocItem - { - Heading = "Features and enhancements", - Slug = SectionSlug("features-enhancements"), - Level = 3 - }; + if (shouldInclude(ChangelogEntryType.Feature) && entriesByType.ContainsKey(ChangelogEntryType.Feature)) + yield return new PageTocItem { Heading = "Features", Slug = SectionSlug("features"), Level = 3 }; + + if (shouldInclude(ChangelogEntryType.Enhancement) && entriesByType.ContainsKey(ChangelogEntryType.Enhancement)) + yield return new PageTocItem { Heading = "Enhancements", Slug = SectionSlug("enhancements"), Level = 3 }; if (shouldInclude(ChangelogEntryType.BugFix) && entriesByType.ContainsKey(ChangelogEntryType.BugFix)) yield return new PageTocItem { Heading = "Fixes", Slug = SectionSlug("fixes"), Level = 3 }; diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogDescriptionVisibility.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogDescriptionVisibility.cs index 00b51272ca..fcfda6631a 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogDescriptionVisibility.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogDescriptionVisibility.cs @@ -6,29 +6,41 @@ namespace Elastic.Markdown.Myst.Directives.Changelog; /// /// Controls changelog entry description (body text) rendering for the {changelog} directive. -/// Mirrors the structure of while using opposite privacy defaults for . +/// Combinable flags: at most one base token (, , or +/// ) plus optional overlays (, +/// ). /// +[Flags] public enum ChangelogDescriptionVisibility { + None = 0, + /// /// Hide record descriptions when the bundle has only public constituent repos (per assembler.yml); /// show when any constituent is private. With no private repos configured, hides descriptions everywhere. /// - Auto, + Auto = 1, /// /// Always render record descriptions when present in source YAML. /// - KeepDescriptions, + KeepDescriptions = 1 << 1, + + /// + /// Never render record descriptions (including dropdown authoring placeholders), except where an overlay applies. + /// + HideDescriptions = 1 << 2, /// - /// Always render record descriptions in the Highlights section only; hide them in every other section. - /// When :highlights: is omitted, descriptions are hidden everywhere. + /// Always render record descriptions in the Highlights section. Combine with other tokens to control remaining sections. + /// When used alone, descriptions are hidden everywhere except Highlights. When :highlights: is omitted, the + /// Highlights overlay has no visible effect. /// - KeepHighlightDescriptions, + KeepHighlightDescriptions = 1 << 3, /// - /// Never render record descriptions (including dropdown authoring placeholders). + /// Always render record descriptions in the Features section. Combine with other tokens to control remaining sections. + /// When used alone, descriptions are hidden everywhere except Features. /// - HideDescriptions + KeepFeatureDescriptions = 1 << 4 } diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs index ace3a5e704..a252e37093 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs @@ -198,14 +198,25 @@ public static bool ShouldHideEntryDescriptionsForRepo( string bundleRepo, HashSet privateRepositories, ChangelogDescriptionVisibility visibility - ) => visibility switch + ) { - ChangelogDescriptionVisibility.HideDescriptions => true, - ChangelogDescriptionVisibility.KeepHighlightDescriptions => true, - ChangelogDescriptionVisibility.KeepDescriptions => false, - ChangelogDescriptionVisibility.Auto => !HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories), - _ => !HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories) - }; + if (visibility.HasFlag(ChangelogDescriptionVisibility.KeepDescriptions)) + return false; + + if (visibility.HasFlag(ChangelogDescriptionVisibility.HideDescriptions)) + return true; + + if (visibility.HasFlag(ChangelogDescriptionVisibility.Auto)) + return !HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories); + + if ( + visibility.HasFlag(ChangelogDescriptionVisibility.KeepHighlightDescriptions) + || visibility.HasFlag(ChangelogDescriptionVisibility.KeepFeatureDescriptions) + ) + return true; + + return !HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories); + } /// /// True when merged (elasticsearch+kibana-style) has at least one @@ -242,7 +253,9 @@ private static string GenerateMarkdown(BundleRenderModel model, ChangelogRenderO var subsections = options.Subsections; var hideLinks = model.HideLinks; var hideEntryDescriptions = model.HideEntryDescriptions; - var hideHighlightDescriptions = options.DescriptionVisibility is not ChangelogDescriptionVisibility.KeepHighlightDescriptions + var hideHighlightDescriptions = !options.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.KeepHighlightDescriptions) + && hideEntryDescriptions; + var hideFeatureDescriptions = !options.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.KeepFeatureDescriptions) && hideEntryDescriptions; var dropdownsEnabled = options.DropdownsEnabled; var typeFilter = options.TypeFilter; @@ -306,6 +319,7 @@ private static string GenerateMarkdown(BundleRenderModel model, ChangelogRenderO groupBySubtype: true, hideLinks, hideEntryDescriptions, + subsections, publishBlocker ); else @@ -325,6 +339,7 @@ private static string GenerateMarkdown(BundleRenderModel model, ChangelogRenderO groupBySubtype: false, hideLinks, hideHighlightDescriptions, + subsections, publishBlocker ); else @@ -372,12 +387,31 @@ private static string GenerateMarkdown(BundleRenderModel model, ChangelogRenderO ); } - if (features.Count > 0 || enhancements.Count > 0) + if (features.Count > 0) { _ = sb.AppendLine(); - _ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Features and enhancements [{repo}-{titleSlug}-features-enhancements]"); - var combined = features.Concat(enhancements).ToList(); - RenderEntriesByArea(sb, combined, repo, owner, subsections, hideLinks, hideEntryDescriptions, publishBlocker); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Features [{repo}-{titleSlug}-features]"); + if (dropdownsEnabled) + RenderDetailedEntries( + sb, + features, + repo, + owner, + groupBySubtype: false, + hideLinks, + hideFeatureDescriptions, + subsections, + publishBlocker + ); + else + RenderEntriesByArea(sb, features, repo, owner, subsections, hideLinks, hideFeatureDescriptions, publishBlocker); + } + + if (enhancements.Count > 0) + { + _ = sb.AppendLine(); + _ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Enhancements [{repo}-{titleSlug}-enhancements]"); + RenderEntriesByArea(sb, enhancements, repo, owner, subsections, hideLinks, hideEntryDescriptions, publishBlocker); } if (bugFixes.Count > 0) @@ -509,12 +543,11 @@ private static void RenderDetailedEntries( bool groupBySubtype, bool hideLinks, bool hideEntryDescriptions, + bool subsections, PublishBlocker? publishBlocker ) { - var grouped = groupBySubtype - ? entries.GroupBy(e => e.Subtype?.ToStringFast(true) ?? string.Empty).OrderBy(g => g.Key).ToList() - : entries.GroupBy(e => publishBlocker.GetPreferredArea(e)).OrderBy(g => g.Key).ToList(); + var grouped = GroupDetailedEntries(entries, groupBySubtype, subsections, publishBlocker); foreach (var group in grouped) { @@ -532,6 +565,22 @@ private static void RenderDetailedEntries( } } + private static List> GroupDetailedEntries( + List entries, + bool groupBySubtype, + bool subsections, + PublishBlocker? publishBlocker + ) + { + if (groupBySubtype) + return entries.GroupBy(e => e.Subtype?.ToStringFast(true) ?? string.Empty).OrderBy(g => g.Key).ToList(); + + if (!subsections) + return entries.GroupBy(_ => string.Empty).ToList(); + + return entries.GroupBy(e => publishBlocker.GetPreferredArea(e)).OrderBy(g => g.Key).ToList(); + } + private static void RenderDetailedEntriesFlattened( StringBuilder sb, List entries, @@ -812,7 +861,7 @@ private static void RenderSeparatedTypeEntries( { if (dropdownsEnabled) { - RenderDetailedEntries(sb, entries, repo, owner, groupBySubtype, hideLinks, hideEntryDescriptions, publishBlocker); + RenderDetailedEntries(sb, entries, repo, owner, groupBySubtype, hideLinks, hideEntryDescriptions, subsections, publishBlocker); return; } diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs index 93229ca8e4..e9b4cb67a7 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs @@ -83,12 +83,17 @@ public async Task RenderAsciidoc(ChangelogRenderContext context, Cancel ctx) _ = sb.AppendLine(); } - // Render features and enhancements - if (features.Count > 0 || enhancements.Count > 0) + if (features.Count > 0) { - RenderSectionHeader(sb, "features-enhancements", context.TitleSlug, "New features and enhancements"); - var combined = features.Concat(enhancements).ToList(); - entriesByAreaRenderer.Render(combined, context); + RenderSectionHeader(sb, "features", context.TitleSlug, "Features"); + entriesByAreaRenderer.Render(features, context); + _ = sb.AppendLine(); + } + + if (enhancements.Count > 0) + { + RenderSectionHeader(sb, "enhancements", context.TitleSlug, "Enhancements"); + entriesByAreaRenderer.Render(enhancements, context); _ = sb.AppendLine(); } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs index 3a3b981301..40a69fc3a8 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs @@ -71,16 +71,18 @@ bool AllEntriesHidden(IReadOnlyCollection entries) => _ = sb.AppendLine(); } - // Features and enhancements - if (features.Count > 0 || enhancements.Count > 0) + if (features.Count > 0 && !AllEntriesHidden(features)) { - var combined = features.Concat(enhancements).ToList(); - if (!AllEntriesHidden(combined)) - { - _ = sb.AppendLine("### Features and enhancements"); - RenderEntriesByArea(sb, combined, context); - _ = sb.AppendLine(); - } + _ = sb.AppendLine("### Features"); + RenderEntriesByArea(sb, features, context); + _ = sb.AppendLine(); + } + + if (enhancements.Count > 0 && !AllEntriesHidden(enhancements)) + { + _ = sb.AppendLine("### Enhancements"); + RenderEntriesByArea(sb, enhancements, context); + _ = sb.AppendLine(); } // Breaking changes diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs index 203a343be8..dbc1ca5aa8 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs @@ -61,25 +61,33 @@ bool AllEntriesHidden(IReadOnlyCollection entries) => entries.Count > 0 && entries.All(entry => ChangelogRenderUtilities.ShouldHideEntry(entry, context.FeatureIdsToHide, context)); // Check if each category has visible entries - var hasVisibleFeatures = (features.Count > 0 || enhancements.Count > 0) - && !(AllEntriesHidden(features) && AllEntriesHidden(enhancements)); + var hasVisibleFeatures = features.Count > 0 && !AllEntriesHidden(features); + var hasVisibleEnhancements = enhancements.Count > 0 && !AllEntriesHidden(enhancements); var hasVisibleFixes = (security.Count > 0 || bugFixes.Count > 0) && !(AllEntriesHidden(security) && AllEntriesHidden(bugFixes)); var hasVisibleDocs = docs.Count > 0 && !AllEntriesHidden(docs); var hasVisibleRegressions = regressions.Count > 0 && !AllEntriesHidden(regressions); var hasVisibleOther = other.Count > 0 && !AllEntriesHidden(other); - var hasAnyVisibleEntries = hasVisibleFeatures || hasVisibleFixes || hasVisibleDocs || hasVisibleRegressions || hasVisibleOther; + var hasAnyVisibleEntries = hasVisibleFeatures + || hasVisibleEnhancements + || hasVisibleFixes + || hasVisibleDocs + || hasVisibleRegressions + || hasVisibleOther; if (hasAnyEntries) { - if (features.Count > 0 || enhancements.Count > 0) + if (features.Count > 0) { - var combined = features.Concat(enhancements).ToList(); - _ = sb.AppendLine( - InvariantCulture, - $"### Features and enhancements [{context.Repo}-{context.TitleSlug}-features-enhancements]" - ); - RenderEntriesByArea(sb, combined, context); + _ = sb.AppendLine(InvariantCulture, $"### Features [{context.Repo}-{context.TitleSlug}-features]"); + RenderEntriesByArea(sb, features, context); + } + + if (enhancements.Count > 0) + { + _ = sb.AppendLine(); + _ = sb.AppendLine(InvariantCulture, $"### Enhancements [{context.Repo}-{context.TitleSlug}-enhancements]"); + RenderEntriesByArea(sb, enhancements, context); } if (security.Count > 0 || bugFixes.Count > 0) diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs index ec5414cdd6..2277fc113f 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs @@ -70,13 +70,13 @@ public async Task RenderChangelogs_WithGfmFileType_CreatesSingleGfmFile() var content = await FileSystem.File.ReadAllTextAsync(outputFile, TestContext.Current.CancellationToken); content.Should().Contain("## 9.2.0"); - content.Should().Contain("### Features and enhancements"); + content.Should().Contain("### Features"); content.Should().Contain("Test feature"); content.Should().Contain("[#100](https://github.com/elastic/elasticsearch/pull/100)"); // Should NOT contain anchor brackets in headings content.Should().NotContain("## 9.2.0 ["); - content.Should().NotContain("### Features and enhancements ["); + content.Should().NotContain("### Features ["); } [Fact] @@ -175,7 +175,7 @@ public async Task RenderChangelogs_WithGfmFileType_IncludesAllSectionTypes() var content = await FileSystem.File.ReadAllTextAsync(outputChangelogPath, TestContext.Current.CancellationToken); // Should include all section types in the proper order - content.Should().Contain("### Features and enhancements"); + content.Should().Contain("### Features"); content.Should().Contain("### Breaking changes"); content.Should().Contain("### Deprecations"); content.Should().Contain("### Bug fixes"); @@ -189,7 +189,7 @@ public async Task RenderChangelogs_WithGfmFileType_IncludesAllSectionTypes() content.Should().Contain("Known issue"); // Check section ordering (features should come before breaking changes) - var featuresIndex = content.IndexOf("### Features and enhancements", StringComparison.Ordinal); + var featuresIndex = content.IndexOf("### Features", StringComparison.Ordinal); var breakingIndex = content.IndexOf("### Breaking changes", StringComparison.Ordinal); var deprecationIndex = content.IndexOf("### Deprecations", StringComparison.Ordinal); var bugFixIndex = content.IndexOf("### Bug fixes", StringComparison.Ordinal); @@ -266,11 +266,11 @@ public async Task RenderChangelogs_WithGfmFileType_HandlesHighlights() // Should include highlights section first content.Should().Contain("### Highlights"); - content.Should().Contain("### Features and enhancements"); + content.Should().Contain("### Features"); // Highlights should come first var highlightsIndex = content.IndexOf("### Highlights", StringComparison.Ordinal); - var featuresIndex = content.IndexOf("### Features and enhancements", StringComparison.Ordinal); + var featuresIndex = content.IndexOf("### Features", StringComparison.Ordinal); highlightsIndex.Should().BeLessThan(featuresIndex); // Both features should be present diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs index 7c1dccad01..4815e1c951 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs @@ -141,8 +141,8 @@ public async Task RenderChangelogs_WithAsciidocFileType_CreatesSingleAsciidocFil // Verify valid asciidoc format elements asciidocContent.Should().Contain("[[release-notes-", "should contain anchor"); asciidocContent.Should().Contain("== 9.2.0", "should contain section header"); - asciidocContent.Should().Contain("[[features-enhancements-", "should contain features section anchor"); - asciidocContent.Should().Contain("=== New features and enhancements", "should contain features section header"); + asciidocContent.Should().Contain("[[features-", "should contain features section anchor"); + asciidocContent.Should().Contain("=== Features", "should contain features section header"); asciidocContent.Should().Contain("* Test feature", "should contain changelog entry"); asciidocContent.Should().Contain("This is a test feature", "should contain description"); @@ -247,8 +247,8 @@ public async Task RenderChangelogs_WithAsciidocFileType_ValidatesAsciidocFormat( asciidocContent.Should().Contain("[float]", "should contain float attribute"); asciidocContent.Should().Contain("=== Bug fixes", "should contain bug fixes header"); - asciidocContent.Should().Contain("[[features-enhancements-9.2.0]]", "should contain features anchor"); - asciidocContent.Should().Contain("=== New features and enhancements", "should contain features header"); + asciidocContent.Should().Contain("[[features-9.2.0]]", "should contain features anchor"); + asciidocContent.Should().Contain("=== Features", "should contain features header"); asciidocContent.Should().Contain("[[breaking-changes-9.2.0]]", "should contain breaking changes anchor"); asciidocContent.Should().Contain("=== Breaking changes", "should contain breaking changes header"); diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs index 47888bf488..96b769d838 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs @@ -77,7 +77,7 @@ public void SetsCorrectBundlesFolderPath() => public void RendersMarkdownContent() { Html.Should().Contain("9.3.0"); - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Add new feature"); Html.Should().Contain("Fixes"); Html.Should().Contain("Fix important bug"); @@ -490,7 +490,7 @@ protected override IReleaseNotesResolver GetReleaseNotesResolver() => public void RendersCdnBundleBody() { Html.Should().Contain("9.4.0"); - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Enhancements"); Html.Should().Contain("Faster vector search on the CDN"); } } @@ -1002,7 +1002,7 @@ public ChangelogSectionOrderTests(ITestOutputHelper output) : base( public void BreakingChangesAppearsFirst() { var breakingIdx = Html.IndexOf("Breaking changes", StringComparison.Ordinal); - var featuresIdx = Html.IndexOf("Features and enhancements", StringComparison.Ordinal); + var featuresIdx = Html.IndexOf("Features", StringComparison.Ordinal); var fixesIdx = Html.IndexOf(">Fixes<", StringComparison.Ordinal); breakingIdx.Should().BeLessThan(featuresIdx, "Breaking changes should appear before Features"); @@ -1013,7 +1013,7 @@ public void BreakingChangesAppearsFirst() public void SecurityAppearsBeforeFeatures() { var securityIdx = Html.IndexOf(">Security<", StringComparison.Ordinal); - var featuresIdx = Html.IndexOf("Features and enhancements", StringComparison.Ordinal); + var featuresIdx = Html.IndexOf("Features", StringComparison.Ordinal); securityIdx.Should().BeLessThan(featuresIdx, "Security should appear before Features"); } @@ -1022,7 +1022,7 @@ public void SecurityAppearsBeforeFeatures() public void KnownIssuesAppearsBeforeFeatures() { var knownIssuesIdx = Html.IndexOf("Known issues", StringComparison.Ordinal); - var featuresIdx = Html.IndexOf("Features and enhancements", StringComparison.Ordinal); + var featuresIdx = Html.IndexOf("Features", StringComparison.Ordinal); knownIssuesIdx.Should().BeLessThan(featuresIdx, "Known issues should appear before Features"); } @@ -1031,7 +1031,7 @@ public void KnownIssuesAppearsBeforeFeatures() public void DeprecationsAppearsBeforeFeatures() { var deprecationsIdx = Html.IndexOf("Deprecations", StringComparison.Ordinal); - var featuresIdx = Html.IndexOf("Features and enhancements", StringComparison.Ordinal); + var featuresIdx = Html.IndexOf("Features", StringComparison.Ordinal); deprecationsIdx.Should().BeLessThan(featuresIdx, "Deprecations should appear before Features"); } diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs index 0f15b2a764..cc5a8befdf 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs @@ -89,7 +89,7 @@ public void RendersAllEntries_NoFiltering() } [Fact] - public void RendersFeaturesSection() => Html.Should().Contain("Features and enhancements"); + public void RendersFeaturesSection() => Html.Should().Contain("Features"); [Fact] public void RendersDeprecationsSection() => Html.Should().Contain("Deprecations"); diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogDescriptionVisibilityTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogDescriptionVisibilityTests.cs index 637f624d0d..35677b48ee 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogDescriptionVisibilityTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogDescriptionVisibilityTests.cs @@ -38,18 +38,37 @@ public void KeepDescriptions_AlwaysReturnsFalse() } [Fact] - public void KeepHighlightDescriptions_AlwaysReturnsTrue() + public void KeepFeatureDescriptions_AlwaysReturnsTrue() { - // Default path hides descriptions; Highlights section overrides separately in the renderer. var result = ChangelogInlineRenderer.ShouldHideEntryDescriptionsForRepo( "kibana", [], - ChangelogDescriptionVisibility.KeepHighlightDescriptions + ChangelogDescriptionVisibility.KeepFeatureDescriptions ); result.Should().BeTrue(); } + [Fact] + public void CombinedFeatureAndHighlightOverlays_AlwaysReturnsTrue() + { + var visibility = ChangelogDescriptionVisibility.KeepFeatureDescriptions | ChangelogDescriptionVisibility.KeepHighlightDescriptions; + + var result = ChangelogInlineRenderer.ShouldHideEntryDescriptionsForRepo("kibana", [], visibility); + + result.Should().BeTrue(); + } + + [Fact] + public void AutoPlusFeatureOverlay_WithPublicRepo_HidesDefaultBodies() + { + var visibility = ChangelogDescriptionVisibility.Auto | ChangelogDescriptionVisibility.KeepFeatureDescriptions; + + var result = ChangelogInlineRenderer.ShouldHideEntryDescriptionsForRepo("kibana", [], visibility); + + result.Should().BeTrue(); + } + [Fact] public void Auto_WithEmptyPrivateRepos_HidesBodies() { @@ -313,3 +332,234 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) => [Fact] public void AutoTreatsFullyPublic_AsHideBody() => Html.Should().NotContain("BODY_INVALID_VISIBILITY"); } + +public class ChangelogKeepFeatureDescriptionsTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + :description-visibility: keep-feature-descriptions + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature with body + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_ONLY_BODY + - title: Enhancement with body + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + description: ENHANCEMENT_HIDDEN_BODY + """ + ) + ); + + [Fact] + public void ParsesKeepFeatureDescriptions() => + Block!.DescriptionVisibility.Should().Be(ChangelogDescriptionVisibility.KeepFeatureDescriptions); + + [Fact] + public void ShowsFeatureBodies() => Html.Should().Contain("FEATURE_ONLY_BODY"); + + [Fact] + public void HidesEnhancementBodies() => Html.Should().NotContain("ENHANCEMENT_HIDDEN_BODY"); +} + +public class ChangelogCombinedFeatureAndHighlightDescriptionsTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + :highlights: + :description-visibility: keep-feature-descriptions, keep-highlight-descriptions + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Highlighted feature + type: feature + highlight: true + products: + - product: elasticsearch + target: 9.3.0 + description: HIGHLIGHT_SECTION_BODY + - title: Regular feature + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_SECTION_BODY + - title: Enhancement with body + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + description: ENHANCEMENT_HIDDEN_BODY + """ + ) + ); + + [Fact] + public void ParsesBothOverlays() + { + Block!.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.KeepFeatureDescriptions).Should().BeTrue(); + Block!.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.KeepHighlightDescriptions).Should().BeTrue(); + } + + [Fact] + public void ShowsHighlightBodies() => Html.Should().Contain("HIGHLIGHT_SECTION_BODY"); + + [Fact] + public void ShowsFeatureBodies() => Html.Should().Contain("FEATURE_SECTION_BODY"); + + [Fact] + public void HidesEnhancementBodies() => Html.Should().NotContain("ENHANCEMENT_HIDDEN_BODY"); +} + +public class ChangelogAutoPlusFeatureDescriptionsTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + :description-visibility: auto, keep-feature-descriptions + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature with body + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_AUTO_OVERLAY_BODY + - title: Enhancement with body + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + description: ENHANCEMENT_AUTO_BODY + """ + ) + ); + + [Fact] + public void ParsesAutoPlusFeatureOverlay() + { + Block!.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.Auto).Should().BeTrue(); + Block!.DescriptionVisibility.HasFlag(ChangelogDescriptionVisibility.KeepFeatureDescriptions).Should().BeTrue(); + } + + [Fact] + public void ShowsFeatureBodiesOnPublicRepos() => Html.Should().Contain("FEATURE_AUTO_OVERLAY_BODY"); + + [Fact] + public void HidesEnhancementBodiesOnPublicRepos() => Html.Should().NotContain("ENHANCEMENT_AUTO_BODY"); +} + +public class ChangelogDuplicateBaseDescriptionVisibilityTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + :description-visibility: keep-descriptions, hide-descriptions + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature keep first + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: BODY_FIRST_BASE_WINS + """ + ) + ); + + [Fact] + public void KeepsFirstBaseToken() => Block!.DescriptionVisibility.Should().Be(ChangelogDescriptionVisibility.KeepDescriptions); + + [Fact] + public void EmitsWarning() => Collector.Warnings.Should().BeGreaterThan(0); + + [Fact] + public void RendersBodiesUsingFirstBase() => Html.Should().Contain("BODY_FIRST_BASE_WINS"); +} + +public class ChangelogHideDescriptionsPlusFeatureOverlayTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + :description-visibility: hide-descriptions, keep-feature-descriptions + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature with body + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_OVERLAY_WINS + - title: Enhancement with body + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + description: ENHANCEMENT_STILL_HIDDEN + """ + ) + ); + + [Fact] + public void ShowsFeatureBodiesDespiteHideBase() => Html.Should().Contain("FEATURE_OVERLAY_WINS"); + + [Fact] + public void HidesEnhancementBodies() => Html.Should().NotContain("ENHANCEMENT_STILL_HIDDEN"); +} diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogDropdownsTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogDropdownsTests.cs index e0cb6fe228..3987157ebf 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogDropdownsTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogDropdownsTests.cs @@ -10,9 +10,9 @@ namespace Elastic.Markdown.Tests.Directives; /// /// Tests for the :dropdowns: parameter on the changelog directive. -/// By default (omitted), separated types (breaking changes, deprecations, known issues, highlights) -/// are rendered as flattened bulleted lists. -/// With :dropdowns:, they render as Myst dropdown sections. +/// By default (omitted), separated types (breaking changes, deprecations, known issues, highlights) +/// are rendered as flattened bulleted lists; Features stay flat bullets. +/// With :dropdowns:, those types and Features render as Myst dropdown sections. /// public class ChangelogDropdownsDefaultTests : DirectiveTest { @@ -408,15 +408,12 @@ public ChangelogDropdownsExplicitWithDifferentTypesTests(ITestOutputHelper outpu [Fact] public void ExplicitDropdownsRendersMixedTypesCorrectly() { - // Regular types should still render as bulleted lists (unchanged behavior) - Html.Should().Contain("Feature addition."); // Regular feature type (in
  • tags) - - // Separated types should render as dropdowns (explicit :dropdowns:) Html.Should().Contain("
    "); + Html.Should().Contain("Feature addition."); Html.Should().Contain("Breaking API change."); Html.Should().Contain("Known issue with search."); - // Should NOT contain flattened format for separated types (check they're in dropdown, not flat list) + Html.Should().NotContain("
  • Feature addition."); Html.Should().NotContain("

  • Breaking API change."); Html.Should().NotContain("

  • Known issue with search."); } @@ -468,3 +465,255 @@ public void ChangelogDropdownTitleStripsBackticksInHtml() Html.Should().NotContain("`ElasticAgentVersion`"); } } + +public class ChangelogDropdownsDefaultFeaturesStayListTests : DirectiveTest +{ + public ChangelogDropdownsDefaultFeaturesStayListTests(ITestOutputHelper output) : base( + output, + """ + :::{changelog} + :description-visibility: keep-descriptions + ::: + """ + ) => + FileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature addition + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_FLAT_BODY + prs: + - "111111" + """ + ) + ); + + [Fact] + public void FeaturesStayBulletsWithoutDropdownsOption() + { + Html.Should().NotContain("

    "); + Html.Should().Contain("
  • "); + Html.Should().Contain("Feature addition"); + Html.Should().Contain("FEATURE_FLAT_BODY"); + } +} + +public class ChangelogDropdownsFeaturesNotEnhancementsTests : DirectiveTest +{ + public ChangelogDropdownsFeaturesNotEnhancementsTests(ITestOutputHelper output) : base( + output, + """ + :::{changelog} + :dropdowns: + :description-visibility: keep-feature-descriptions + ::: + """ + ) => + FileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature addition + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_DROPDOWN_BODY + prs: + - "111111" + - title: Faster existing query + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + description: ENHANCEMENT_LIST_BODY + prs: + - "222222" + """ + ) + ); + + [Fact] + public void FeaturesRenderAsDropdowns() + { + Html.Should().Contain("
    "); + Html.Should().Contain("Feature addition"); + Html.Should().Contain("FEATURE_DROPDOWN_BODY"); + Html.Should().NotContain("
  • Feature addition"); + } + + [Fact] + public void EnhancementsStayBullets() + { + Html.Should().Contain("Faster existing query"); + Html.Should().NotContain("ENHANCEMENT_LIST_BODY"); + } +} + +public class ChangelogDropdownsFeaturesAutoHidesDescriptionTests : DirectiveTest +{ + public ChangelogDropdownsFeaturesAutoHidesDescriptionTests(ITestOutputHelper output) : base( + output, + """ + :::{changelog} + :dropdowns: + ::: + """ + ) => + FileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature addition + type: feature + products: + - product: elasticsearch + target: 9.3.0 + description: FEATURE_AUTO_HIDDEN_BODY + prs: + - "111111" + """ + ) + ); + + [Fact] + public void RendersFeatureDropdownWithoutDescription() + { + Html.Should().Contain("

    "); + Html.Should().Contain("Feature addition"); + Html.Should().Contain("#111111"); + Html.Should().NotContain("FEATURE_AUTO_HIDDEN_BODY"); + } +} + +public class ChangelogDropdownsWithoutSubsectionsOmitsAreaHeadersTests : DirectiveTest +{ + public ChangelogDropdownsWithoutSubsectionsOmitsAreaHeadersTests(ITestOutputHelper output) : base( + output, + """ + :::{changelog} + :dropdowns: + :description-visibility: keep-feature-descriptions + ::: + """ + ) => + FileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature in Search + type: feature + products: + - product: elasticsearch + target: 9.3.0 + areas: + - Search + description: Search feature body + prs: + - "111111" + - title: Feature in Indexing + type: feature + products: + - product: elasticsearch + target: 9.3.0 + areas: + - Indexing + description: Indexing feature body + prs: + - "222222" + """ + ) + ); + + [Fact] + public void RendersDropdowns() => Html.Should().Contain("
    "); + + [Fact] + public void OmitsAreaHeaders() + { + Html.Should().NotContain("Search"); + Html.Should().NotContain("Indexing"); + } + + [Fact] + public void StillRendersFeatureTitles() + { + Html.Should().Contain("Feature in Search"); + Html.Should().Contain("Feature in Indexing"); + } +} + +public class ChangelogDropdownsWithSubsectionsRendersAreaHeadersTests : DirectiveTest +{ + public ChangelogDropdownsWithSubsectionsRendersAreaHeadersTests(ITestOutputHelper output) : base( + output, + """ + :::{changelog} + :dropdowns: + :subsections: + :description-visibility: keep-feature-descriptions + ::: + """ + ) => + FileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Feature in Search + type: feature + products: + - product: elasticsearch + target: 9.3.0 + areas: + - Search + description: Search feature body + prs: + - "111111" + - title: Feature in Indexing + type: feature + products: + - product: elasticsearch + target: 9.3.0 + areas: + - Indexing + description: Indexing feature body + prs: + - "222222" + """ + ) + ); + + [Fact] + public void RendersDropdowns() => Html.Should().Contain("
    "); + + [Fact] + public void RendersAreaHeaders() + { + Html.Should().Contain("Search"); + Html.Should().Contain("Indexing"); + } +} diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogFeatureEnhancementSectionTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogFeatureEnhancementSectionTests.cs new file mode 100644 index 0000000000..032506289e --- /dev/null +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogFeatureEnhancementSectionTests.cs @@ -0,0 +1,145 @@ +// 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.Abstractions.TestingHelpers; +using AwesomeAssertions; +using Elastic.Markdown.Myst.Directives.Changelog; + +namespace Elastic.Markdown.Tests.Directives; + +public class ChangelogFeatureAndEnhancementSectionsTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.0 + entries: + - title: Brand new capability + type: feature + products: + - product: elasticsearch + target: 9.3.0 + - title: Improved existing capability + type: enhancement + products: + - product: elasticsearch + target: 9.3.0 + - title: Bug fix + type: bug-fix + products: + - product: elasticsearch + target: 9.3.0 + """ + ) + ); + + [Fact] + public void RendersSeparateFeaturesHeading() => Html.Should().Contain(">Features<"); + + [Fact] + public void RendersSeparateEnhancementsHeading() => Html.Should().Contain(">Enhancements<"); + + [Fact] + public void OmitsCombinedHeading() => Html.Should().NotContain("Features and enhancements"); + + [Fact] + public void FeaturesAppearBeforeEnhancements() + { + var featuresIdx = Html.IndexOf(">Features<", StringComparison.Ordinal); + var enhancementsIdx = Html.IndexOf(">Enhancements<", StringComparison.Ordinal); + featuresIdx.Should().BeLessThan(enhancementsIdx); + } + + [Fact] + public void TocIncludesBothSections() + { + var toc = Block!.GeneratedTableOfContent.ToList(); + toc.Should().Contain(t => t.Heading == "Features" && t.Slug.EndsWith("-features")); + toc.Should().Contain(t => t.Heading == "Enhancements" && t.Slug.EndsWith("-enhancements")); + } +} + +public class ChangelogEnhancementsOnlyOmitsFeaturesSectionTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.3.6.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.3.6 + entries: + - title: Faster existing query + type: enhancement + products: + - product: elasticsearch + target: 9.3.6 + """ + ) + ); + + [Fact] + public void RendersEnhancements() => Html.Should().Contain(">Enhancements<"); + + [Fact] + public void OmitsEmptyFeaturesSection() => Html.Should().NotContain(">Features<"); + + [Fact] + public void TocOmitsFeatures() + { + var toc = Block!.GeneratedTableOfContent.ToList(); + toc.Should().NotContain(t => t.Heading == "Features"); + toc.Should().Contain(t => t.Heading == "Enhancements"); + } +} + +public class ChangelogFeaturesOnlyOmitsEnhancementsSectionTests(ITestOutputHelper output) : DirectiveTest( + output, + """ + :::{changelog} + ::: + """ +) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile( + "docs/changelog/bundles/9.4.0.yaml", + new MockFileData( + """ + products: + - product: elasticsearch + target: 9.4.0 + entries: + - title: Entirely new API + type: feature + products: + - product: elasticsearch + target: 9.4.0 + """ + ) + ); + + [Fact] + public void RendersFeatures() => Html.Should().Contain(">Features<"); + + [Fact] + public void OmitsEmptyEnhancementsSection() => Html.Should().NotContain(">Enhancements<"); +} diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs index 39f7dd567d..50a9929d50 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs @@ -453,7 +453,7 @@ public void MergedBundle_TocSlug_MatchesHeadingId() { var section = Block!.GeneratedTableOfContent.Single(t => t.Level == 3); - section.Slug.Should().Be("elasticsearchkibana-2025-08-05-features-enhancements"); + section.Slug.Should().Be("elasticsearchkibana-2025-08-05-features"); Html.Should().Contain($"id=\"{section.Slug}\""); } diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogHighlightsOptionTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogHighlightsOptionTests.cs index c9eddb2289..ce62607bb5 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogHighlightsOptionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogHighlightsOptionTests.cs @@ -78,7 +78,7 @@ public ChangelogHighlightsOptionDefaultOffTests(ITestOutputHelper output) : base [Fact] public void StillRendersHighlightedEntryUnderTypeSection() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Highlighted feature"); Html.Should().Contain("Regular feature"); Html.Should().Contain("Fixes"); @@ -118,7 +118,7 @@ public ChangelogHighlightsOptionEnabledTests(ITestOutputHelper output) : base( [Fact] public void DuplicatesHighlightedEntryInTypeSection() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Highlighted feature"); Html.Should().Contain("Regular feature"); } @@ -157,7 +157,7 @@ public void RendersHighlightsAndSeparatedTypes() { Html.Should().Contain("Highlights"); Html.Should().Contain("Breaking changes"); - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); } } @@ -218,7 +218,7 @@ public void EmitsWarningPointingToHighlightsOption() => [Fact] public void RendersDefaultTypeSectionsNotHighlightsOnly() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Highlighted feature"); Html.Should().NotContain("id=\"elasticsearch-9.3.0-highlights\""); } @@ -300,7 +300,7 @@ public void ParsesKeepHighlightDescriptions() => [Fact] public void StillRendersTitlesInTypeSections() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Highlighted feature"); Html.Should().Contain("Regular feature"); } diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs index 61fada3446..72e09876bc 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs @@ -94,7 +94,7 @@ public void TocIncludesOtherSection() public void TocIncludesFeaturesSection() { var tocItems = Block!.GeneratedTableOfContent.ToList(); - tocItems.Should().Contain(t => t.Heading == "Features and enhancements"); + tocItems.Should().Contain(t => t.Heading == "Features"); } [Fact] @@ -123,7 +123,7 @@ public void HtmlContainsAllSections() { Html.Should().Contain("Documentation"); Html.Should().Contain("Other changes"); - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); } } @@ -193,7 +193,7 @@ public void TocExcludesHiddenOtherSection() public void TocRetainsVisibleFeaturesSection() { var tocItems = Block!.GeneratedTableOfContent.ToList(); - tocItems.Should().Contain(t => t.Heading == "Features and enhancements"); + tocItems.Should().Contain(t => t.Heading == "Features"); } [Fact] @@ -207,7 +207,7 @@ public void AnchorsExcludeHiddenOtherSection() public void AnchorsRetainVisibleFeaturesSection() { var anchors = Block!.GeneratedAnchors.ToList(); - anchors.Should().Contain(a => a.Contains("features-enhancements")); + anchors.Should().Contain(a => a.Contains("features")); } [Fact] @@ -215,7 +215,7 @@ public void HtmlMatchesTocFiltering() { Html.Should().NotContain("Other changes"); Html.Should().NotContain("Hidden other change"); - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Visible feature"); } } @@ -371,7 +371,7 @@ public void TocExcludesHideFeatureFilteredSection() public void TocRetainsUnfilteredSection() { var tocItems = Block!.GeneratedTableOfContent.ToList(); - tocItems.Should().Contain(t => t.Heading == "Features and enhancements"); + tocItems.Should().Contain(t => t.Heading == "Features"); } [Fact] @@ -385,7 +385,7 @@ public void AnchorsExcludeOnlyHideFeatureFilteredSection() [Fact] public void HtmlMatchesTocAndAnchors() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Visible feature"); Html.Should().Contain("Documentation"); Html.Should().Contain("Docs entry blocked by type"); @@ -483,7 +483,7 @@ public void TocIncludesOtherSection() public void TocIncludesFeaturesSection() { var tocItems = Block!.GeneratedTableOfContent.ToList(); - tocItems.Should().Contain(t => t.Heading == "Features and enhancements"); + tocItems.Should().Contain(t => t.Heading == "Features"); } [Fact] @@ -492,7 +492,7 @@ public void AnchorsIncludeAllSections() var anchors = Block!.GeneratedAnchors.ToList(); anchors.Should().Contain(a => a.Contains("-docs")); anchors.Should().Contain(a => a.EndsWith("-other")); - anchors.Should().Contain(a => a.Contains("features-enhancements")); + anchors.Should().Contain(a => a.Contains("features")); } } @@ -658,7 +658,7 @@ public void BothVersionsHaveDocumentationInToc() public void FirstVersionRetainsFeaturesInToc() { var tocItems = Block!.GeneratedTableOfContent.ToList(); - tocItems.Should().Contain(t => t.Heading == "Features and enhancements"); + tocItems.Should().Contain(t => t.Heading == "Features"); } [Fact] diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs index 25ed1f3e81..045d798425 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs @@ -89,7 +89,7 @@ public ChangelogTypeFilterDefaultTests(ITestOutputHelper output) : base( [Fact] public void DefaultBehaviorShowsFeatures() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("New feature"); } @@ -199,7 +199,7 @@ public ChangelogTypeFilterAllTests(ITestOutputHelper output) : base( [Fact] public void ShowsAllEntryTypes() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("New feature"); Html.Should().Contain(">Fixes<"); Html.Should().Contain("Bug fix"); @@ -303,7 +303,7 @@ public void ShowsBreakingChanges() [Fact] public void ExcludesOtherTypes() { - Html.Should().NotContain("Features and enhancements"); + Html.Should().NotContain("Features"); Html.Should().NotContain("New feature"); Html.Should().NotContain("Known issues"); Html.Should().NotContain("Known issue"); @@ -379,7 +379,7 @@ public void ShowsDeprecations() [Fact] public void ExcludesOtherTypes() { - Html.Should().NotContain("Features and enhancements"); + Html.Should().NotContain("Features"); Html.Should().NotContain("New feature"); } } @@ -453,7 +453,7 @@ public void ShowsKnownIssues() [Fact] public void ExcludesOtherTypes() { - Html.Should().NotContain("Features and enhancements"); + Html.Should().NotContain("Features"); Html.Should().NotContain("New feature"); } } @@ -511,7 +511,7 @@ public ChangelogTypeFilterInvalidTests(ITestOutputHelper output) : base( [Fact] public void DefaultBehaviorIsApplied() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("New feature"); Html.Should().NotContain("Breaking changes"); } @@ -567,7 +567,7 @@ public ChangelogTypeFilterCaseInsensitiveTests(ITestOutputHelper output) : base( [Fact] public void ShowsAllTypes() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Breaking changes"); } } @@ -638,7 +638,7 @@ public void TypeFilterAndSubsectionsBothWork() [Fact] public void ShowsAllTypesWithSubsections() { - Html.Should().Contain("Features and enhancements"); + Html.Should().Contain("Features"); Html.Should().Contain("Search feature"); Html.Should().Contain("Indexing feature"); Html.Should().Contain("Breaking changes"); @@ -695,7 +695,7 @@ public void GeneratedAnchorsRespectTypeFilter() var anchors = Block!.GeneratedAnchors.ToList(); anchors.Should().NotContain(a => a.Contains("breaking-changes")); - anchors.Should().NotContain(a => a.Contains("features-enhancements")); + anchors.Should().NotContain(a => a.Contains("features")); } } @@ -750,7 +750,7 @@ public void TableOfContentsRespectTypeFilter() tocItems.Should().NotContain(t => t.Heading == "Deprecations"); tocItems.Should().Contain(t => t.Heading == "9.3.0" && t.Level == 2); - tocItems.Should().NotContain(t => t.Heading == "Features and enhancements"); + tocItems.Should().NotContain(t => t.Heading == "Features"); } }