Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/syntax/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +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` |
| `:config: path` | Path to `changelog.yml` configuration | auto-discover |

### Example with options
Expand All @@ -34,6 +35,7 @@ The directive supports the following options:
:type: all
:subsections:
:link-visibility: keep-links
:description-visibility: keep-descriptions
:::
```

Expand Down Expand Up @@ -114,6 +116,18 @@ Bundles whose repo is listed as private in `assembler.yml` hide links by default

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, and the first paragraph inside breaking-change, deprecation, known-issue, and highlight dropdowns). 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).

| 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 ⇒ record descriptions are omitted under `auto`. |
| `keep-descriptions` | Always render record 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. |
| `hide-descriptions` | Always omit record `description` bodies (titles, PR/issue links, Impact and Action sections, and bundle-level intros are unaffected). |

**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.

#### `:subsections:`

When enabled, entries are grouped by "area" within each section.
Expand Down Expand Up @@ -254,6 +268,8 @@ When present, the `release-date` field is rendered immediately after the version

Bundle descriptions are rendered when present in the bundle YAML file. The description appears after the release date (if any) but before any entry sections. Descriptions support Markdown formatting including links, lists, and multiple paragraphs.

**Record descriptions:** Each changelog entry may have its own `description` field in YAML (shown as body text under list items or as the introductory paragraph inside dropdowns). Visibility of **these** descriptions is controlled with `:description-visibility:` (defaults to `auto`; see Option details section). Do not confuse bundle `description` (intro prose) with per-record `description` (entry bodies).

### Section types

| Section | Entry type | Rendering |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ public class ChangelogBlock(DirectiveBlockParser parser, ParserContext context)
/// </summary>
public ChangelogLinkVisibility LinkVisibility { get; private set; }

/// <summary>
/// Visibility of changelog record <c>description</c> body text (see :description-visibility: option).
/// </summary>
public ChangelogDescriptionVisibility DescriptionVisibility { get; private set; }

/// <summary>
/// Returns all anchors that will be generated by this directive during rendering.
/// </summary>
Expand All @@ -183,6 +188,7 @@ public override void FinalizeAndValidate(ParserContext context)
LoadConfiguration();
LoadPrivateRepositories();
LinkVisibility = ParseLinkVisibility();
DescriptionVisibility = ParseDescriptionVisibility();
if (Found)
LoadAndCacheBundles();
}
Expand All @@ -209,6 +215,28 @@ private ChangelogLinkVisibility EmitInvalidLinkVisibilityWarning(string value)
return ChangelogLinkVisibility.Auto;
}

private ChangelogDescriptionVisibility ParseDescriptionVisibility()
{
var value = Prop("description-visibility");
if (string.IsNullOrWhiteSpace(value))
return ChangelogDescriptionVisibility.Auto;

return value.ToLowerInvariant() switch
{
"auto" => ChangelogDescriptionVisibility.Auto,
"keep-descriptions" => ChangelogDescriptionVisibility.KeepDescriptions,
"hide-descriptions" => ChangelogDescriptionVisibility.HideDescriptions,
_ => EmitInvalidDescriptionVisibilityWarning(value)
};
}

private ChangelogDescriptionVisibility EmitInvalidDescriptionVisibilityWarning(string value)
{
this.EmitWarning(
$"Invalid :description-visibility: value '{value}'. Valid values are: auto, keep-descriptions, hide-descriptions. Using auto.");
return ChangelogDescriptionVisibility.Auto;
}

/// <summary>
/// Parses and validates the :type: option.
/// Valid values: all, breaking-change, deprecation, known-issue, highlight.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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

namespace Elastic.Markdown.Myst.Directives.Changelog;

