Skip to content

Commit 9a34861

Browse files
lcawlcotti
andauthored
Add bundle release date command options (#3072)
Co-authored-by: Felipe Cotti <felipe.cotti@elastic.co>
1 parent 4cdebe6 commit 9a34861

23 files changed

Lines changed: 486 additions & 70 deletions

File tree

config/changelog.example.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ bundle:
235235
# repo: elasticsearch
236236
# Optional: default GitHub owner applied to all profiles that do not specify their own.
237237
# owner: elastic
238+
# Optional: control auto-population of release-date for all profiles by default.
239+
# When true (default), auto-populate release dates. Profiles can override this setting.
240+
# release_dates: true
238241

239242
# Named bundle profiles for different release scenarios.
240243
# Profiles can be used with both 'changelog bundle' and 'changelog remove':
@@ -259,6 +262,9 @@ bundle:
259262
# # - Bug fixes and stability enhancements
260263
# #
261264
# # Download the release binaries: https://github.com/{owner}/{repo}/releases/tag/v{version}
265+
# # Optional: control auto-population of release-date for this profile.
266+
# # When true (default), auto-populate release dates. When false, equivalent to --no-release-date.
267+
# # release_dates: true
262268
# Example: GitHub release profile (fetches PR list directly from a GitHub release)
263269
# Use when you want to bundle or remove changelogs based on a published GitHub release.
264270
# elasticsearch-gh-release:

docs/cli/changelog/bundle.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,18 @@ The `--input-products` option determines which changelog files are gathered for
124124
: This value replaces information that would otherwise be derived from changelogs.
125125
: When `rules.bundle.products` per-product overrides are configured, `--output-products` also supplies the product IDs used to choose the **rule context product** (first alphabetically) for Mode 3. To use a different product's rules, run a separate bundle with only that product in `--output-products`. For details, refer to [Product-specific bundle rules](/contribute/configure-changelogs.md#rules-bundle-products).
126126

127+
`--no-release-date`
128+
: Optional: Skip auto-population of release date in the bundle.
129+
: By default, bundles are created with a `release-date` field set to today's date (UTC) or the GitHub release published date when using `--release-version`.
130+
: Mutually exclusive with `--release-date`.
131+
: **Not available in profile mode** — use bundle configuration instead.
132+
133+
`--release-date <string?>`
134+
: Optional: Explicit release date for the bundle in YYYY-MM-DD format.
135+
: Overrides the default auto-population behavior (today's date or GitHub release published date).
136+
: Mutually exclusive with `--no-release-date`.
137+
: **Not available in profile mode** — use bundle configuration instead.
138+
127139
`--owner <string?>`
128140
: Optional: The GitHub repository owner, required when pull requests or issues are specified as numbers.
129141
: Precedence: `--owner` flag > `bundle.owner` in `changelog.yml` > `elastic`.

docs/cli/changelog/gh-release.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ docs-builder changelog gh-release <repo> [version] [options...] [-h|--help]
3838
`--output <string?>`
3939
: Optional: Output directory for the generated changelog files. Falls back to `bundle.directory` in `changelog.yml` when not specified. Defaults to `./changelogs`.
4040

41+
`--release-date <string?>`
42+
: Optional: Explicit release date for the bundle in YYYY-MM-DD format.
43+
: By default, the bundle uses the GitHub release's published date. This option overrides that behavior.
44+
: If the GitHub release has no published date, falls back to today's date (UTC).
45+
4146
`--strip-title-prefix`
4247
: Optional: Remove square brackets and the text within them from the beginning of pull request titles, and also remove a colon if it follows the closing bracket.
4348
: For example, `"[Inference API] New embedding model support"` becomes `"New embedding model support"`.

docs/syntax/changelog.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ The directive supports the following options:
2525
| `:type: value` | Filter entries by type | Excludes separated types |
2626
| `:subsections:` | Group entries by area/component | false |
2727
| `:link-visibility: value` | Visibility of pull request (PR) and issue links | `auto` |
28-
| `:config: path` | Path to `changelog.yml` configuration (reserved for future use) | auto-discover |
28+
| `:config: path` | Path to `changelog.yml` configuration | auto-discover |
2929

3030
### Example with options
3131

@@ -122,11 +122,12 @@ If a changelog has multiple area values, only the first one is used.
122122

123123
#### `:config:`
124124

125-
Explicit path to a `changelog.yml` configuration file. If not specified, the directive auto-discovers from:
126-
1. `changelog.yml` in the docset root
127-
2. `docs/changelog.yml` relative to docset root
125+
Explicit path to a `changelog.yml` or `changelog.yaml` configuration file, relative to the documentation source directory. If not specified, the directive auto-discovers from these locations (first match wins):
128126

129-
Reserved for future configuration use. The directive does not currently load or apply configuration from this file.
127+
1. `changelog.yml` or `changelog.yaml` in the documentation source directory
128+
2. `changelog.yml` or `changelog.yaml` in the parent directory (typically the repository root)
129+
130+
Both explicit and auto-discovered paths must resolve within the repository checkout directory and must not traverse symlinks.
130131

131132
## Filtering entries with bundle rules
132133

@@ -249,7 +250,7 @@ Download the release binaries: https://github.com/elastic/elasticsearch/releases
249250
...
250251
```
251252

252-
When present, the `release-date` field is rendered immediately after the version heading as italicized text (e.g., `_Released: 2026-04-09_`). This is purely informative for end-users and is especially useful for components released outside the usual stack lifecycle, such as APM agents and EDOT agents.
253+
When present, the `release-date` field is rendered immediately after the version heading as italicized text (e.g., `_Released: April 9, 2026_`). This is purely informative for end-users and is especially useful for components released outside the usual stack lifecycle, such as APM agents and EDOT agents. If the `release-date` field is present in a bundle, it is always displayed. To control release dates, set `release_dates: false` at the bundle or profile level in the configuration (see [profile configuration](/cli/changelog/bundle.md)); when false, this prevents the date from being written to the bundle during bundling. Defaults to true when omitted.
253254

254255
Bundle descriptions are rendered when present in the bundle YAML file. The description appears after the release date (if any) but before any entry sections. Descriptions support Markdown formatting including links, lists, and multiple paragraphs.
255256

src/Elastic.Documentation.Configuration/Changelog/BundleConfiguration.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ public record BundleConfiguration
5151
/// </summary>
5252
public IReadOnlyList<string>? LinkAllowRepos { get; init; }
5353

54+
/// <summary>
55+
/// When true, auto-populate release date in bundle output. Defaults to true when omitted.
56+
/// </summary>
57+
public bool? ReleaseDates { get; init; }
58+
5459
/// <summary>
5560
/// Named bundle profiles for different release scenarios.
5661
/// </summary>
@@ -111,6 +116,11 @@ public record BundleProfile
111116
/// </summary>
112117
public IReadOnlyList<string>? HideFeatures { get; init; }
113118

119+
/// <summary>
120+
/// When true, auto-populate release date in bundle output. Defaults to true when omitted.
121+
/// </summary>
122+
public bool? ReleaseDates { get; init; }
123+
114124
/// <summary>
115125
/// Profile source type. When set to <c>"github_release"</c>, the profile fetches
116126
/// PR references directly from a GitHub release and uses them as the bundle filter.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,9 @@ private static LoadedBundle MergeBundleGroup(IGrouping<string, LoadedBundle> gro
242242
_ => releaseDates[0]
243243
};
244244

245-
var mergedData = first.Data with { Description = mergedDescription, ReleaseDate = mergedReleaseDate };
245+
var mergedData = first.Data != null
246+
? first.Data with { Description = mergedDescription, ReleaseDate = mergedReleaseDate }
247+
: new Bundle { Description = mergedDescription, ReleaseDate = mergedReleaseDate };
246248

247249
return new LoadedBundle(
248250
first.Version,

src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.IO.Abstractions;
56
using Elastic.Documentation;
67
using Elastic.Documentation.Configuration;
78
using Elastic.Documentation.Configuration.Assembler;
@@ -287,37 +288,91 @@ private void ExtractBundlesFolderPath()
287288
}
288289

289290
/// <summary>
290-
/// Reserved for future config loading (e.g., bundle.directory). The directive no longer applies rules.publish.
291-
/// Emits a warning when an explicit :config: path is specified but the file is not found.
291+
/// Loads changelog configuration settings from the config file.
292+
/// Uses the explicit :config: path if specified, otherwise auto-discovers changelog.yml.
293+
/// Reserved for future directive-relevant settings.
292294
/// </summary>
293-
private void LoadConfiguration()
294-
{
295-
if (string.IsNullOrWhiteSpace(ConfigPath))
296-
return;
295+
private void LoadConfiguration() =>
296+
// Config file resolution is kept so the path validation infrastructure
297+
// stays exercised; settings are currently handled at bundle time.
298+
_ = ResolveConfigPath();
297299

298-
var trimmedPath = ConfigPath.TrimStart('/');
299-
if (Path.IsPathRooted(trimmedPath))
300+
/// <summary>
301+
/// The trust boundary for changelog config file resolution: checkout (git) root
302+
/// when available, otherwise the documentation source directory.
303+
/// Both explicit <c>:config:</c> paths and auto-discovered candidates are validated
304+
/// against this same root.
305+
/// </summary>
306+
private IDirectoryInfo ConfigTrustRoot =>
307+
Build.DocumentationCheckoutDirectory ?? Build.DocumentationSourceDirectory;
308+
309+
private string? ResolveConfigPath()
310+
{
311+
if (!string.IsNullOrWhiteSpace(ConfigPath))
300312
{
301-
this.EmitError("Changelog config path must not be an absolute path.");
302-
return;
313+
// A leading '/' or '\' is treated as relative to docset root
314+
var trimmedPath = ConfigPath.TrimStart('/', '\\');
315+
if (Path.IsPathRooted(trimmedPath))
316+
{
317+
this.EmitError("Changelog config path must not be an absolute path.");
318+
return null;
319+
}
320+
321+
var explicitPath = Path.GetFullPath(Build.DocumentationSourceDirectory.ResolvePathFrom(trimmedPath));
322+
return ValidateConfigCandidate(explicitPath, emitDiagnostics: true);
303323
}
304324

305-
var explicitPath = Path.GetFullPath(Build.DocumentationSourceDirectory.ResolvePathFrom(ConfigPath));
306-
var file = Build.ReadFileSystem.FileInfo.New(explicitPath);
307-
if (!file.IsSubPathOf(Build.DocumentationSourceDirectory))
325+
// Auto-discover: try .yml and .yaml in each candidate location.
326+
string[] relativePaths =
327+
[
328+
"changelog.yml", "changelog.yaml",
329+
"../changelog.yml", "../changelog.yaml"
330+
];
331+
332+
return relativePaths
333+
.Select(rel => Path.GetFullPath(Build.DocumentationSourceDirectory.ResolvePathFrom(rel)))
334+
.Select(abs => ValidateConfigCandidate(abs, emitDiagnostics: false))
335+
.FirstOrDefault(p => p != null);
336+
}
337+
338+
/// <summary>
339+
/// Validates a config file candidate against the shared trust rules:
340+
/// must be within <see cref="ConfigTrustRoot"/>, must not be/traverse symlinks,
341+
/// and must exist on the (scoped) filesystem.
342+
/// </summary>
343+
private string? ValidateConfigCandidate(string fullPath, bool emitDiagnostics)
344+
{
345+
try
308346
{
309-
this.EmitError("Changelog config path must resolve within the documentation source directory.");
310-
return;
311-
}
347+
var file = Build.ReadFileSystem.FileInfo.New(fullPath);
348+
349+
if (!file.IsSubPathOf(ConfigTrustRoot))
350+
{
351+
if (emitDiagnostics)
352+
this.EmitError("Changelog config path must resolve within the documentation directory.");
353+
return null;
354+
}
355+
356+
if (SymlinkValidator.ValidateFileAccess(file, ConfigTrustRoot) is { } accessError)
357+
{
358+
if (emitDiagnostics)
359+
this.EmitError(accessError);
360+
return null;
361+
}
312362

313-
if (SymlinkValidator.ValidateFileAccess(file, Build.DocumentationSourceDirectory) is { } accessError)
363+
if (!Build.ReadFileSystem.File.Exists(fullPath))
364+
{
365+
if (emitDiagnostics)
366+
this.EmitWarning($"Specified changelog config path '{ConfigPath}' not found.");
367+
return null;
368+
}
369+
370+
return fullPath;
371+
}
372+
catch
314373
{
315-
this.EmitError(accessError);
316-
return;
374+
return null;
317375
}
318-
319-
if (!Build.ReadFileSystem.File.Exists(explicitPath))
320-
this.EmitWarning($"Specified changelog config path '{ConfigPath}' not found.");
321376
}
322377

323378
/// <summary>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 YamlDotNet.Serialization;
6+
7+
namespace Elastic.Markdown.Myst.Directives.Changelog;
8+
9+
/// <summary>
10+
/// Minimal YAML DTO for reading changelog.yml settings needed by the {changelog} directive.
11+
/// Only the fields relevant to directive rendering are included; everything else is ignored.
12+
/// </summary>
13+
[YamlSerializable]
14+
internal sealed record ChangelogDirectiveConfigYaml
15+
{
16+
public ChangelogDirectiveBundleConfigYaml? Bundle { get; set; }
17+
}
18+
19+
/// <summary>
20+
/// Minimal bundle section from changelog.yml, containing only directive-relevant settings.
21+
/// Reserved for future directive-relevant settings.
22+
/// </summary>
23+
[YamlSerializable]
24+
internal sealed record ChangelogDirectiveBundleConfigYaml;

src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ private static string GenerateMarkdown(
178178

179179
_ = sb.AppendLine(CultureInfo.InvariantCulture, $"## {title}");
180180

181-
// Add release date if present
182181
if (releaseDate is { } date)
183182
{
184183
_ = sb.AppendLine();

src/Elastic.Markdown/Myst/YamlSerialization.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using Elastic.Documentation.AppliesTo;
66
using Elastic.Documentation.Configuration.Products;
7+
using Elastic.Markdown.Myst.Directives.Changelog;
78
using Elastic.Markdown.Myst.Directives.Contributors;
89
using Elastic.Markdown.Myst.Directives.Settings;
910
using Elastic.Markdown.Myst.FrontMatter;
@@ -39,4 +40,6 @@ public static T Deserialize<T>(string yaml, ProductsConfiguration products)
3940
[YamlSerializable(typeof(SettingMutability))]
4041
[YamlSerializable(typeof(ApplicableTo))]
4142
[YamlSerializable(typeof(ContributorEntry))]
43+
[YamlSerializable(typeof(ChangelogDirectiveConfigYaml))]
44+
[YamlSerializable(typeof(ChangelogDirectiveBundleConfigYaml))]
4245
public partial class DocsBuilderYamlStaticContext;

0 commit comments

Comments
 (0)