diff --git a/config/changelog.example.yml b/config/changelog.example.yml index 5fdc00164..99eae8baa 100644 --- a/config/changelog.example.yml +++ b/config/changelog.example.yml @@ -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 diff --git a/docs/cli/changelog/cmd-bundle.md b/docs/cli/changelog/cmd-bundle.md index 46478fefd..52551f515 100644 --- a/docs/cli/changelog/cmd-bundle.md +++ b/docs/cli/changelog/cmd-bundle.md @@ -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). diff --git a/docs/contribute/bundle-changelogs.md b/docs/contribute/bundle-changelogs.md index c283a3101..180978fa5 100644 --- a/docs/contribute/bundle-changelogs.md +++ b/docs/contribute/bundle-changelogs.md @@ -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. diff --git a/docs/contribute/configure-changelogs-ref.md b/docs/contribute/configure-changelogs-ref.md index d993c0f21..13398e8f6 100644 --- a/docs/contribute/configure-changelogs-ref.md +++ b/docs/contribute/configure-changelogs-ref.md @@ -137,7 +137,7 @@ These settings are located in the `bundle.profiles.` 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` diff --git a/src/Elastic.Documentation.Configuration/Changelog/VersionLifecycleInference.cs b/src/Elastic.Documentation.Configuration/Changelog/VersionLifecycleInference.cs index a0ccb0e06..2cfe125a2 100644 --- a/src/Elastic.Documentation.Configuration/Changelog/VersionLifecycleInference.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/VersionLifecycleInference.cs @@ -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; /// @@ -10,12 +12,15 @@ namespace Elastic.Documentation.Configuration.Changelog; public static class VersionLifecycleInference { /// - /// Infers the lifecycle from a semantic version string. + /// Infers the lifecycle from a semantic version string or ISO date target. /// - /// The version string (e.g., "1.0.0", "1.0.0-beta.1", "1.0.0-alpha") + /// The version string (e.g., "1.0.0", "1.0.0-beta.1", "2026-07-21") /// The inferred lifecycle string: "ga", "beta", or "preview" 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) diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs index 25a525d1d..66e03bd31 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs @@ -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() { diff --git a/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionLifecycleInferenceTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionLifecycleInferenceTests.cs new file mode 100644 index 000000000..12ce46f89 --- /dev/null +++ b/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionLifecycleInferenceTests.cs @@ -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); + } +}