Skip to content

Commit 94a6131

Browse files
authored
[Release notes] Add bundle-amend --remove option (#3513)
1 parent ff3ad36 commit 94a6131

20 files changed

Lines changed: 1119 additions & 184 deletions

File tree

docs/cli-schema.json

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2869,7 +2869,7 @@
28692869
"changelog"
28702870
],
28712871
"name": "bundle-amend",
2872-
"summary": "Append additional changelog entries to a published bundle without modifying it.",
2872+
"summary": "Append or exclude changelog entries in a published bundle without modifying it.",
28732873
"notes": "Creates an immutable .amend-N.yaml sidecar file alongside the original bundle.",
28742874
"usage": "docs-builder changelog bundle-amend \u003Cbundle-path\u003E [options]",
28752875
"examples": [],
@@ -2901,7 +2901,16 @@
29012901
"name": "add",
29022902
"type": "array",
29032903
"required": false,
2904-
"summary": "Required: Path(s) to changelog YAML file(s) to add as comma-separated values (e.g., --add \u0022file1.yaml,file2.yaml\u0022). Supports tilde (~) expansion and relative paths.",
2904+
"summary": "Optional: Changelog YAML paths to add. Repeat --add or pass a comma-separated list in one value (for example, --add \u0022file1.yaml,file2.yaml\u0022). Supports tilde (~) expansion and relative paths.",
2905+
"repeatable": true,
2906+
"elementType": "string"
2907+
},
2908+
{
2909+
"role": "flag",
2910+
"name": "remove",
2911+
"type": "array",
2912+
"required": false,
2913+
"summary": "Optional: Changelog YAML paths to exclude from the effective bundle. Repeat --remove or pass a comma-separated list in one value. Supports tilde (~) expansion and relative paths.",
29052914
"repeatable": true,
29062915
"elementType": "string"
29072916
},
@@ -2910,9 +2919,25 @@
29102919
"name": "resolve",
29112920
"type": "boolean",
29122921
"required": false,
2913-
"summary": "Optional: Copy the contents of each changelog file into the entries array. Use --no-resolve to explicitly turn off resolve (overrides inference from original bundle).",
2922+
"summary": "Optional: When using --add, inline each added changelog\u0027s content in the amend file. Use --no-resolve to record file references only. When omitted, inferred from the parent bundle. Does not apply to --remove.",
29142923
"defaultValue": "default"
29152924
},
2925+
{
2926+
"role": "flag",
2927+
"name": "force",
2928+
"type": "boolean",
2929+
"required": false,
2930+
"summary": "Optional: When removing, match by file name even if the bundle checksum differs from the file on disk.",
2931+
"defaultValue": "false"
2932+
},
2933+
{
2934+
"role": "flag",
2935+
"name": "dry-run",
2936+
"type": "boolean",
2937+
"required": false,
2938+
"summary": "Optional: Preview changes without writing an amend file.",
2939+
"defaultValue": "false"
2940+
},
29162941
{
29172942
"role": "flag",
29182943
"name": "log-level",

docs/cli/changelog/cmd-bundle-amend.md

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
## Description
22

3-
Amend a bundle with additional changelog entries.
3+
Amend a bundle with additional or excluded changelog entries without modifying the parent bundle file.
44
Amend bundles follow a specific naming convention: `{parent-bundle-name}.amend-{N}.yaml` where `{N}` is a sequence number.
55

6+
Specify at least one of `--add` or `--remove`.
7+
68
To create a bundle, use [](/cli/changelog/bundle.md).
79
For details and examples, go to [](/contribute/bundle-changelogs.md).
810

911
## Resolve behaviour
1012

11-
By default, the `bundle-amend` command **infers** whether to resolve entries from the original bundle.
13+
By default, the `bundle-amend` command **infers** whether to resolve entries from the original bundle when you use `--add`.
1214
If the original bundle contains resolved entries (with inline `title`, `type`, and so on), the amend file will also be resolved.
1315
If the original bundle contains only file references, the amend file will also contain only file references.
1416

@@ -19,9 +21,13 @@ You can override this behaviour:
1921
- `--resolve`: Force entries to be resolved (inline content), regardless of the original bundle.
2022
- `--no-resolve`: Force entries to contain only file references, regardless of the original bundle.
2123

24+
`--resolve` and `--no-resolve` apply only to `--add`. Removals always record `exclude-entries` with file name and checksum.
25+
2226
## Output
2327

24-
Amend bundles contain only the additional entries, not a full repetition of the original bundle. For example:
28+
Amend bundles contain only the changes for that amend file, not a full repetition of the original bundle.
29+
30+
Additions:
2531

2632
```yaml
2733
# 9.3.0.amend-1.yaml
@@ -31,8 +37,20 @@ entries:
3137
checksum: abc123def456
3238
```
3339
34-
When bundles are loaded (either via the `changelog render` command or the `{changelog}` directive), amend files are **automatically merged** with their parent bundles.
35-
The entries from all matching amend files are combined with the parent bundle's entries, and the result is rendered as a single release.
40+
Removals:
41+
42+
```yaml
43+
# 9.3.0.amend-2.yaml
44+
exclude-entries:
45+
- file:
46+
name: 138723.yaml
47+
checksum: def456abc123
48+
```
49+
50+
An amend file can contain both `exclude-entries` and `entries`. Within each amend file, exclusions are applied before additions.
51+
52+
When bundles are loaded (either via the `changelog render` command or the `{changelog}` directive), amend files are **automatically merged** with their parent bundles in sequence (`amend-1`, `amend-2`, …).
53+
The result is rendered as a single release.
3654

3755
:::{note}
3856
Amend bundles do not need to include `products` or `hide-features` fields — they inherit these from their parent bundle. If an amend bundle is found without a matching parent bundle, it remains standalone.
@@ -52,14 +70,53 @@ docs-builder changelog bundle-amend \
5270

5371
The new bundle automatically matches the resolve style of the original bundle.
5472

73+
### Remove a changelog from a bundle
74+
75+
```sh
76+
docs-builder changelog bundle-amend \
77+
./docs/changelog/bundles/9.3.0.yaml \
78+
--remove ./docs/changelog/138723.yaml
79+
```
80+
81+
The CLI computes the file checksum automatically and matches it against the effective bundle (parent plus any existing amend files).
82+
If the bundle contains the file with a different checksum, the command fails unless you pass `--force` to remove by file name only.
83+
5584
### Add multiple changelogs to a bundle
5685

86+
Comma-separated list:
87+
5788
```sh
5889
docs-builder changelog bundle-amend \
5990
./docs/changelog/bundles/9.3.0.yaml \
6091
--add "./docs/changelog/138723.yaml,./docs/changelog/1770424335.yaml"
6192
```
6293

94+
Or repeat `--add`:
95+
96+
```sh
97+
docs-builder changelog bundle-amend \
98+
./docs/changelog/bundles/9.3.0.yaml \
99+
--add ./docs/changelog/138723.yaml \
100+
--add ./docs/changelog/1770424335.yaml
101+
```
102+
103+
### Remove multiple changelogs from a bundle
104+
105+
```sh
106+
docs-builder changelog bundle-amend \
107+
./docs/changelog/bundles/9.3.0.yaml \
108+
--remove "./docs/changelog/old-a.yaml,./docs/changelog/old-b.yaml"
109+
```
110+
111+
### Replace an entry in one amend file
112+
113+
```sh
114+
docs-builder changelog bundle-amend \
115+
./docs/changelog/bundles/9.3.0.yaml \
116+
--remove ./docs/changelog/old-entry.yaml \
117+
--add ./docs/changelog/new-entry.yaml
118+
```
119+
63120
### Force resolve or file-reference style
64121

65122
```sh
@@ -74,3 +131,12 @@ docs-builder changelog bundle-amend 9.3.0.yaml \
74131
--add ./docs/changelog/late-addition.yaml \
75132
--no-resolve
76133
```
134+
135+
### Preview without writing an amend file
136+
137+
```sh
138+
docs-builder changelog bundle-amend \
139+
./docs/changelog/bundles/9.3.0.yaml \
140+
--remove ./docs/changelog/138723.yaml \
141+
--dry-run
142+
```

docs/cli/changelog/cmd-remove.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ Before deleting anything, the command checks whether any matching files are refe
1111

1212
For more context, go to [](/contribute/bundle-changelogs.md#changelog-remove).
1313

14+
## Bundle dependency check
15+
16+
The dependency scan considers the **effective** entry list for each parent bundle: parent `entries` merged with amend sidecars (`{bundle}.amend-N.yaml`) in numeric order. Within each amend file, `exclude-entries` are applied before additions, matching how bundles are loaded for [`changelog render`](/cli/changelog/render.md) and the `{changelog}` directive.
17+
18+
A changelog file blocks deletion only when it still appears in that effective list as an **unresolved** entry (the bundle references the file by name and checksum rather than embedding inline title and type). Resolved entries do not require the source file on disk.
19+
20+
If you removed an entry from a bundle with [`changelog bundle-amend --remove`](/cli/changelog/bundle-amend.md), the corresponding `exclude-entries` record drops it from the effective list, so `changelog remove` can delete the source file even when the parent bundle still lists it.
21+
22+
Amend sidecar files are not scanned as parent bundles themselves—only the parent bundle file plus its amend chain is evaluated. If an amend file cannot be parsed, the command logs a warning and uses the parent bundle entries only for that dependency check.
23+
1424
## Directory resolution
1525

1626
Both modes use the same ordered fallback to locate changelog YAML files and existing bundles.

docs/contribute/bundle-changelogs.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,16 @@ docs-builder changelog bundle-amend \
249249

250250
Amend bundles follow a specific naming convention: `{parent-bundle-name}.amend-{N}.yaml` where `{N}` is a sequence number.
251251

252-
:::{note}
253-
There is currently no command to **remove** changelogs from a bundle. You must edit the bundle file manually or else re-generate the bundle with an updated source of truth or a new rule that excludes the changelog.
254-
:::
252+
To remove entries from an existing bundle without editing the parent file, use `--remove` on the same command:
253+
254+
```sh
255+
docs-builder changelog bundle-amend \
256+
./docs/releases/9.3.0.yaml \
257+
--remove "./docs/changelog/138723.yaml"
258+
```
259+
260+
This creates an amend file with `exclude-entries` that is merged when the bundle is rendered.
261+
After excluding an entry from unresolved bundles, you can use `changelog remove` to delete the source changelog file.
255262

256263
When bundles are turned into docs (either via the `changelog render` command or the `{changelog}` directive), amend files are **automatically merged** with their parent bundles.
257264
The changelogs from all matching amend files are combined with the parent bundle's changelogs and the result is rendered as a single release.

docs/syntax/changelog.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ Bundles with the same target version/date are automatically merged into a single
286286

287287
Bundles can have associated **amend files** that follow the naming pattern `{bundle-name}.amend-{N}.yaml` (e.g., `9.3.0.amend-1.yaml`). When loading bundles, the directive automatically discovers and merges amend files with their parent bundles.
288288

289-
This allows you to add late additions to a release without modifying the original bundle file:
289+
This allows you to add or remove late changes to a release without modifying the original bundle file:
290290

291291
```
292292
bundles/
@@ -295,6 +295,8 @@ bundles/
295295
└── 9.3.0.amend-2.yaml # Second amend (auto-merged with parent)
296296
```
297297

298+
Amend files may contain `entries` (additions) and `exclude-entries` (removals). Within each amend file, exclusions are applied before additions. Amend files are processed in numeric order.
299+
298300
All entries from the parent and amend bundles are rendered together as a single release section. The parent bundle's metadata (products, hide-features, repo) is preserved.
299301

300302
## Default folder structure
@@ -386,8 +388,9 @@ This prevents silent data loss where changelog entries would be quietly omitted
386388
To fix this, either:
387389

388390
- Restore the missing changelog files, or
389-
- Re-create the bundle with `--resolve` to embed entry content directly (making the bundle self-contained), or
390-
- Remove the unresolvable entry from the bundle file.
391+
- Re-create the bundle with `--resolve` to embed entry content directly (making the bundle self-contained).
392+
393+
`bundle-amend --remove` only applies when the source changelog file is still available (for example, to drop an entry from the effective bundle before you delete the file with `changelog remove`).
391394

392395
:::{tip}
393396
In general, if you want to be able to remove changelog files after your releases, create your bundles with the `--resolve` option or set `bundle.resolve` to `true` in the changelog configuration file.

src/Elastic.Documentation.Configuration/ReleaseNotes/Bundle.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ public sealed record BundleDto
3030
[YamlMember(Alias = "hide-features", ApplyNamingConventions = false)]
3131
public List<string>? HideFeatures { get; set; }
3232
public List<BundledEntryDto>? Entries { get; set; }
33+
/// <summary>
34+
/// Entries to exclude when this amend file is merged with its parent bundle.
35+
/// </summary>
36+
[YamlMember(Alias = "exclude-entries", ApplyNamingConventions = false)]
37+
public List<BundledEntryDto>? ExcludeEntries { get; set; }
3338
}
3439

3540
/// <summary>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Globalization;
6+
using System.Text.RegularExpressions;
7+
using Elastic.Documentation.ReleaseNotes;
8+
9+
namespace Elastic.Documentation.Configuration.ReleaseNotes;
10+
11+
/// <summary>
12+
/// Merges parent bundle entries with amend files (exclusions first, then additions per amend).
13+
/// </summary>
14+
public static partial class BundleAmendMerger
15+
{
16+
[GeneratedRegex(@"\.amend-(\d+)\.ya?ml$", RegexOptions.IgnoreCase)]
17+
private static partial Regex AmendFileRegex();
18+
19+
/// <summary>Whether a path is an amend sidecar (<c>{name}.amend-{N}.yaml</c>).</summary>
20+
public static bool IsAmendFile(string filePath) => AmendFileRegex().IsMatch(filePath);
21+
22+
/// <summary>Numeric suffix from an amend file path; <c>0</c> when not an amend file.</summary>
23+
public static int GetAmendFileNumber(string filePath)
24+
{
25+
var match = AmendFileRegex().Match(filePath);
26+
return match.Success && int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var number)
27+
? number
28+
: 0;
29+
}
30+
31+
/// <summary>
32+
/// Applies amend bundles in order to parent entries and returns the effective entry list.
33+
/// </summary>
34+
public static List<BundledEntry> MergeEntries(
35+
IReadOnlyList<BundledEntry> parentEntries,
36+
IReadOnlyList<Bundle> amendBundlesInOrder)
37+
{
38+
var current = parentEntries.ToList();
39+
foreach (var amend in amendBundlesInOrder)
40+
current = ApplySingleAmend(current, amend);
41+
return current;
42+
}
43+
44+
/// <summary>
45+
/// Collects all exclusion keys already applied by prior amend files.
46+
/// </summary>
47+
public static HashSet<string> CollectAppliedExclusionKeys(
48+
IReadOnlyList<Bundle> amendBundlesInOrder)
49+
{
50+
var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
51+
foreach (var amend in amendBundlesInOrder)
52+
{
53+
foreach (var exclusion in amend.ExcludeEntries)
54+
_ = keys.Add(BuildExclusionKey(exclusion));
55+
}
56+
return keys;
57+
}
58+
59+
/// <summary>Whether a bundled entry matches an exclusion record.</summary>
60+
public static bool EntryMatchesExclusion(BundledEntry entry, BundledEntry exclusion)
61+
{
62+
var entryFileName = NormalizeFileName(entry.File?.Name);
63+
var exclusionFileName = NormalizeFileName(exclusion.File?.Name);
64+
if (string.IsNullOrEmpty(entryFileName) || string.IsNullOrEmpty(exclusionFileName))
65+
return false;
66+
67+
if (!string.Equals(entryFileName, exclusionFileName, StringComparison.OrdinalIgnoreCase))
68+
return false;
69+
70+
if (string.IsNullOrWhiteSpace(exclusion.File?.Checksum))
71+
return true;
72+
73+
return string.Equals(entry.File?.Checksum, exclusion.File.Checksum, StringComparison.OrdinalIgnoreCase);
74+
}
75+
76+
/// <summary>Builds a stable key for an exclusion record (file name + checksum).</summary>
77+
public static string BuildExclusionKey(BundledEntry exclusion)
78+
{
79+
var name = NormalizeFileName(exclusion.File?.Name) ?? string.Empty;
80+
var checksum = exclusion.File?.Checksum ?? string.Empty;
81+
return $"{name}|{checksum}";
82+
}
83+
84+
private static List<BundledEntry> ApplySingleAmend(IReadOnlyList<BundledEntry> entries, Bundle amend)
85+
{
86+
var result = ApplyExclusions(entries, amend.ExcludeEntries);
87+
if (amend.Entries.Count > 0)
88+
result.AddRange(amend.Entries);
89+
return result;
90+
}
91+
92+
private static List<BundledEntry> ApplyExclusions(
93+
IReadOnlyList<BundledEntry> entries,
94+
IReadOnlyList<BundledEntry> exclusions)
95+
{
96+
if (exclusions.Count == 0)
97+
return entries.ToList();
98+
99+
return entries
100+
.Where(entry => !exclusions.Any(exclusion => EntryMatchesExclusion(entry, exclusion)))
101+
.ToList();
102+
}
103+
104+
private static string? NormalizeFileName(string? fileName)
105+
{
106+
if (string.IsNullOrWhiteSpace(fileName))
107+
return null;
108+
109+
var normalized = fileName.Replace('\\', '/');
110+
var lastSlash = normalized.LastIndexOf('/');
111+
return lastSlash >= 0 ? normalized[(lastSlash + 1)..] : normalized;
112+
}
113+
}

0 commit comments

Comments
 (0)