/// <summary>
/// Controls changelog entry description (body text) rendering for the {changelog} directive.
/// Mirrors the structure of <see cref="ChangelogLinkVisibility"/> while using opposite privacy defaults for <see cref="Auto"/>.
/// </summary>
public enum ChangelogDescriptionVisibility
{
/// <summary>
/// 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.
/// </summary>
Auto,

/// <summary>
/// Always render record descriptions when present in source YAML.
/// </summary>
KeepDescriptions,

/// <summary>
/// Never render record descriptions (including dropdown authoring placeholders).
/// </summary>
HideDescriptions
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public static class ChangelogInlineRenderer
block.PrivateRepositories,
block.HideFeatures,
typeFilter,
block.LinkVisibility);
block.LinkVisibility,
block.DescriptionVisibility);
_ = sb.Append(bundleMarkdown);

isFirst = false;
Expand All @@ -53,7 +54,8 @@ private static string RenderSingleBundle(
HashSet<string> privateRepositories,
HashSet<string> hideFeatures,
ChangelogTypeFilter typeFilter,
ChangelogLinkVisibility linkVisibility)
ChangelogLinkVisibility linkVisibility,
ChangelogDescriptionVisibility descriptionVisibility)
{
var titleSlug = ChangelogTextUtilities.TitleToSlug(bundle.Version);

Expand All @@ -78,8 +80,22 @@ private static string RenderSingleBundle(
_ => ShouldHideLinksForRepo(bundle.Repo, privateRepositories)
};

var hideEntryDescriptions = ShouldHideEntryDescriptionsForRepo(bundle.Repo, privateRepositories, descriptionVisibility);

var displayVersion = VersionOrDate.FormatDisplayVersion(bundle.Version);
return GenerateMarkdown(displayVersion, titleSlug, bundle.Repo, bundle.Owner, entriesByType, subsections, hideLinks, typeFilter, publishBlocker, bundle.Data?.Description, bundle.Data?.ReleaseDate);
return GenerateMarkdown(
displayVersion,
titleSlug,
bundle.Repo,
bundle.Owner,
entriesByType,
subsections,
hideLinks,
hideEntryDescriptions,
typeFilter,
publishBlocker,
bundle.Data?.Description,
bundle.Data?.ReleaseDate);
}

/// <summary>
Expand Down Expand Up @@ -123,10 +139,35 @@ public static bool ShouldHideLinksForRepo(string bundleRepo, HashSet<string> pri
if (privateRepositories.Count == 0)
return false;

// Split on '+' to handle merged bundles (e.g., "elasticsearch+kibana+private-repo")
return HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories);
}

/// <summary>
/// When true, changelog entry YAML <c>description</c> bodies (bullet text and dropdown intro) must not be rendered.
/// </summary>
public static bool ShouldHideEntryDescriptionsForRepo(
string bundleRepo,
HashSet<string> privateRepositories,
ChangelogDescriptionVisibility visibility) =>
visibility switch
{
ChangelogDescriptionVisibility.HideDescriptions => true,
ChangelogDescriptionVisibility.KeepDescriptions => false,
ChangelogDescriptionVisibility.Auto => !HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories),
_ => !HasAnyPrivateRepoConstituent(bundleRepo, privateRepositories)
};

