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
9 changes: 8 additions & 1 deletion docs/cli/release/changelog-render.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ docs-builder changelog render [options...] [-h|--help]
: Defaults to false.

`--hide-private-links`
: Optional: Hide private links by commenting them out in markdown output.
: 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 <string[]?>`
: 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`).
: When specifying feature IDs directly, provide comma-separated values.
: When specifying a file path, provide a single value that points to a newline-delimited file. The file should contain one feature ID per line.
: Entries with matching `feature-id` values will be commented out in the markdown output and a warning will be emitted.
6 changes: 5 additions & 1 deletion docs/contribute/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,8 @@ Options:
--output <string?> Optional: Output directory for rendered markdown files. Defaults to current directory [Default: null]
--title <string?> 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
--hide-private-links Optional: Hide private links by commenting them out in the markdown output. Defaults to false
--hide-features <string[]?> 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]
```

Before you can use this command you must create changelog files and collect them into bundles.
Expand Down Expand Up @@ -314,3 +315,6 @@ For example, the `index.md` output file contains information derived from the ch
```

To comment out the pull request and issue links, for example if they relate to a private repository, use the `--hide-private-links` option.

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).
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ public class ChangelogRenderInput
public string? Title { get; set; }
public bool Subsections { get; set; }
public bool HidePrivateLinks { get; set; }
public string[]? HideFeatures { get; set; }
}

178 changes: 171 additions & 7 deletions src/services/Elastic.Documentation.Services/ChangelogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1452,17 +1452,131 @@ Cancel ctx
// Convert title to slug format for folder names and anchors (lowercase, dashes instead of spaces)
var titleSlug = TitleToSlug(title);

// Load feature IDs to hide - check if --hide-features contains a file path or a list of feature IDs
var featureIdsToHide = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (input.HideFeatures is { Length: > 0 })
{
// If there's exactly one value, check if it's a file path
if (input.HideFeatures.Length == 1)
{
var singleValue = input.HideFeatures[0];

if (_fileSystem.File.Exists(singleValue))
{
// File exists, read feature IDs from it
var featureIdsFileContent = await _fileSystem.File.ReadAllTextAsync(singleValue, ctx);
var featureIdsFromFile = featureIdsFileContent
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(f => !string.IsNullOrWhiteSpace(f))
.ToArray();

foreach (var featureId in featureIdsFromFile)
{
_ = featureIdsToHide.Add(featureId);
}
}
else
{
// Check if it looks like a file path
var looksLikeFilePath = singleValue.Contains(_fileSystem.Path.DirectorySeparatorChar) ||
singleValue.Contains(_fileSystem.Path.AltDirectorySeparatorChar) ||
_fileSystem.Path.HasExtension(singleValue);

if (looksLikeFilePath)
{
// File path doesn't exist
collector.EmitError(singleValue, $"File does not exist: {singleValue}");
return false;
}
else
{
// Doesn't look like a file path, treat as feature ID
_ = featureIdsToHide.Add(singleValue);
}
}
}
else
{
// Multiple values - process all values first, then check for errors
var nonExistentFiles = new List<string>();
foreach (var value in input.HideFeatures)
{
if (_fileSystem.File.Exists(value))
{
// File exists, read feature IDs from it
var featureIdsFileContent = await _fileSystem.File.ReadAllTextAsync(value, ctx);
var featureIdsFromFile = featureIdsFileContent
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(f => !string.IsNullOrWhiteSpace(f))
.ToArray();

foreach (var featureId in featureIdsFromFile)
{
_ = featureIdsToHide.Add(featureId);
}
}
else
{
// Check if it looks like a file path
var looksLikeFilePath = value.Contains(_fileSystem.Path.DirectorySeparatorChar) ||
value.Contains(_fileSystem.Path.AltDirectorySeparatorChar) ||
_fileSystem.Path.HasExtension(value);

if (looksLikeFilePath)
{
// Track non-existent files to check later
nonExistentFiles.Add(value);
}
else
{
// Doesn't look like a file path, treat as feature ID
_ = featureIdsToHide.Add(value);
}
}
}

// Report errors for non-existent files
if (nonExistentFiles.Count > 0)
{
foreach (var filePath in nonExistentFiles)
{
collector.EmitError(filePath, $"File does not exist: {filePath}");
}
return false;
}
}
}

// Track hidden entries for warnings
var hiddenEntries = new List<(string title, string featureId)>();
foreach (var (entry, _) in allResolvedEntries)
{
if (!string.IsNullOrWhiteSpace(entry.FeatureId) && featureIdsToHide.Contains(entry.FeatureId))
{
hiddenEntries.Add((entry.Title ?? "Unknown", entry.FeatureId));
}
}

