Skip to content
Open
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
10 changes: 10 additions & 0 deletions docs/syntax/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,16 @@ The CDN base URL is build configuration, not authored per page: it defaults to t

Bundles are fetched **once at build startup** for every declared product, not per directive. If a declared product's registry cannot be fetched the build fails; an individual bundle that is missing from the CDN is skipped with a warning. For the full design — including the manifest format and infrastructure — see [Changelog bundle registry and CDN delivery](/development/changelog-bundle-registry.md).

##### Unreleased-version visibility [unreleased-version-visibility]

Prestage products commit and upload their release bundles **before** release day, so the CDN can hold bundles for versions that are not yet released. CDN-mode rendering filters those out based on the build's content source:

- On **production** (the `current` content source), a bundle whose target version is newer than the product's current release in `versions.yml` is hidden (with a hint diagnostic).
- On **staging** (the `next` content source), staged bundles remain visible, enabling pre-release review.
- Local and isolated builds render everything.

The `versions.yml` bump on release day automatically makes staged bundles visible on production. Products without a semver versioning system (date-based targets such as `cloud-serverless`) are never filtered.

##### Declaring CDN-backed products [declaring-cdn-backed-products]

List each CDN-sourced product under `release_notes` in `docset.yml`. Every entry must reference a product ID from `products.yml` that participates in the release notes system:
Expand Down
7 changes: 7 additions & 0 deletions src/Elastic.Documentation.Configuration/BuildContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ public record BuildContext : IDocumentationSetContext, IDocumentationConfigurati

public BuildType BuildType { get; init; } = BuildType.Isolated;

/// <summary>
/// The content source this build publishes (assembler builds only): <see cref="Assembler.ContentSource.Current"/>
/// for production, <see cref="Assembler.ContentSource.Next"/> for staging. Null for isolated/local
/// builds, which have no publish target and render everything.
/// </summary>
public ContentSource? ContentSource { get; init; }

// This property is used to determine if the site should be indexed by search engines
public bool AllowIndexing { get; init; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ private void LoadCdnBundles(string product)

private void ApplyLoadedBundles(IReadOnlyList<LoadedBundle> loadedBundles)
{
var filteredBundles = FilterByVersion(loadedBundles);
var filteredBundles = FilterUnreleasedVersions(FilterByVersion(loadedBundles));

// Sort by version (descending - newest first)
// Supports both semver (e.g., "9.3.0") and date-based (e.g., "2025-08-05") versions
Expand All @@ -554,6 +554,43 @@ private void ApplyLoadedBundles(IReadOnlyList<LoadedBundle> loadedBundles)
}
}

/// <summary>
/// Prestage visibility filtering (release-notes onboarding RFC, B1): Prestage bundles are
/// uploaded to S3 weeks before release day, so CDN-mode rendering must not show bundles whose
/// target version the published content source has not released yet. Production publishes the
/// <see cref="ContentSource.Current"/> content source, where only versions at or below the
/// versioning system's current release render; staging publishes <see cref="ContentSource.Next"/>
/// and keeps them visible, enabling pre-release review. The versions.yml bump on release day
/// then makes staged bundles visible on production automatically. Local/isolated builds (no
/// content source) and date-based/versionless products are never filtered.
/// </summary>
private IReadOnlyList<LoadedBundle> FilterUnreleasedVersions(IReadOnlyList<LoadedBundle> bundles)
{
if (CdnProduct is null || Build.ContentSource != ContentSource.Current)
return bundles;

var versioningSystem = Build.ProductsConfiguration.Products.TryGetValue(CdnProduct, out var product)
? product.VersioningSystem
: null;
if (versioningSystem is null || versioningSystem.IsVersionless)
return bundles;

var visible = new List<LoadedBundle>(bundles.Count);
foreach (var bundle in bundles)
{
if (SemVersion.TryParse(bundle.Version, out var target) && target > versioningSystem.Current)
{
this.EmitHint(
$"Hiding changelog bundle '{CdnProduct} {bundle.Version}': it targets a version newer than the current release ({versioningSystem.Current}) and this build publishes the 'current' content source.");
continue;
}

visible.Add(bundle);
}

return visible;
}

/// <summary>Filters bundles by the optional <c>:version:</c> value; warns and renders empty when nothing matches.</summary>
private IReadOnlyList<LoadedBundle> FilterByVersion(IReadOnlyList<LoadedBundle> bundles)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ IReadOnlySet<Exporter> availableExporters
Id = env.Optimizely.Id
},
CanonicalBaseUrl = new Uri("https://www.elastic.co"), // Always use the production URL. In case a page is leaked to a search engine, it should point to the production site.
BuildType = BuildType.Assembler
BuildType = BuildType.Assembler,
ContentSource = env.ContentSource
};
BuildContext = buildContext;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// 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.Assembler;
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Documentation.Diagnostics;
using Elastic.Markdown.Myst.Directives.Changelog;

namespace Elastic.Markdown.Tests.Directives;