/// <summary>
/// True when merged <paramref name="bundleRepo"/> (<c>elasticsearch+kibana</c>-style) has at least one
/// component listed as private for the build.
/// </summary>
public static bool HasAnyPrivateRepoConstituent(string bundleRepo, HashSet<string> privateRepositories)
{
if (privateRepositories.Count == 0)
return false;

var repos = bundleRepo.Split('+', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

// Hide links if ANY component repo is private
return repos.Any(privateRepositories.Contains);
}

Expand All @@ -151,6 +192,7 @@ private static string GenerateMarkdown(
Dictionary<ChangelogEntryType, List<ChangelogEntry>> entriesByType,
bool subsections,
bool hideLinks,
bool hideEntryDescriptions,
ChangelogTypeFilter typeFilter,
PublishBlocker? publishBlocker,
string? description = null,
Expand Down Expand Up @@ -207,79 +249,79 @@ private static string GenerateMarkdown(
if (typeFilter == ChangelogTypeFilter.Highlight)
{
if (highlights.Count > 0)
RenderDetailedEntries(sb, highlights, repo, owner, groupBySubtype: false, hideLinks, publishBlocker);
RenderDetailedEntries(sb, highlights, repo, owner, groupBySubtype: false, hideLinks, hideEntryDescriptions, publishBlocker);
return sb.ToString();
}

if (breakingChanges.Count > 0)
{
_ = sb.AppendLine();
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Breaking changes [{repo}-{titleSlug}-breaking-changes]");
RenderDetailedEntries(sb, breakingChanges, repo, owner, groupBySubtype: true, hideLinks, publishBlocker);
RenderDetailedEntries(sb, breakingChanges, repo, owner, groupBySubtype: true, hideLinks, hideEntryDescriptions, publishBlocker);
}

if (highlights.Count > 0 && typeFilter == ChangelogTypeFilter.All)
{
_ = sb.AppendLine();
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Highlights [{repo}-{titleSlug}-highlights]");
RenderDetailedEntries(sb, highlights, repo, owner, groupBySubtype: false, hideLinks, publishBlocker);
RenderDetailedEntries(sb, highlights, repo, owner, groupBySubtype: false, hideLinks, hideEntryDescriptions, publishBlocker);
}

if (security.Count > 0)
{
_ = sb.AppendLine();
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Security [{repo}-{titleSlug}-security]");
RenderEntriesByArea(sb, security, repo, owner, subsections, hideLinks, publishBlocker);
RenderEntriesByArea(sb, security, repo, owner, subsections, hideLinks, hideEntryDescriptions, publishBlocker);
}

if (knownIssues.Count > 0)
{
_ = sb.AppendLine();
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Known issues [{repo}-{titleSlug}-known-issues]");
RenderDetailedEntries(sb, knownIssues, repo, owner, groupBySubtype: false, hideLinks, publishBlocker);
RenderDetailedEntries(sb, knownIssues, repo, owner, groupBySubtype: false, hideLinks, hideEntryDescriptions, publishBlocker);
}

if (deprecations.Count > 0)
{
_ = sb.AppendLine();
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Deprecations [{repo}-{titleSlug}-deprecations]");
RenderDetailedEntries(sb, deprecations, repo, owner, groupBySubtype: false, hideLinks, publishBlocker);
RenderDetailedEntries(sb, deprecations, repo, owner, groupBySubtype: false, hideLinks, hideEntryDescriptions, publishBlocker);
}

if (features.Count > 0 || enhancements.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, publishBlocker);
RenderEntriesByArea(sb, combined, repo, owner, subsections, hideLinks, hideEntryDescriptions, publishBlocker);
}

if (bugFixes.Count > 0)
{
_ = sb.AppendLine();
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Fixes [{repo}-{titleSlug}-fixes]");
RenderEntriesByArea(sb, bugFixes, repo, owner, subsections, hideLinks, publishBlocker);
RenderEntriesByArea(sb, bugFixes, repo, owner, subsections, hideLinks, hideEntryDescriptions, publishBlocker);
}

if (docs.Count > 0)
{
_ = sb.AppendLine();
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Documentation [{repo}-{titleSlug}-docs]");
RenderEntriesByArea(sb, docs, repo, owner, subsections, hideLinks, publishBlocker);
RenderEntriesByArea(sb, docs, repo, owner, subsections, hideLinks, hideEntryDescriptions, publishBlocker);
}

if (regressions.Count > 0)
{
_ = sb.AppendLine();
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Regressions [{repo}-{titleSlug}-regressions]");
RenderEntriesByArea(sb, regressions, repo, owner, subsections, hideLinks, publishBlocker);
RenderEntriesByArea(sb, regressions, repo, owner, subsections, hideLinks, hideEntryDescriptions, publishBlocker);
}

if (other.Count > 0)
{
_ = sb.AppendLine();
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"### Other changes [{repo}-{titleSlug}-other]");
RenderEntriesByArea(sb, other, repo, owner, subsections, hideLinks, publishBlocker);
RenderEntriesByArea(sb, other, repo, owner, subsections, hideLinks, hideEntryDescriptions, publishBlocker);
}

return sb.ToString();
Expand All @@ -292,6 +334,7 @@ private static void RenderEntriesByArea(
string owner,
bool subsections,
bool hideLinks,
bool hideEntryDescriptions,
PublishBlocker? publishBlocker)
{
if (subsections)
Expand All @@ -309,29 +352,29 @@ private static void RenderEntriesByArea(
}

foreach (var entry in areaGroup)
RenderSingleEntry(sb, entry, repo, owner, hideLinks);
RenderSingleEntry(sb, entry, repo, owner, hideLinks, hideEntryDescriptions);
}
}
else
{
foreach (var entry in entries)
RenderSingleEntry(sb, entry, repo, owner, hideLinks);
RenderSingleEntry(sb, entry, repo, owner, hideLinks, hideEntryDescriptions);
}
}