// Emit warnings for hidden entries
if (hiddenEntries.Count > 0)
{
foreach (var (entryTitle, featureId) in hiddenEntries)
{
collector.EmitWarning(string.Empty, $"Changelog entry '{entryTitle}' with feature-id '{featureId}' will be commented out in markdown output");
}
}

// Render markdown files (use first repo found, or default)
var repoForRendering = allResolvedEntries.Count > 0 ? allResolvedEntries[0].repo : defaultRepo;

// Render index.md (features, enhancements, bug fixes, security)
await RenderIndexMarkdown(collector, outputDir, title, titleSlug, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, ctx);
await RenderIndexMarkdown(collector, outputDir, title, titleSlug, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, featureIdsToHide, ctx);

// Render breaking-changes.md
await RenderBreakingChangesMarkdown(collector, outputDir, title, titleSlug, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, ctx);
await RenderBreakingChangesMarkdown(collector, outputDir, title, titleSlug, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, featureIdsToHide, ctx);

// Render deprecations.md
await RenderDeprecationsMarkdown(collector, outputDir, title, titleSlug, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, ctx);
await RenderDeprecationsMarkdown(collector, outputDir, title, titleSlug, repoForRendering, allResolvedEntries.Select(e => e.entry).ToList(), entriesByType, input.Subsections, input.HidePrivateLinks, featureIdsToHide, ctx);

_logger.LogInformation("Rendered changelog markdown files to {OutputDir}", outputDir);

Expand Down Expand Up @@ -1501,6 +1615,7 @@ private async Task RenderIndexMarkdown(
Dictionary<string, List<ChangelogData>> entriesByType,
bool subsections,
bool hidePrivateLinks,
HashSet<string> featureIdsToHide,
Cancel ctx
)
{
Expand Down Expand Up @@ -1548,15 +1663,15 @@ 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);
RenderEntriesByArea(sb, combined, repo, subsections, hidePrivateLinks, featureIdsToHide);
}

