diff --git a/docs/cli/release/changelog-render.md b/docs/cli/release/changelog-render.md index 437483fbe..61fc68e48 100644 --- a/docs/cli/release/changelog-render.md +++ b/docs/cli/release/changelog-render.md @@ -14,12 +14,15 @@ docs-builder changelog render [options...] [-h|--help] ## Options -`--input >` +`--input ` : One or more bundle input files. -: Each item can be specified as "bundle-file-path, changelog-file-path, repo" to accommodate files coming from multiple locations. -: For example, `--input "./changelog-bundle.yaml,./changelogs,elasticsearch"`. -: Only `bundle-file-path` is required. +: Each bundle is specified as "bundle-file-path|changelog-file-path|repo|link-visibility" using pipe (`|`) as delimiter. +: To merge multiple bundles, separate them with commas: `--input "bundle1|dir1|repo1|keep-links,bundle2|dir2|repo2|hide-links"`. +: For example, `--input "/path/to/changelog-bundle.yaml|/path/to/changelogs|elasticsearch|keep-links"`. +: Only `bundle-file-path` is required for each bundle. : Use `repo` if your changelogs do not contain full URLs for the pull requests or issues; otherwise they will be incorrectly derived with "elastic/elastic" in the URL by default. +: Use `link-visibility` to control whether PR/issue links are shown or hidden for entries from this bundle. Valid values are `keep-links` (default) or `hide-links`. Use `hide-links` for bundles from private repositories. +: **Important**: Paths must be absolute or use environment variables. Tilde (`~`) expansion is not supported. `--output ` : Optional: The output directory for rendered markdown files. @@ -34,11 +37,6 @@ docs-builder changelog render [options...] [-h|--help] : Optional: Group entries by area in subsections. : Defaults to false. -`--hide-private-links` -: Optional: Hide private links by commenting them out in the markdown output. -: This option is useful when rendering changelog bundles in private repositories. -: Defaults to false. - `--hide-features ` : Optional: Filter by feature IDs (comma-separated), or a path to a newline-delimited file containing feature IDs. Can be specified multiple times. : Each occurrence can be either comma-separated feature IDs (e.g., `--hide-features "feature:new-search-api,feature:enhanced-analytics"`) or a file path (e.g., `--hide-features /path/to/file.txt`). diff --git a/docs/contribute/changelog.md b/docs/contribute/changelog.md index 93c05ade5..d9057eba0 100644 --- a/docs/contribute/changelog.md +++ b/docs/contribute/changelog.md @@ -341,11 +341,10 @@ For up-to-date details, use the `-h` command option: Render bundled changelog(s) to markdown files Options: - --input > Required: Bundle input(s) in format "bundle-file-path, changelog-file-path, repo". Can be specified multiple times. Only bundle-file-path is required. [Required] + --input Required: Bundle input(s) in format "bundle-file-path|changelog-file-path|repo|link-visibility" (use pipe as delimiter). To merge multiple bundles, separate them with commas. Only bundle-file-path is required. link-visibility can be "hide-links" or "keep-links" (default). Paths must be absolute or use environment variables; tilde (~) expansion is not supported. [Required] --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. For breaking changes with a subtype, groups by subtype instead of area. Defaults to false - --hide-private-links Optional: Hide private links by commenting them out in the markdown output. Defaults to false --hide-features Filter by feature IDs (comma-separated), or a path to a newline-delimited file containing feature IDs. Can be specified multiple times. Entries with matching feature-id values will be commented out in the markdown output. [Default: null] --config Optional: Path to the changelog.yml configuration file. Defaults to 'docs/changelog.yml' [Default: null] ``` @@ -373,17 +372,21 @@ To create markdown files from this bundle, run the `docs-builder changelog rende ```sh docs-builder changelog render \ - --input "./changelog-bundle.yaml,./changelogs,elasticsearch" \ <1> + --input "/path/to/changelog-bundle.yaml|/path/to/changelogs|elasticsearch|keep-links,/path/to/other-bundle.yaml|/path/to/other-changelogs|kibana|hide-links" \ <1> --title 9.2.2 \ <2> - --output ./release-notes \ <3> - --subsections \ <4> + --output /path/to/release-notes \ <3> + --subsections <4> ``` -1. Provide information about the changelog bundle. The format is `", , "`. Only the `` is required. The `` is useful if the changelogs are not in the default directory and are not resolved within the bundle. The `` is necessary if your changelogs do not contain full URLs for the pull requests or issues. You can specify `--input` multiple times to merge multiple bundles. +1. Provide information about the changelog bundle(s). The format for each bundle is `"|||"` using pipe (`|`) as delimiter. To merge multiple bundles, separate them with commas (`,`). Only the `` is required for each bundle. The `` is useful if the changelogs are not in the default directory and are not resolved within the bundle. The `` is necessary if your changelogs do not contain full URLs for the pull requests or issues. The `` can be `hide-links` or `keep-links` (default) to control whether PR/issue links are hidden for entries from private repositories. 2. The `--title` value is used for an output folder name and for section titles in the markdown files. If you omit `--title` and the first bundle contains a product `target` value, that value is used. Otherwise, if none of the bundles have product `target` fields, the title defaults to "unknown". 3. By default the command creates the output files in the current directory. 4. By default the changelog areas are not displayed in the output. Add `--subsections` to group changelog details by their `areas`. For breaking changes that have a `subtype` value, the subsections will be grouped by subtype instead of area. +:::{important} +Paths in the `--input` option must be absolute paths or use environment variables. Tilde (`~`) expansion is not supported. +::: + For example, the `index.md` output file contains information derived from the changelogs: ```md @@ -401,7 +404,7 @@ For example, the `index.md` output file contains information derived from the ch * Break on FieldData when building global ordinals. [#108875](https://github.com/elastic/elastic/pull/108875) ``` -To comment out the pull request and issue links, for example if they relate to a private repository, use the `--hide-private-links` option. +To comment out the pull request and issue links, for example if they relate to a private repository, add `hide-links` to the `--input` option for that bundle. This allows you to selectively hide links per bundle when merging changelogs from multiple repositories. If you have changelogs with `feature-id` values and you want them to be omitted from the output, use the `--hide-features` option. For more information, refer to [](/cli/release/changelog-render.md). diff --git a/src/services/Elastic.Documentation.Services/Changelog/BundleInput.cs b/src/services/Elastic.Documentation.Services/Changelog/BundleInput.cs index 60387a5dd..1fc3f2ff9 100644 --- a/src/services/Elastic.Documentation.Services/Changelog/BundleInput.cs +++ b/src/services/Elastic.Documentation.Services/Changelog/BundleInput.cs @@ -5,12 +5,18 @@ namespace Elastic.Documentation.Services.Changelog; /// -/// Input for a single bundle file with optional directory and repo +/// Input for a single bundle file with optional directory, repo, and link visibility /// public class BundleInput { public string BundleFile { get; set; } = string.Empty; public string? Directory { get; set; } public string? Repo { get; set; } + /// + /// Whether to hide PR/issue links for entries from this bundle. + /// When true, links are commented out in the markdown output. + /// Defaults to false (links are shown). + /// + public bool HideLinks { get; set; } } diff --git a/src/services/Elastic.Documentation.Services/Changelog/ChangelogRenderInput.cs b/src/services/Elastic.Documentation.Services/Changelog/ChangelogRenderInput.cs index 0d726761d..c9b86586a 100644 --- a/src/services/Elastic.Documentation.Services/Changelog/ChangelogRenderInput.cs +++ b/src/services/Elastic.Documentation.Services/Changelog/ChangelogRenderInput.cs @@ -13,7 +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; } public string[]? HideFeatures { get; set; } public string? Config { get; set; } } diff --git a/src/services/Elastic.Documentation.Services/ChangelogService.cs b/src/services/Elastic.Documentation.Services/ChangelogService.cs index 520128b79..04713496b 100644 --- a/src/services/Elastic.Documentation.Services/ChangelogService.cs +++ b/src/services/Elastic.Documentation.Services/ChangelogService.cs @@ -1592,7 +1592,7 @@ Cancel ctx } // Merge phase: Now that validation passed, load and merge all bundles - var allResolvedEntries = new List<(ChangelogData entry, string repo, HashSet bundleProductIds)>(); + var allResolvedEntries = new List<(ChangelogData entry, string repo, HashSet bundleProductIds, bool hideLinks)>(); var allProducts = new HashSet<(string product, string target)>(); foreach (var (bundledData, bundleInput, bundleDirectory) in bundleDataList) @@ -1653,7 +1653,7 @@ Cancel ctx if (entryData != null) { - allResolvedEntries.Add((entryData, repo, bundleProductIds)); + allResolvedEntries.Add((entryData, repo, bundleProductIds, bundleInput.HideLinks)); } } } @@ -1806,7 +1806,7 @@ Cancel ctx // Track hidden entries for warnings var hiddenEntries = new List<(string title, string featureId)>(); - foreach (var (entry, _, _) in allResolvedEntries) + foreach (var (entry, _, _, _) in allResolvedEntries) { if (!string.IsNullOrWhiteSpace(entry.FeatureId) && featureIdsToHide.Contains(entry.FeatureId)) { @@ -1826,7 +1826,7 @@ Cancel ctx // Check entries against render blockers and track blocked entries // render_blockers matches against bundle products, not individual entry products var blockedEntries = new List<(string title, List reasons)>(); - foreach (var (entry, _, bundleProductIds) in allResolvedEntries) + foreach (var (entry, _, bundleProductIds, _) in allResolvedEntries) { var isBlocked = ShouldBlockEntry(entry, bundleProductIds, renderBlockers, out var blockReasons); if (isBlocked) @@ -1874,25 +1874,39 @@ Cancel ctx // Create mapping from entries to their bundle product IDs for render_blockers checking // Use a custom comparer for reference equality since entries are objects var entryToBundleProducts = new Dictionary>(); - foreach (var (entry, _, bundleProductIds) in allResolvedEntries) + foreach (var (entry, _, bundleProductIds, _) in allResolvedEntries) { entryToBundleProducts[entry] = bundleProductIds; } - // Render markdown files (use first repo found, or default) - var repoForRendering = allResolvedEntries.Count > 0 ? allResolvedEntries[0].repo : defaultRepo; + // Create mapping from entries to their repo for PR link formatting + var entryToRepo = new Dictionary(); + foreach (var (entry, repo, _, _) in allResolvedEntries) + { + entryToRepo[entry] = repo; + } + + // Create mapping from entries to their hideLinks setting for per-bundle link visibility + var entryToHideLinks = new Dictionary(); + foreach (var (entry, _, _, hideLinks) in allResolvedEntries) + { + entryToHideLinks[entry] = hideLinks; + } + + // Render markdown files (use first repo found for section anchors, or default) + var repoForAnchors = allResolvedEntries.Count > 0 ? allResolvedEntries[0].repo : defaultRepo; // Render index.md (features, enhancements, bug fixes, security, docs, regression, other) - await RenderIndexMarkdown(collector, outputDir, title, titleSlug, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, featureIdsToHide, renderBlockers, entryToBundleProducts, ctx); + await RenderIndexMarkdown(collector, outputDir, title, titleSlug, repoForAnchors, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, featureIdsToHide, renderBlockers, entryToBundleProducts, entryToRepo, entryToHideLinks, ctx); // Render breaking-changes.md - await RenderBreakingChangesMarkdown(collector, outputDir, title, titleSlug, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, featureIdsToHide, renderBlockers, entryToBundleProducts, ctx); + await RenderBreakingChangesMarkdown(collector, outputDir, title, titleSlug, repoForAnchors, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, featureIdsToHide, renderBlockers, entryToBundleProducts, entryToRepo, entryToHideLinks, ctx); // Render deprecations.md - await RenderDeprecationsMarkdown(collector, outputDir, title, titleSlug, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, featureIdsToHide, renderBlockers, entryToBundleProducts, ctx); + await RenderDeprecationsMarkdown(collector, outputDir, title, titleSlug, repoForAnchors, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, featureIdsToHide, renderBlockers, entryToBundleProducts, entryToRepo, entryToHideLinks, ctx); // Render known-issues.md - await RenderKnownIssuesMarkdown(collector, outputDir, title, titleSlug, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, featureIdsToHide, renderBlockers, entryToBundleProducts, ctx); + await RenderKnownIssuesMarkdown(collector, outputDir, title, titleSlug, repoForAnchors, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, featureIdsToHide, renderBlockers, entryToBundleProducts, entryToRepo, entryToHideLinks, ctx); _logger.LogInformation("Rendered changelog markdown files to {OutputDir}", outputDir); @@ -1930,10 +1944,11 @@ private async Task RenderIndexMarkdown( List entries, Dictionary> entriesByType, bool subsections, - bool hidePrivateLinks, HashSet featureIdsToHide, Dictionary? renderBlockers, Dictionary> entryToBundleProducts, + Dictionary entryToRepo, + Dictionary entryToHideLinks, Cancel ctx ) { @@ -1981,7 +1996,7 @@ Cancel ctx { sb.AppendLine(CultureInfo.InvariantCulture, $"### Features and enhancements [{repo}-{titleSlug}-features-enhancements]"); var combined = features.Concat(enhancements).ToList(); - RenderEntriesByArea(sb, combined, repo, subsections, hidePrivateLinks, featureIdsToHide, renderBlockers, entryToBundleProducts); + RenderEntriesByArea(sb, combined, subsections, featureIdsToHide, renderBlockers, entryToBundleProducts, entryToRepo, entryToHideLinks); } if (security.Count > 0 || bugFixes.Count > 0) @@ -1989,28 +2004,28 @@ Cancel ctx sb.AppendLine(); sb.AppendLine(CultureInfo.InvariantCulture, $"### Fixes [{repo}-{titleSlug}-fixes]"); var combined = security.Concat(bugFixes).ToList(); - RenderEntriesByArea(sb, combined, repo, subsections, hidePrivateLinks, featureIdsToHide, renderBlockers, entryToBundleProducts); + RenderEntriesByArea(sb, combined, subsections, featureIdsToHide, renderBlockers, entryToBundleProducts, entryToRepo, entryToHideLinks); } if (docs.Count > 0) { sb.AppendLine(); sb.AppendLine(CultureInfo.InvariantCulture, $"### Documentation [{repo}-{titleSlug}-docs]"); - RenderEntriesByArea(sb, docs, repo, subsections, hidePrivateLinks, featureIdsToHide, renderBlockers, entryToBundleProducts); + RenderEntriesByArea(sb, docs, subsections, featureIdsToHide, renderBlockers, entryToBundleProducts, entryToRepo, entryToHideLinks); } if (regressions.Count > 0) { sb.AppendLine(); sb.AppendLine(CultureInfo.InvariantCulture, $"### Regressions [{repo}-{titleSlug}-regressions]"); - RenderEntriesByArea(sb, regressions, repo, subsections, hidePrivateLinks, featureIdsToHide, renderBlockers, entryToBundleProducts); + RenderEntriesByArea(sb, regressions, subsections, featureIdsToHide, renderBlockers, entryToBundleProducts, entryToRepo, entryToHideLinks); } if (other.Count > 0) { sb.AppendLine(); sb.AppendLine(CultureInfo.InvariantCulture, $"### Other changes [{repo}-{titleSlug}-other]"); - RenderEntriesByArea(sb, other, repo, subsections, hidePrivateLinks, featureIdsToHide, renderBlockers, entryToBundleProducts); + RenderEntriesByArea(sb, other, subsections, featureIdsToHide, renderBlockers, entryToBundleProducts, entryToRepo, entryToHideLinks); } } else @@ -2039,10 +2054,11 @@ private async Task RenderBreakingChangesMarkdown( List entries, Dictionary> entriesByType, bool subsections, - bool hidePrivateLinks, HashSet featureIdsToHide, Dictionary? renderBlockers, Dictionary> entryToBundleProducts, + Dictionary entryToRepo, + Dictionary entryToHideLinks, Cancel ctx ) { @@ -2070,6 +2086,8 @@ Cancel ctx foreach (var entry in group) { var bundleProductIds = entryToBundleProducts.GetValueOrDefault(entry, new HashSet(StringComparer.OrdinalIgnoreCase)); + var entryRepo = entryToRepo.GetValueOrDefault(entry, repo); + var entryHideLinks = entryToHideLinks.GetValueOrDefault(entry, false); var shouldHide = (!string.IsNullOrWhiteSpace(entry.FeatureId) && featureIdsToHide.Contains(entry.FeatureId)) || ShouldBlockEntry(entry, bundleProductIds, renderBlockers, out _); @@ -2085,18 +2103,18 @@ Cancel ctx var hasIssues = entry.Issues != null && entry.Issues.Count > 0; if (hasPr || hasIssues) { - if (hidePrivateLinks) + if (entryHideLinks) { // When hiding private links, put them on separate lines as comments if (hasPr) { - sb.AppendLine(FormatPrLink(entry.Pr!, repo, hidePrivateLinks)); + sb.AppendLine(FormatPrLink(entry.Pr!, entryRepo, entryHideLinks)); } if (hasIssues) { foreach (var issue in entry.Issues!) { - sb.AppendLine(FormatIssueLink(issue, repo, hidePrivateLinks)); + sb.AppendLine(FormatIssueLink(issue, entryRepo, entryHideLinks)); } } sb.AppendLine("For more information, check the pull request or issue above."); @@ -2106,14 +2124,14 @@ Cancel ctx sb.Append("For more information, check "); if (hasPr) { - sb.Append(FormatPrLink(entry.Pr!, repo, hidePrivateLinks)); + sb.Append(FormatPrLink(entry.Pr!, entryRepo, entryHideLinks)); } if (hasIssues) { foreach (var issue in entry.Issues!) { sb.Append(' '); - sb.Append(FormatIssueLink(issue, repo, hidePrivateLinks)); + sb.Append(FormatIssueLink(issue, entryRepo, entryHideLinks)); } } sb.AppendLine("."); @@ -2175,10 +2193,11 @@ private async Task RenderDeprecationsMarkdown( List entries, Dictionary> entriesByType, bool subsections, - bool hidePrivateLinks, HashSet featureIdsToHide, Dictionary? renderBlockers, Dictionary> entryToBundleProducts, + Dictionary entryToRepo, + Dictionary entryToHideLinks, Cancel ctx ) { @@ -2202,6 +2221,8 @@ Cancel ctx foreach (var entry in areaGroup) { var bundleProductIds = entryToBundleProducts.GetValueOrDefault(entry, new HashSet(StringComparer.OrdinalIgnoreCase)); + var entryRepo = entryToRepo.GetValueOrDefault(entry, repo); + var entryHideLinks = entryToHideLinks.GetValueOrDefault(entry, false); var shouldHide = (!string.IsNullOrWhiteSpace(entry.FeatureId) && featureIdsToHide.Contains(entry.FeatureId)) || ShouldBlockEntry(entry, bundleProductIds, renderBlockers, out _); @@ -2217,18 +2238,18 @@ Cancel ctx var hasIssues = entry.Issues != null && entry.Issues.Count > 0; if (hasPr || hasIssues) { - if (hidePrivateLinks) + if (entryHideLinks) { // When hiding private links, put them on separate lines as comments if (hasPr) { - sb.AppendLine(FormatPrLink(entry.Pr!, repo, hidePrivateLinks)); + sb.AppendLine(FormatPrLink(entry.Pr!, entryRepo, entryHideLinks)); } if (hasIssues) { foreach (var issue in entry.Issues!) { - sb.AppendLine(FormatIssueLink(issue, repo, hidePrivateLinks)); + sb.AppendLine(FormatIssueLink(issue, entryRepo, entryHideLinks)); } } sb.AppendLine("For more information, check the pull request or issue above."); @@ -2238,14 +2259,14 @@ Cancel ctx sb.Append("For more information, check "); if (hasPr) { - sb.Append(FormatPrLink(entry.Pr!, repo, hidePrivateLinks)); + sb.Append(FormatPrLink(entry.Pr!, entryRepo, entryHideLinks)); } if (hasIssues) { foreach (var issue in entry.Issues!) { sb.Append(' '); - sb.Append(FormatIssueLink(issue, repo, hidePrivateLinks)); + sb.Append(FormatIssueLink(issue, entryRepo, entryHideLinks)); } } sb.AppendLine("."); @@ -2307,10 +2328,11 @@ private async Task RenderKnownIssuesMarkdown( List entries, Dictionary> entriesByType, bool subsections, - bool hidePrivateLinks, HashSet featureIdsToHide, Dictionary? renderBlockers, Dictionary> entryToBundleProducts, + Dictionary entryToRepo, + Dictionary entryToHideLinks, Cancel ctx ) { @@ -2334,6 +2356,8 @@ Cancel ctx foreach (var entry in areaGroup) { var bundleProductIds = entryToBundleProducts.GetValueOrDefault(entry, new HashSet(StringComparer.OrdinalIgnoreCase)); + var entryRepo = entryToRepo.GetValueOrDefault(entry, repo); + var entryHideLinks = entryToHideLinks.GetValueOrDefault(entry, false); var shouldHide = (!string.IsNullOrWhiteSpace(entry.FeatureId) && featureIdsToHide.Contains(entry.FeatureId)) || ShouldBlockEntry(entry, bundleProductIds, renderBlockers, out _); @@ -2349,18 +2373,18 @@ Cancel ctx var hasIssues = entry.Issues != null && entry.Issues.Count > 0; if (hasPr || hasIssues) { - if (hidePrivateLinks) + if (entryHideLinks) { // When hiding private links, put them on separate lines as comments if (hasPr) { - sb.AppendLine(FormatPrLink(entry.Pr!, repo, hidePrivateLinks)); + sb.AppendLine(FormatPrLink(entry.Pr!, entryRepo, entryHideLinks)); } if (hasIssues) { foreach (var issue in entry.Issues!) { - sb.AppendLine(FormatIssueLink(issue, repo, hidePrivateLinks)); + sb.AppendLine(FormatIssueLink(issue, entryRepo, entryHideLinks)); } } sb.AppendLine("For more information, check the pull request or issue above."); @@ -2370,14 +2394,14 @@ Cancel ctx sb.Append("For more information, check "); if (hasPr) { - sb.Append(FormatPrLink(entry.Pr!, repo, hidePrivateLinks)); + sb.Append(FormatPrLink(entry.Pr!, entryRepo, entryHideLinks)); } if (hasIssues) { foreach (var issue in entry.Issues!) { sb.Append(' '); - sb.Append(FormatIssueLink(issue, repo, hidePrivateLinks)); + sb.Append(FormatIssueLink(issue, entryRepo, entryHideLinks)); } } sb.AppendLine("."); @@ -2429,7 +2453,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, bool hidePrivateLinks, HashSet featureIdsToHide, Dictionary? renderBlockers, Dictionary> entryToBundleProducts) + private void RenderEntriesByArea(StringBuilder sb, List entries, bool subsections, HashSet featureIdsToHide, Dictionary? renderBlockers, Dictionary> entryToBundleProducts, Dictionary entryToRepo, Dictionary entryToHideLinks) { var groupedByArea = entries.GroupBy(e => GetComponent(e)).ToList(); foreach (var areaGroup in groupedByArea) @@ -2444,6 +2468,8 @@ private void RenderEntriesByArea(StringBuilder sb, List entries, foreach (var entry in areaGroup) { var bundleProductIds = entryToBundleProducts.GetValueOrDefault(entry, new HashSet(StringComparer.OrdinalIgnoreCase)); + var entryRepo = entryToRepo.GetValueOrDefault(entry, "elastic"); + var entryHideLinks = entryToHideLinks.GetValueOrDefault(entry, false); var shouldHide = (!string.IsNullOrWhiteSpace(entry.FeatureId) && featureIdsToHide.Contains(entry.FeatureId)) || ShouldBlockEntry(entry, bundleProductIds, renderBlockers, out _); @@ -2455,7 +2481,7 @@ private void RenderEntriesByArea(StringBuilder sb, List entries, sb.Append(Beautify(entry.Title)); var hasCommentedLinks = false; - if (hidePrivateLinks) + if (entryHideLinks) { // When hiding private links, put them on separate lines as comments with proper indentation if (!string.IsNullOrWhiteSpace(entry.Pr)) @@ -2466,7 +2492,7 @@ private void RenderEntriesByArea(StringBuilder sb, List entries, sb.Append("% "); } sb.Append(" "); - sb.Append(FormatPrLink(entry.Pr, repo, hidePrivateLinks)); + sb.Append(FormatPrLink(entry.Pr, entryRepo, entryHideLinks)); hasCommentedLinks = true; } @@ -2480,7 +2506,7 @@ private void RenderEntriesByArea(StringBuilder sb, List entries, sb.Append("% "); } sb.Append(" "); - sb.Append(FormatIssueLink(issue, repo, hidePrivateLinks)); + sb.Append(FormatIssueLink(issue, entryRepo, entryHideLinks)); hasCommentedLinks = true; } } @@ -2496,7 +2522,7 @@ private void RenderEntriesByArea(StringBuilder sb, List entries, sb.Append(' '); if (!string.IsNullOrWhiteSpace(entry.Pr)) { - sb.Append(FormatPrLink(entry.Pr, repo, hidePrivateLinks)); + sb.Append(FormatPrLink(entry.Pr, entryRepo, entryHideLinks)); sb.Append(' '); } @@ -2504,7 +2530,7 @@ private void RenderEntriesByArea(StringBuilder sb, List entries, { foreach (var issue in entry.Issues) { - sb.Append(FormatIssueLink(issue, repo, hidePrivateLinks)); + sb.Append(FormatIssueLink(issue, entryRepo, entryHideLinks)); sb.Append(' '); } } @@ -2513,8 +2539,8 @@ private void RenderEntriesByArea(StringBuilder sb, List entries, if (!string.IsNullOrWhiteSpace(entry.Description)) { // Add blank line before description - // When hidePrivateLinks is true and links exist, add an indented blank line - if (hidePrivateLinks && hasCommentedLinks) + // When hiding links, add an indented blank line if there are commented links + if (entryHideLinks && hasCommentedLinks) { sb.AppendLine(" "); } diff --git a/src/tooling/docs-builder/Arguments/BundleInputParser.cs b/src/tooling/docs-builder/Arguments/BundleInputParser.cs index f97e7011b..7b1ae91f0 100644 --- a/src/tooling/docs-builder/Arguments/BundleInputParser.cs +++ b/src/tooling/docs-builder/Arguments/BundleInputParser.cs @@ -2,30 +2,33 @@ // 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 ConsoleAppFramework; using Elastic.Documentation.Services.Changelog; namespace Documentation.Builder.Arguments; /// -/// Parser for bundle input format: "bundle-file-path, changelog-file-path, repo" +/// Utility class for parsing bundle input format: "bundle-file-path|changelog-file-path|repo|link-visibility" +/// Uses pipe (|) as delimiter since ConsoleAppFramework auto-splits string[] by comma. /// Only bundle-file-path is required. -/// Can be specified multiple times. /// -[AttributeUsage(AttributeTargets.Parameter)] -public class BundleInputParserAttribute : Attribute, IArgumentParser> +public static class BundleInputParser { - public static bool TryParse(ReadOnlySpan s, out List result) + /// + /// Parses a single input string into a BundleInput object. + /// Format: "bundle-file-path|changelog-file-path|repo|link-visibility" (only bundle-file-path is required) + /// Uses pipe (|) as delimiter since ConsoleAppFramework auto-splits string[] by comma. + /// link-visibility can be "hide-links" or "keep-links" (default is keep-links if omitted). + /// + public static BundleInput? Parse(string input) { - result = []; + if (string.IsNullOrWhiteSpace(input)) + return null; - // Split by comma to get parts - var parts = s.ToString().Split(',', StringSplitOptions.TrimEntries); + // Split by pipe to get parts (comma is auto-split by ConsoleAppFramework) + var parts = input.Split('|', StringSplitOptions.TrimEntries); if (parts.Length == 0 || string.IsNullOrWhiteSpace(parts[0])) - { - return false; - } + return null; var bundleInput = new BundleInput { @@ -44,8 +47,37 @@ public static bool TryParse(ReadOnlySpan s, out List result) bundleInput.Repo = parts[2]; } - result.Add(bundleInput); - return true; + // Link visibility is optional (fourth part) - "hide-links" or "keep-links" + if (parts.Length > 3 && !string.IsNullOrWhiteSpace(parts[3])) + { + bundleInput.HideLinks = parts[3].Equals("hide-links", StringComparison.OrdinalIgnoreCase); + } + + return bundleInput; + } + + /// + /// Parses multiple input strings into a list of BundleInput objects. + /// Each input is in format: "bundle-file-path|changelog-file-path|repo|link-visibility" (only bundle-file-path is required) + /// Uses pipe (|) as delimiter since ConsoleAppFramework auto-splits string[] by comma. + /// Multiple bundles can be specified by comma-separating them in a single --input option. + /// link-visibility can be "hide-links" or "keep-links" (default is keep-links if omitted). + /// + public static List ParseAll(string[]? inputs) + { + var result = new List(); + + if (inputs == null || inputs.Length == 0) + return result; + + foreach (var input in inputs) + { + var bundleInput = Parse(input); + if (bundleInput != null) + result.Add(bundleInput); + } + + return result; } } diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs index c57baf3de..3f0d98efa 100644 --- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs +++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs @@ -286,20 +286,18 @@ async static (s, collector, state, ctx) => await s.BundleChangelogs(collector, s /// /// Render bundled changelog(s) to markdown files /// - /// Required: Bundle input(s) in format "bundle-file-path, changelog-file-path, repo". Can be specified multiple times. Only bundle-file-path is required. + /// Required: Bundle input(s) in format "bundle-file-path|changelog-file-path|repo|link-visibility" (use pipe as delimiter). To merge multiple bundles, separate them with commas. Only bundle-file-path is required. link-visibility can be "hide-links" or "keep-links" (default). Paths must be absolute or use environment variables; tilde (~) expansion is not supported. /// 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. For breaking changes with a subtype, groups by subtype instead of area. Defaults to false - /// Optional: Hide private links by commenting them out in the markdown output. Defaults to false /// Filter by feature IDs (comma-separated), or a path to a newline-delimited file containing feature IDs. Can be specified multiple times. Entries with matching feature-id values will be commented out in the markdown output. /// [Command("render")] public async Task Render( - [BundleInputParser] List input, + string[]? input = null, string? output = null, string? title = null, bool subsections = false, - bool hidePrivateLinks = false, string[]? hideFeatures = null, string? config = null, Cancel ctx = default @@ -331,13 +329,15 @@ public async Task Render( } } + // Parse each --input value into BundleInput objects + var bundles = BundleInputParser.ParseAll(input); + var renderInput = new ChangelogRenderInput { - Bundles = input ?? [], + Bundles = bundles, Output = output, Title = title, Subsections = subsections, - HidePrivateLinks = hidePrivateLinks, HideFeatures = allFeatureIds.Count > 0 ? allFeatureIds.ToArray() : null, Config = config };