private static void RenderSingleEntry(StringBuilder sb, ChangelogEntry entry, string repo, string owner, bool hideLinks)
private static void RenderSingleEntry(StringBuilder sb, ChangelogEntry entry, string repo, string owner, bool hideLinks, bool hideEntryDescriptions)
{
_ = sb.Append("* ");
_ = sb.Append(ChangelogTextUtilities.Beautify(entry.Title));

RenderEntryLinks(sb, entry, repo, owner, hideLinks);

if (!string.IsNullOrWhiteSpace(entry.Description))
{
_ = sb.AppendLine();
var indented = ChangelogTextUtilities.Indent(entry.Description);
_ = sb.AppendLine(indented);
}
if (hideEntryDescriptions || string.IsNullOrWhiteSpace(entry.Description))
return;

_ = sb.AppendLine();
var indented = ChangelogTextUtilities.Indent(entry.Description);
_ = sb.AppendLine(indented);
}

private static void RenderEntryLinks(StringBuilder sb, ChangelogEntry entry, string repo, string owner, bool hideLinks)
Expand Down Expand Up @@ -373,6 +416,7 @@ private static void RenderDetailedEntries(
string owner,
bool groupBySubtype,
bool hideLinks,
bool hideEntryDescriptions,
PublishBlocker? publishBlocker)
{
var grouped = groupBySubtype
Expand All @@ -391,16 +435,21 @@ private static void RenderDetailedEntries(
}

foreach (var entry in group)
RenderDetailedEntry(sb, entry, repo, owner, hideLinks);
RenderDetailedEntry(sb, entry, repo, owner, hideLinks, hideEntryDescriptions);
}
}

private static void RenderDetailedEntry(StringBuilder sb, ChangelogEntry entry, string repo, string owner, bool hideLinks)
private static void RenderDetailedEntry(StringBuilder sb, ChangelogEntry entry, string repo, string owner, bool hideLinks, bool hideEntryDescriptions)
{
_ = sb.AppendLine();
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"::::{{dropdown}} {ChangelogTextUtilities.Beautify(entry.Title)}");
_ = sb.AppendLine(entry.Description ?? "% Describe the change");
_ = sb.AppendLine();
if (!hideEntryDescriptions)
{
_ = sb.AppendLine(entry.Description ?? "% Describe the change");
_ = sb.AppendLine();
}
else
_ = sb.AppendLine();

RenderDetailedEntryLinks(sb, entry, repo, owner, hideLinks);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ public ChangelogTitleDescriptionSpacingTests(ITestOutputHelper output) : base(ou
// language=markdown
"""
:::{changelog}
:description-visibility: keep-descriptions
:::
""") => FileSystem.AddFile("docs/changelog/bundles/9.3.0.yaml", new MockFileData(
// language=yaml
Expand Down
Loading
Loading