if (security.Count > 0 || bugFixes.Count > 0)
{
sb.AppendLine();
sb.AppendLine(CultureInfo.InvariantCulture, $"### Fixes [{repo}-{titleSlug}-fixes]");
var combined = security.Concat(bugFixes).ToList();
RenderEntriesByArea(sb, combined, repo, subsections, hidePrivateLinks);
RenderEntriesByArea(sb, combined, repo, subsections, hidePrivateLinks, featureIdsToHide);
}
}
else
Expand Down Expand Up @@ -1586,6 +1701,7 @@ private async Task RenderBreakingChangesMarkdown(
Dictionary<string, List<ChangelogData>> entriesByType,
bool subsections,
bool hidePrivateLinks,
HashSet<string> featureIdsToHide,
Cancel ctx
)
{
Expand All @@ -1608,7 +1724,13 @@ Cancel ctx

foreach (var entry in areaGroup)
{
var shouldHide = !string.IsNullOrWhiteSpace(entry.FeatureId) && featureIdsToHide.Contains(entry.FeatureId);

sb.AppendLine();
if (shouldHide)
{
sb.AppendLine("<!--");
}
sb.AppendLine(CultureInfo.InvariantCulture, $"::::{{dropdown}} {Beautify(entry.Title)}");
sb.AppendLine(entry.Description ?? "% Describe the functionality that changed");
sb.AppendLine();
Expand Down Expand Up @@ -1668,6 +1790,10 @@ Cancel ctx
}

sb.AppendLine("::::");
if (shouldHide)
{
sb.AppendLine("-->");
}
}
}
}
Expand Down Expand Up @@ -1698,6 +1824,7 @@ private async Task RenderDeprecationsMarkdown(
Dictionary<string, List<ChangelogData>> entriesByType,
bool subsections,
bool hidePrivateLinks,
HashSet<string> featureIdsToHide,
Cancel ctx
)
{
Expand All @@ -1720,7 +1847,13 @@ Cancel ctx

foreach (var entry in areaGroup)
{
var shouldHide = !string.IsNullOrWhiteSpace(entry.FeatureId) && featureIdsToHide.Contains(entry.FeatureId);

sb.AppendLine();
if (shouldHide)
{
sb.AppendLine("<!--");
}
sb.AppendLine(CultureInfo.InvariantCulture, $"::::{{dropdown}} {Beautify(entry.Title)}");
sb.AppendLine(entry.Description ?? "% Describe the functionality that was deprecated");
sb.AppendLine();
Expand Down Expand Up @@ -1780,6 +1913,10 @@ Cancel ctx
}

sb.AppendLine("::::");
if (shouldHide)
{
sb.AppendLine("-->");
}
}
}
}
Expand All @@ -1799,7 +1936,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<ChangelogData> entries, string repo, bool subsections, bool hidePrivateLinks)
private void RenderEntriesByArea(StringBuilder sb, List<ChangelogData> entries, string repo, bool subsections, bool hidePrivateLinks, HashSet<string> featureIdsToHide)
{
var groupedByArea = entries.GroupBy(e => GetComponent(e)).ToList();
foreach (var areaGroup in groupedByArea)
Expand All @@ -1813,6 +1950,12 @@ private void RenderEntriesByArea(StringBuilder sb, List<ChangelogData> entries,

foreach (var entry in areaGroup)
{
var shouldHide = !string.IsNullOrWhiteSpace(entry.FeatureId) && featureIdsToHide.Contains(entry.FeatureId);

if (shouldHide)
{
sb.Append("% ");
}
sb.Append("* ");
sb.Append(Beautify(entry.Title));

Expand All @@ -1823,6 +1966,10 @@ private void RenderEntriesByArea(StringBuilder sb, List<ChangelogData> entries,
if (!string.IsNullOrWhiteSpace(entry.Pr))
{
sb.AppendLine();
if (shouldHide)
{
sb.Append("% ");
}
sb.Append(" ");
sb.Append(FormatPrLink(entry.Pr, repo, hidePrivateLinks));
hasCommentedLinks = true;
Expand All @@ -1833,6 +1980,10 @@ private void RenderEntriesByArea(StringBuilder sb, List<ChangelogData> entries,
foreach (var issue in entry.Issues)
{
sb.AppendLine();
if (shouldHide)
{
sb.Append("% ");
}
sb.Append(" ");
sb.Append(FormatIssueLink(issue, repo, hidePrivateLinks));
hasCommentedLinks = true;
Expand Down Expand Up @@ -1877,7 +2028,20 @@ private void RenderEntriesByArea(StringBuilder sb, List<ChangelogData> entries,
sb.AppendLine();
}
var indented = Indent(entry.Description);
sb.AppendLine(indented);
if (shouldHide)
{
// Comment out each line of the description
var indentedLines = indented.Split('\n');
foreach (var line in indentedLines)
{
sb.Append("% ");
sb.AppendLine(line);
}
}
else
{
sb.AppendLine(indented);
}
}
else
{
Expand Down
32 changes: 30 additions & 2 deletions src/tooling/docs-builder/Commands/ChangelogCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ async static (s, collector, state, ctx) => await s.BundleChangelogs(collector, s
/// <param name="output">Optional: Output directory for rendered markdown files. Defaults to current directory</param>
/// <param name="title">Optional: Title to use for section headers in output markdown files. Defaults to version from first bundle</param>
/// <param name="subsections">Optional: Group entries by area/component in subsections. Defaults to false</param>
/// <param name="hidePrivateLinks">Optional: Hide private links by commenting them out in markdown output. Defaults to false</param>
/// <param name="hidePrivateLinks">Optional: Hide private links by commenting them out in the markdown output. Defaults to false</param>
/// <param name="hideFeatures">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.</param>
/// <param name="ctx"></param>
[Command("render")]
public async Task<int> Render(
Expand All @@ -190,20 +191,47 @@ public async Task<int> Render(
string? title = null,
bool subsections = false,
bool hidePrivateLinks = false,
string[]? hideFeatures = null,
Cancel ctx = default
)
{
await using var serviceInvoker = new ServiceInvoker(collector);

var service = new ChangelogService(logFactory, configurationContext, null);

// Process each --hide-features occurrence: each can be comma-separated feature IDs or a file path
var allFeatureIds = new List<string>();
if (hideFeatures is { Length: > 0 })
{
foreach (var hideFeaturesValue in hideFeatures)
{
if (string.IsNullOrWhiteSpace(hideFeaturesValue))
continue;

// Check if it contains commas - if so, split and add each as a feature ID
if (hideFeaturesValue.Contains(','))
{
var commaSeparatedFeatureIds = hideFeaturesValue
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(f => !string.IsNullOrWhiteSpace(f));
allFeatureIds.AddRange(commaSeparatedFeatureIds);
}
else
{
// Single value - pass as-is (will be handled by service layer as file path or feature ID)
allFeatureIds.Add(hideFeaturesValue);
}
}
}

var renderInput = new ChangelogRenderInput
{
Bundles = input ?? [],
Output = output,
Title = title,
Subsections = subsections,
HidePrivateLinks = hidePrivateLinks
HidePrivateLinks = hidePrivateLinks,
HideFeatures = allFeatureIds.Count > 0 ? allFeatureIds.ToArray() : null
};

serviceInvoker.AddCommand(service, renderInput,
Expand Down
Loading
Loading