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
4 changes: 3 additions & 1 deletion config/changelog.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,9 @@ bundle:
# Example: Serverless release profile (filter by promotion report, PR, or issue list)
# serverless-release:
# output: "serverless-{version}.yaml"
# output_products: "cloud-serverless {version}"
# output_products: "cloud-serverless {version}" # omit lifecycle
# # output_products: "cloud-serverless {version} {lifecycle}" # derive (ga for ISO dates)
# # output_products: "cloud-serverless {version} preview" # hardcode for exceptional releases
# # Optional: profile-specific GitHub repo name (overrides bundle.repo if set).
# # Only needed when this profile's product ID differs from the repository name.
# repo: elasticsearch
Expand Down
7 changes: 7 additions & 0 deletions docs/cli/changelog/cmd-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ For standard profiles, `{version}` is copied verbatim from your command argument
| `9.2.0` | `9.2.0` | `ga` |
| `9.2.0-beta.1` | `9.2.0-beta.1` | `beta` |
| `9.2.0-preview.1` | `9.2.0-preview.1` | `preview` |
| `2026-07-21` | `2026-07-21` | `ga` |

Profile mode does not accept lifecycle on the command line. For semver and date-based releases alike, set lifecycle in the profile YAML:

- Omit lifecycle from the pattern (for example, `"cloud-serverless {version}"`) to leave the field out of the bundle.
- Use `{lifecycle}` to derive it from the version or date argument (as in the table above).
- Hardcode a lifecycle (for example, `"cloud-serverless {version} ga"` or `"cloud-serverless {version} preview"`) when you need an explicit value. Non-`ga` date-based releases are exceptional and should use a hardcoded lifecycle.