/// <summary>
/// Prestage visibility filtering (release-notes onboarding RFC, B1): CDN-mode <c>{changelog}</c>
/// hides bundles targeting versions newer than the versioning system's current release when the
/// build publishes the <c>current</c> content source (production), keeps them on <c>next</c>
/// (staging), and never filters local/isolated builds or products without a semver versioning
/// system. The test versions configuration pins stack current to 8.0.0.
/// </summary>
public abstract class ChangelogVersionVisibilityTestBase(ITestOutputHelper output) : DirectiveTest<ChangelogBlock>(output,
// language=markdown
"""
:::{changelog}
:cdn: elasticsearch
:::
""")
{
protected const string ReleasedTitle = "Released change";
protected const string UnreleasedTitle = "Unreleased staged change";

protected override IReleaseNotesResolver GetReleaseNotesResolver() =>
ChangelogCdnTestResolver.For("elasticsearch",
("7.9.0.yaml",
// language=yaml
"""
products:
- product: elasticsearch
target: 7.9.0
repo: elasticsearch
owner: elastic
entries:
- title: Released change
type: enhancement
products:
- product: elasticsearch
target: 7.9.0
prs:
- "100"
"""),
("8.1.0.yaml",
// language=yaml
"""
products:
- product: elasticsearch
target: 8.1.0
repo: elasticsearch
owner: elastic
entries:
- title: Unreleased staged change
type: enhancement
products:
- product: elasticsearch
target: 8.1.0
prs:
- "200"
"""));
}

public class ChangelogVisibilityOnProductionTests(ITestOutputHelper output) : ChangelogVersionVisibilityTestBase(output)
{
protected override ContentSource? GetContentSource() => ContentSource.Current;

[Fact]
public void HidesBundlesTargetingUnreleasedVersions()
{
Html.Should().Contain(ReleasedTitle);
Html.Should().NotContain(UnreleasedTitle, "8.1.0 is newer than the current release (8.0.0) and production publishes 'current'");
}

[Fact]
public void EmitsHintForHiddenBundle() =>
Collector.Diagnostics.Should().Contain(d =>
d.Severity == Severity.Hint && d.Message.Contains("elasticsearch 8.1.0"));
}

public class ChangelogVisibilityOnStagingTests(ITestOutputHelper output) : ChangelogVersionVisibilityTestBase(output)
{
protected override ContentSource? GetContentSource() => ContentSource.Next;

[Fact]
public void ShowsUnreleasedBundlesForPreReleaseReview()
{
Html.Should().Contain(ReleasedTitle);
Html.Should().Contain(UnreleasedTitle, "staging publishes 'next' so staged bundles are reviewable before release day");
}
}

public class ChangelogVisibilityOnIsolatedBuildTests(ITestOutputHelper output) : ChangelogVersionVisibilityTestBase(output)
{
// No override: isolated/local builds have no content source and render everything.

[Fact]
public void ShowsAllBundles()
{
Html.Should().Contain(ReleasedTitle);
Html.Should().Contain(UnreleasedTitle);
}
}

/// <summary>
/// Products without a registered semver versioning system (date-promotion products, products not in
/// products.yml) are never filtered — their targets are dates or unknown schemes, not stack versions.
/// </summary>
public class ChangelogVisibilityUnversionedProductTests(ITestOutputHelper output) : DirectiveTest<ChangelogBlock>(output,
// language=markdown
"""
:::{changelog}
:cdn: cdn-visibility-unversioned
:::
""")
{
protected override ContentSource? GetContentSource() => ContentSource.Current;

protected override IReleaseNotesResolver GetReleaseNotesResolver() =>
ChangelogCdnTestResolver.For("cdn-visibility-unversioned",
("9.9.0.yaml",
// language=yaml
"""
products:
- product: cdn-visibility-unversioned
target: 9.9.0
repo: widget
owner: elastic
entries:
- title: Future-looking change
type: enhancement
products:
- product: cdn-visibility-unversioned
target: 9.9.0
prs:
- "300"
"""));

[Fact]
public void DoesNotFilterProductsWithoutVersioningSystem() =>
Html.Should().Contain("Future-looking change");
}
13 changes: 12 additions & 1 deletion tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using AwesomeAssertions;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
using Elastic.Documentation.Configuration.ReleaseNotes;
using Elastic.Markdown.IO;
using Elastic.Markdown.Myst.Directives;
Expand Down Expand Up @@ -75,7 +76,11 @@ protected DirectiveTest(ITestOutputHelper output, [LanguageInjection("markdown")
var configurationContext = TestHelpers.CreateConfigurationContext(FileSystem);
// ReSharper disable once VirtualMemberCallInConstructor
var environment = GetEnvironment();
var context = new BuildContext(Collector, FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem), configurationContext, environment);
// ReSharper disable once VirtualMemberCallInConstructor
var context = new BuildContext(Collector, FileSystemFactory.ScopeCurrentWorkingDirectory(FileSystem), configurationContext, environment)
{
ContentSource = GetContentSource()
};
var linkResolver = new TestCrossLinkResolver();
// ReSharper disable once VirtualMemberCallInConstructor
Set = new DocumentationSet(context, logger, linkResolver, GetReleaseNotesResolver());
Expand Down Expand Up @@ -103,6 +108,12 @@ protected virtual void AddToFileSystem(MockFileSystem fileSystem) { }
/// <summary>Override to inject a deterministic environment for env-dependent config (e.g. <c>storybook.registry</c>).</summary>
protected virtual IEnvironmentVariables? GetEnvironment() => null;

/// <summary>
/// Override to simulate an assembler build publishing a specific content source
/// (<c>current</c> = production, <c>next</c> = staging). Null (default) mimics isolated builds.
/// </summary>
protected virtual ContentSource? GetContentSource() => null;

public virtual async ValueTask InitializeAsync()
{
_ = Collector.StartAsync(TestContext.Current.CancellationToken);
Expand Down
Loading