diff --git a/docs/contribute/changelog.md b/docs/contribute/changelog.md index 4effb6a3b..f85ce839e 100644 --- a/docs/contribute/changelog.md +++ b/docs/contribute/changelog.md @@ -258,6 +258,7 @@ Options: --output Optional: Output directory for rendered markdown files. Defaults to current directory [Default: null] --title Optional: Title to use for section headers in output markdown files. Defaults to version from first bundle [Default: null] --subsections Optional: Group entries by area/component in subsections. Defaults to false + --hide-private-links Optional: Hide private links by commenting them out in markdown output. Defaults to false ``` Before you can use this command you must create changelog files and collect them into bundles. diff --git a/src/services/Elastic.Documentation.Services/Changelog/ChangelogRenderInput.cs b/src/services/Elastic.Documentation.Services/Changelog/ChangelogRenderInput.cs index e1b134295..b0c596464 100644 --- a/src/services/Elastic.Documentation.Services/Changelog/ChangelogRenderInput.cs +++ b/src/services/Elastic.Documentation.Services/Changelog/ChangelogRenderInput.cs @@ -13,5 +13,6 @@ public class ChangelogRenderInput public string? Output { get; set; } public string? Title { get; set; } public bool Subsections { get; set; } + public bool HidePrivateLinks { get; set; } } diff --git a/src/services/Elastic.Documentation.Services/ChangelogService.cs b/src/services/Elastic.Documentation.Services/ChangelogService.cs index bd35b04de..eeb5374ff 100644 --- a/src/services/Elastic.Documentation.Services/ChangelogService.cs +++ b/src/services/Elastic.Documentation.Services/ChangelogService.cs @@ -1354,13 +1354,13 @@ Cancel ctx var repoForRendering = allResolvedEntries.Count > 0 ? allResolvedEntries[0].repo : defaultRepo; // Render index.md (features, enhancements, bug fixes, security) - await RenderIndexMarkdown(collector, outputDir, title, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, ctx); + await RenderIndexMarkdown(collector, outputDir, title, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, ctx); // Render breaking-changes.md - await RenderBreakingChangesMarkdown(collector, outputDir, title, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, ctx); + await RenderBreakingChangesMarkdown(collector, outputDir, title, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, ctx); // Render deprecations.md - await RenderDeprecationsMarkdown(collector, outputDir, title, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, ctx); + await RenderDeprecationsMarkdown(collector, outputDir, title, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, ctx); _logger.LogInformation("Rendered changelog markdown files to {OutputDir}", outputDir); @@ -1397,6 +1397,7 @@ private async Task RenderIndexMarkdown( List entries, Dictionary> entriesByType, bool subsections, + bool hidePrivateLinks, Cancel ctx ) { @@ -1444,7 +1445,7 @@ Cancel ctx { sb.AppendLine(CultureInfo.InvariantCulture, $"### Features and enhancements [{repo}-{title}-features-enhancements]"); var combined = features.Concat(enhancements).ToList(); - RenderEntriesByArea(sb, combined, repo, subsections); + RenderEntriesByArea(sb, combined, repo, subsections, hidePrivateLinks); } if (security.Count > 0 || bugFixes.Count > 0) @@ -1452,7 +1453,7 @@ Cancel ctx sb.AppendLine(); sb.AppendLine(CultureInfo.InvariantCulture, $"### Fixes [{repo}-{title}-fixes]"); var combined = security.Concat(bugFixes).ToList(); - RenderEntriesByArea(sb, combined, repo, subsections); + RenderEntriesByArea(sb, combined, repo, subsections, hidePrivateLinks); } } else @@ -1480,6 +1481,7 @@ private async Task RenderBreakingChangesMarkdown( List entries, Dictionary> entriesByType, bool subsections, + bool hidePrivateLinks, Cancel ctx ) { @@ -1506,20 +1508,39 @@ Cancel ctx sb.AppendLine(CultureInfo.InvariantCulture, $"::::{{dropdown}} {Beautify(entry.Title)}"); sb.AppendLine(entry.Description ?? "% Describe the functionality that changed"); sb.AppendLine(); - sb.Append("For more information, check "); - if (!string.IsNullOrWhiteSpace(entry.Pr)) + if (hidePrivateLinks) { - sb.Append(FormatPrLink(entry.Pr, repo)); + // When hiding private links, put them on separate lines as comments + if (!string.IsNullOrWhiteSpace(entry.Pr)) + { + sb.AppendLine(FormatPrLink(entry.Pr, repo, hidePrivateLinks)); + } + if (entry.Issues != null && entry.Issues.Count > 0) + { + foreach (var issue in entry.Issues) + { + sb.AppendLine(FormatIssueLink(issue, repo, hidePrivateLinks)); + } + } + sb.AppendLine("For more information, check the pull request or issue above."); } - if (entry.Issues != null && entry.Issues.Count > 0) + else { - foreach (var issue in entry.Issues) + sb.Append("For more information, check "); + if (!string.IsNullOrWhiteSpace(entry.Pr)) { - sb.Append(' '); - sb.Append(FormatIssueLink(issue, repo)); + sb.Append(FormatPrLink(entry.Pr, repo, hidePrivateLinks)); } + if (entry.Issues != null && entry.Issues.Count > 0) + { + foreach (var issue in entry.Issues) + { + sb.Append(' '); + sb.Append(FormatIssueLink(issue, repo, hidePrivateLinks)); + } + } + sb.AppendLine("."); } - sb.AppendLine("."); sb.AppendLine(); if (!string.IsNullOrWhiteSpace(entry.Impact)) @@ -1571,6 +1592,7 @@ private async Task RenderDeprecationsMarkdown( List entries, Dictionary> entriesByType, bool subsections, + bool hidePrivateLinks, Cancel ctx ) { @@ -1597,20 +1619,39 @@ Cancel ctx sb.AppendLine(CultureInfo.InvariantCulture, $"::::{{dropdown}} {Beautify(entry.Title)}"); sb.AppendLine(entry.Description ?? "% Describe the functionality that was deprecated"); sb.AppendLine(); - sb.Append("For more information, check "); - if (!string.IsNullOrWhiteSpace(entry.Pr)) + if (hidePrivateLinks) { - sb.Append(FormatPrLink(entry.Pr, repo)); + // When hiding private links, put them on separate lines as comments + if (!string.IsNullOrWhiteSpace(entry.Pr)) + { + sb.AppendLine(FormatPrLink(entry.Pr, repo, hidePrivateLinks)); + } + if (entry.Issues != null && entry.Issues.Count > 0) + { + foreach (var issue in entry.Issues) + { + sb.AppendLine(FormatIssueLink(issue, repo, hidePrivateLinks)); + } + } + sb.AppendLine("For more information, check the pull request or issue above."); } - if (entry.Issues != null && entry.Issues.Count > 0) + else { - foreach (var issue in entry.Issues) + sb.Append("For more information, check "); + if (!string.IsNullOrWhiteSpace(entry.Pr)) { - sb.Append(' '); - sb.Append(FormatIssueLink(issue, repo)); + sb.Append(FormatPrLink(entry.Pr, repo, hidePrivateLinks)); } + if (entry.Issues != null && entry.Issues.Count > 0) + { + foreach (var issue in entry.Issues) + { + sb.Append(' '); + sb.Append(FormatIssueLink(issue, repo, hidePrivateLinks)); + } + } + sb.AppendLine("."); } - sb.AppendLine("."); sb.AppendLine(); if (!string.IsNullOrWhiteSpace(entry.Impact)) @@ -1653,7 +1694,7 @@ Cancel ctx } [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0058:Expression value is never used", Justification = "StringBuilder methods return builder for chaining")] - private void RenderEntriesByArea(StringBuilder sb, List entries, string repo, bool subsections) + private void RenderEntriesByArea(StringBuilder sb, List entries, string repo, bool subsections, bool hidePrivateLinks) { var groupedByArea = entries.GroupBy(e => GetComponent(e)).ToList(); foreach (var areaGroup in groupedByArea) @@ -1669,26 +1710,67 @@ private void RenderEntriesByArea(StringBuilder sb, List entries, { sb.Append("* "); sb.Append(Beautify(entry.Title)); - sb.Append(' '); - if (!string.IsNullOrWhiteSpace(entry.Pr)) + var hasCommentedLinks = false; + if (hidePrivateLinks) { - sb.Append(FormatPrLink(entry.Pr, repo)); - sb.Append(' '); - } + // When hiding private links, put them on separate lines as comments with proper indentation + if (!string.IsNullOrWhiteSpace(entry.Pr)) + { + sb.AppendLine(); + sb.Append(" "); + sb.Append(FormatPrLink(entry.Pr, repo, hidePrivateLinks)); + hasCommentedLinks = true; + } + + if (entry.Issues != null && entry.Issues.Count > 0) + { + foreach (var issue in entry.Issues) + { + sb.AppendLine(); + sb.Append(" "); + sb.Append(FormatIssueLink(issue, repo, hidePrivateLinks)); + hasCommentedLinks = true; + } + } - if (entry.Issues != null && entry.Issues.Count > 0) + // Add newline after the last link if there are commented links + if (hasCommentedLinks) + { + sb.AppendLine(); + } + } + else { - foreach (var issue in entry.Issues) + sb.Append(' '); + if (!string.IsNullOrWhiteSpace(entry.Pr)) { - sb.Append(FormatIssueLink(issue, repo)); + sb.Append(FormatPrLink(entry.Pr, repo, hidePrivateLinks)); sb.Append(' '); } + + if (entry.Issues != null && entry.Issues.Count > 0) + { + foreach (var issue in entry.Issues) + { + sb.Append(FormatIssueLink(issue, repo, hidePrivateLinks)); + sb.Append(' '); + } + } } if (!string.IsNullOrWhiteSpace(entry.Description)) { - sb.AppendLine(); + // Add blank line before description + // When hidePrivateLinks is true and links exist, add an indented blank line + if (hidePrivateLinks && hasCommentedLinks) + { + sb.AppendLine(" "); + } + else + { + sb.AppendLine(); + } var indented = Indent(entry.Description); sb.AppendLine(indented); } @@ -1747,40 +1829,58 @@ private static string Indent(string text) [GeneratedRegex(@"\d+$", RegexOptions.None)] private static partial Regex IssueNumberRegex(); - private static string FormatPrLink(string pr, string repo) + private static string FormatPrLink(string pr, string repo, bool hidePrivateLinks) { // Extract PR number var match = PrNumberRegex().Match(pr); var prNumber = match.Success ? match.Value : pr; // Format as markdown link + string link; if (pr.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - return $"[#{prNumber}]({pr})"; + link = $"[#{prNumber}]({pr})"; } else { var url = $"https://github.com/elastic/{repo}/pull/{prNumber}"; - return $"[#{prNumber}]({url})"; + link = $"[#{prNumber}]({url})"; } + + // Comment out link if hiding private links + if (hidePrivateLinks) + { + return $"% {link}"; + } + + return link; } - private static string FormatIssueLink(string issue, string repo) + private static string FormatIssueLink(string issue, string repo, bool hidePrivateLinks) { // Extract issue number var match = IssueNumberRegex().Match(issue); var issueNumber = match.Success ? match.Value : issue; // Format as markdown link + string link; if (issue.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - return $"[#{issueNumber}]({issue})"; + link = $"[#{issueNumber}]({issue})"; } else { var url = $"https://github.com/elastic/{repo}/issues/{issueNumber}"; - return $"[#{issueNumber}]({url})"; + link = $"[#{issueNumber}]({url})"; } + + // Comment out link if hiding private links + if (hidePrivateLinks) + { + return $"% {link}"; + } + + return link; } } diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs index ba2d263d7..e1f2b5591 100644 --- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs +++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs @@ -162,6 +162,7 @@ async static (s, collector, state, ctx) => await s.BundleChangelogs(collector, s /// Optional: Output directory for rendered markdown files. Defaults to current directory /// Optional: Title to use for section headers in output markdown files. Defaults to version from first bundle /// Optional: Group entries by area/component in subsections. Defaults to false + /// Optional: Hide private links by commenting them out in markdown output. Defaults to false /// [Command("render")] public async Task Render( @@ -169,6 +170,7 @@ public async Task Render( string? output = null, string? title = null, bool subsections = false, + bool hidePrivateLinks = false, Cancel ctx = default ) { @@ -181,7 +183,8 @@ public async Task Render( Bundles = input ?? [], Output = output, Title = title, - Subsections = subsections + Subsections = subsections, + HidePrivateLinks = hidePrivateLinks }; serviceInvoker.AddCommand(service, renderInput,