Skip to content
Closed
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
3 changes: 3 additions & 0 deletions docs/cli/release/changelog-render.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ The `render` command automatically discovers and merges `.amend-*.yaml` files wi
You can configure `block` definitions in your `changelog.yml` configuration file to automatically comment out changelog entries based on their products, areas, and/or types.
For more information, refer to [](/contribute/changelog.md#example-block-label).

When (deprecated) `rules.publish.products` per-product overrides are configured and changelog entries belong to multiple products, the applicable rule is chosen using the intersection + alphabetical first-match algorithm.
For details, refer to [Per-product rule resolution for multi-product entries](/contribute/changelog.md#changelog-bundle-multi-product-rules).

## Output formats

### Markdown format
Expand Down
4 changes: 4 additions & 0 deletions docs/contribute/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ When `--output-products` is not set, the entry's own product list is used as the

`rules.publish` still works for backward compatibility, but will be removed in a future release. The migration is straightforward — copy the same fields from `rules.publish` into `rules.bundle`.

When a changelog entry belongs to more than one product and `rules.publish.products` per-product overrides are configured, the applicable rule is resolved using the same intersection + alphabetical first-match algorithm as `rules.bundle`.
The difference is how the *context* is determined: for `rules.publish`, the context is the **bundle's top-level `products:` array** — the products declared in the bundle YAML file being rendered, not a CLI option.
For details, refer to [Per-product rule resolution for multi-product entries](#changelog-bundle-multi-product-rules).

**Before (deprecated):**

```yaml
Expand Down
2 changes: 2 additions & 0 deletions docs/syntax/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ Each field supports **exclude** (block if matches) or **include** (block if does

For details, refer to the [Rules reference](/contribute/changelog.md#rules-reference).

The `{changelog}` directive selects **one** product's rule using the resolved `:product:` value and applies it uniformly to all entries in the bundle. This differs from the `changelog render` command, which determines the applicable rule per entry using the bundle's top-level `products:` array as context. For details about the `changelog render` behavior for multi-product entries, refer to [Per-product rule resolution for multi-product entries](/contribute/changelog.md#changelog-bundle-multi-product-rules).

## Feature hiding from bundles

When bundles contain a `hide-features` field, entries with matching `feature-id` values are automatically filtered out from the rendered output. This allows you to hide unreleased or experimental features without modifying the bundle at render time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ public static string GetComponent(ChangelogEntry entry, ChangelogRenderContext?

private static PublishBlocker? GetPublishBlockerForEntry(ChangelogEntry entry, ChangelogRenderContext context)
{
var productIds = context.EntryToBundleProducts.GetValueOrDefault(entry);
if (productIds == null || context.Configuration?.Rules?.Publish == null)
var bundleProducts = context.EntryToBundleProducts.GetValueOrDefault(entry);
if (bundleProducts == null || context.Configuration?.Rules?.Publish == null)
return null;

foreach (var productId in productIds)
{
var blocker = GetPublishBlockerForProduct(context.Configuration.Rules.Publish, productId);
if (blocker != null)
return blocker;
}
var publishRules = context.Configuration.Rules.Publish;

return null;
// When no per-product overrides are configured, return the global blocker directly.
if (publishRules.ByProduct is not { Count: > 0 } byProduct)
return publishRules.Blocker;

var entryOwnIds = entry.Products?.Select(p => p.ProductId) ?? [];
return PublishBlockerExtensions.ResolveBlocker(bundleProducts, entryOwnIds, byProduct, publishRules.Blocker);
}

/// <summary>
Expand Down Expand Up @@ -72,32 +72,7 @@ public static bool ShouldHideEntry(
if (context?.Configuration?.Rules?.Publish == null)
return false;

// Get product IDs for this entry
var productIds = context.EntryToBundleProducts.GetValueOrDefault(entry, new HashSet<string>(StringComparer.OrdinalIgnoreCase));
if (productIds.Count == 0)
return false;

// Check each product's publish configuration
foreach (var productId in productIds)
{
var blocker = GetPublishBlockerForProduct(context.Configuration.Rules.Publish, productId);
if (blocker != null && blocker.ShouldBlock(entry))
return true;
}

return false;
}

/// <summary>
/// Gets the publish blocker configuration for a specific product, checking product-specific overrides first
/// </summary>
private static PublishBlocker? GetPublishBlockerForProduct(PublishRules publishRules, string productId)
{
// Check product-specific override first
if (publishRules.ByProduct?.TryGetValue(productId, out var productBlocker) == true)
return productBlocker;

// Fall back to global publish blocker
return publishRules.Blocker;
var blocker = GetPublishBlockerForEntry(entry, context);
return blocker?.ShouldBlock(entry) ?? false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,30 +215,29 @@ private static void EmitBlockedEntryWarnings(
if (context.Configuration?.Rules?.Publish == null)
return;

var publishRules = context.Configuration.Rules.Publish;
var byProduct = publishRules.ByProduct ?? new Dictionary<string, PublishBlocker>(StringComparer.OrdinalIgnoreCase);

var visibleEntries = entries.Where(resolved =>
string.IsNullOrWhiteSpace(resolved.Entry.FeatureId) ||
!context.FeatureIdsToHide.Contains(resolved.Entry.FeatureId));

foreach (var resolved in visibleEntries)
{
// Get product IDs for this entry
var productIds = context.EntryToBundleProducts.GetValueOrDefault(resolved.Entry, new HashSet<string>(StringComparer.OrdinalIgnoreCase));
if (productIds.Count == 0)
var bundleProducts = context.EntryToBundleProducts.GetValueOrDefault(resolved.Entry, new HashSet<string>(StringComparer.OrdinalIgnoreCase));
if (bundleProducts.Count == 0)
continue;

// Check each product's publish configuration
foreach (var productId in productIds)
{
var blocker = GetPublishBlockerForProduct(context.Configuration.Rules.Publish, productId);
if (blocker != null && blocker.ShouldBlock(resolved.Entry))
{
var reasons = GetBlockReasons(resolved.Entry, blocker);
var prefix = blocker.TypesMode == FieldMode.Include || blocker.AreasMode == FieldMode.Include ? "[+include]" : "[-exclude]";
var productInfo = productIds.Count > 1 ? $" for product '{productId}'" : "";
var entryIdentifier = GetEntryIdentifier(resolved.Entry, context);
collector.EmitWarning(string.Empty, $"{prefix} Changelog entry {entryIdentifier} will be commented out{productInfo} because it matches rules configuration: {reasons}");
}
}
var entryOwnIds = resolved.Entry.Products?.Select(p => p.ProductId) ?? [];
var blocker = PublishBlockerExtensions.ResolveBlocker(bundleProducts, entryOwnIds, byProduct, publishRules.Blocker);

if (blocker == null || !blocker.ShouldBlock(resolved.Entry))
continue;

var reasons = GetBlockReasons(resolved.Entry, blocker);
var prefix = blocker.TypesMode == FieldMode.Include || blocker.AreasMode == FieldMode.Include ? "[+include]" : "[-exclude]";
var entryIdentifier = GetEntryIdentifier(resolved.Entry, context);
collector.EmitWarning(string.Empty, $"{prefix} Changelog entry {entryIdentifier} will be commented out because it matches rules configuration: {reasons}");
}
}

Expand Down Expand Up @@ -302,16 +301,6 @@ private static string GetBlockReasons(ChangelogEntry entry, PublishBlocker block
return string.Join(" and ", reasons);
}

private static PublishBlocker? GetPublishBlockerForProduct(PublishRules publishRules, string productId)
{
// Check product-specific override first
if (publishRules.ByProduct?.TryGetValue(productId, out var productBlocker) == true)
return productBlocker;

// Fall back to global publish blocker
return publishRules.Blocker;
}

private static bool ValidateEntryTypes(
IDiagnosticsCollector collector,
IReadOnlyList<ResolvedEntry> entries,
Expand Down
Loading
Loading