For more information about acceptable product and lifecycle values, go to [Product format](#product-format).

Expand Down
4 changes: 3 additions & 1 deletion docs/contribute/bundle-changelogs.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ bundle:
1. This profile collects all changelogs from the `directory`.
2. This profile collects any changelogs that have `product: cloud-serverless`, any lifecycle, and the date partially specified in the command.
3. This profile collects any changelogs that have `product: elasticsearch`, `lifecycle: ga`, and the version specified in the command.
4. In this case, the lifecycle is inferred from the version specified in the command. For example, if the version is `9.2.0-beta.1` the lifecycle is `beta`. Refer to [](/cli/changelog/bundle.md#lifecycle-inference).
4. In this case, the lifecycle is inferred from the version specified in the command. For example, if the version is `9.2.0-beta.1` the lifecycle is `beta`. ISO date arguments (for example, `2026-07-21`) derive `ga`. Refer to [](/cli/changelog/bundle.md#lifecycle-inference).

For date-based and semver profiles, lifecycle is controlled only in the profile YAML: omit it from the pattern, use `{lifecycle}` to derive it, or hardcode `ga`, `beta`, or `preview`. Non-`ga` date-based releases are exceptional and should hardcode the lifecycle.

:::{note}
The `products` field determines which changelog files are gathered for consideration. You can still apply [rules](#rules) afterward to further filter changelogs from the bundle. The input stage and bundle filtering stage are conceptually separate.
Expand Down
2 changes: 1 addition & 1 deletion docs/contribute/configure-changelogs-ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ These settings are located in the `bundle.profiles.<name>` section of the config
: When set, the products array in the bundle is exactly the value you specify, replacing anything that would be derived from the matched changelogs. Use this to publish a single, authoritative product entry with a specific version and lifecycle.
: The `{lifecycle}` placeholder is substituted at runtime with the inferred lifecycle. For `source: github_release` profiles this comes from the release tag suffix. For standard profiles it comes from the version argument. Refer to [](/cli/changelog/bundle.md#lifecycle-inference).
: If you omit lifecycle from the pattern (for example, `"elasticsearch {version}"`), the lifecycle field is omitted from the products array entirely.
: Example: `"elasticsearch {version} {lifecycle}"` or `"elasticsearch {version} ga"` to hardcode GA regardless of tag.
: Example: `"elasticsearch {version} {lifecycle}"` or `"elasticsearch {version} ga"` to hardcode GA regardless of tag. The same omit, derive, and hardcode patterns apply to date-based targets (for example, `"cloud-serverless {version}"`, `"cloud-serverless {version} {lifecycle}"`, or `"cloud-serverless {version} preview"` for exceptional non-GA date releases).
: Refer to [](/cli/changelog/bundle.md#product-format).

`owner`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// 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 System.Globalization;

namespace Elastic.Documentation.Configuration.Changelog;

/// <summary>
Expand All @@ -10,12 +12,15 @@ namespace Elastic.Documentation.Configuration.Changelog;
public static class VersionLifecycleInference
{
/// <summary>
/// Infers the lifecycle from a semantic version string.
/// Infers the lifecycle from a semantic version string or ISO date target.
/// </summary>
/// <param name="version">The version string (e.g., "1.0.0", "1.0.0-beta.1", "1.0.0-alpha")</param>
/// <param name="version">The version string (e.g., "1.0.0", "1.0.0-beta.1", "2026-07-21")</param>
/// <returns>The inferred lifecycle string: "ga", "beta", or "preview"</returns>
public static string InferLifecycle(string version)
{
if (DateOnly.TryParseExact(version, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out _))
return "ga";

// Parse semver prerelease
var dashIndex = version.IndexOf('-');
if (dashIndex < 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2931,6 +2931,70 @@ public async Task BundleChangelogs_WithProfile_MalformedProductsPattern_EmitsErr
d.Message.Contains("elasticsearch 9.2.0 ga extra bad", StringComparison.Ordinal));
}

[Fact]
public async Task BundleChangelogs_WithProfile_DateVersionAndLifecyclePlaceholder_WritesGaLifecycle()
{
// language=yaml
var configContent =
"""
bundle:
profiles:
serverless-release:
output: "serverless-{version}.yaml"
output_products: "cloud-serverless {version} {lifecycle}"
""";

var configPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "changelog.yml");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(configPath)!);
await FileSystem.File.WriteAllTextAsync(configPath, configContent, TestContext.Current.CancellationToken);

// language=yaml
var changelog1 =
"""
title: Serverless feature
type: feature
products:
- product: cloud-serverless
target: 2026-07-21
lifecycle: ga
prs:
- https://github.com/elastic/kibana/pull/100
""";

var file1 = FileSystem.Path.Join(_changelogDir, "1755268130-serverless-feature.yaml");
await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken);

var prListPath = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString(), "prs.txt");
FileSystem.Directory.CreateDirectory(FileSystem.Path.GetDirectoryName(prListPath)!);
await FileSystem.File.WriteAllTextAsync(prListPath, "https://github.com/elastic/kibana/pull/100\n", TestContext.Current.CancellationToken);

var outputDir = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString());
FileSystem.Directory.CreateDirectory(outputDir);

var input = new BundleChangelogsArguments
{
Directory = _changelogDir,
Profile = "serverless-release",
ProfileArgument = "2026-07-21",
ProfileReport = prListPath,
Config = configPath,
OutputDirectory = outputDir
};

var result = await ServiceWithConfig.BundleChangelogs(Collector, input, TestContext.Current.CancellationToken);

result.Should().BeTrue($"Expected bundling to succeed, but got errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}");
Collector.Errors.Should().Be(0);

var outputFiles = FileSystem.Directory.GetFiles(outputDir, "*.yaml");
outputFiles.Should().NotBeEmpty("Expected an output file to be created");
var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current.CancellationToken);

bundleContent.Should().Contain("target: 2026-07-21");
bundleContent.Should().Contain("lifecycle: ga", "ISO date version args should derive ga lifecycle, not preview");
bundleContent.Should().NotContain("lifecycle: preview");
}

[Fact]
public async Task BundleChangelogs_WithProfile_RepoAndOwner_WritesValuesToProductEntries()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// 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 AwesomeAssertions;
using Elastic.Documentation.Configuration.Changelog;

namespace Elastic.Documentation.Configuration.Tests.ReleaseNotes;

public class VersionLifecycleInferenceTests
{
[Theory]
[InlineData("9.2.0", "ga")]
[InlineData("9.2.0-beta.1", "beta")]
[InlineData("9.2.0-preview.1", "preview")]
[InlineData("9.2.0-alpha.1", "preview")]
[InlineData("9.2.0-rc.1", "ga")]
[InlineData("2026-07-21", "ga")]
[InlineData("2025-06-01", "ga")]
public void InferLifecycle_InfersCorrectly(string version, string expected)
{
VersionLifecycleInference.InferLifecycle(version).Should().Be(expected);
}
}
Loading