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
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ namespace Elastic.Documentation.ReleaseNotes;
/// <summary>
/// Shared matching rule for the <c>changelog</c> directive's <c>:version:</c> filter. A bundle
/// matches a requested version when the requested value equals either the bundle's declared target
/// or its file name (with or without extension), compared case-insensitively. Kept in one place so
/// the directive's post-load filter and the CDN fetcher's download-time filter agree.
/// or its file name (with or without extension), compared case-insensitively and ignoring a leading
/// <c>v</c> on either side (release tags are <c>v9.5.0</c>; generated version pages are <c>9.5.0</c>).
/// Kept in one place so the directive's post-load filter and the CDN fetcher's download-time filter agree.
/// </summary>
public static class ChangelogVersionMatch
{
Expand All @@ -21,16 +22,18 @@ public static bool Matches(string requested, string? target, string? file)
if (string.IsNullOrWhiteSpace(requested))
return true;

var value = requested.Trim();
var value = StripLeadingV(requested.Trim());

if (!string.IsNullOrWhiteSpace(target) && string.Equals(target.Trim(), value, StringComparison.OrdinalIgnoreCase))
if (!string.IsNullOrWhiteSpace(target) && string.Equals(StripLeadingV(target.Trim()), value, StringComparison.OrdinalIgnoreCase))
return true;

if (string.IsNullOrWhiteSpace(file))
return false;

var name = Path.GetFileName(file);
return string.Equals(name, value, StringComparison.OrdinalIgnoreCase)
|| string.Equals(Path.GetFileNameWithoutExtension(file), value, StringComparison.OrdinalIgnoreCase);
return string.Equals(StripLeadingV(name), value, StringComparison.OrdinalIgnoreCase)
|| string.Equals(StripLeadingV(Path.GetFileNameWithoutExtension(file)), value, StringComparison.OrdinalIgnoreCase);
}

private static string StripLeadingV(string version) => version.TrimStart('v', 'V');
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Elastic.Documentation.Diagnostics;
using Elastic.Documentation.FileSystems;
using Elastic.Documentation.Services;
using Elastic.Documentation.Versions;
using Microsoft.Extensions.Logging;

namespace Elastic.Documentation.Isolated;
Expand Down Expand Up @@ -53,7 +54,10 @@ IEnvironmentVariables environmentVariables
{
private readonly ILogger _logger = logFactory.CreateLogger<UnifiedReleaseNotesBuildService>();

private sealed record ProductBundles(Product Product, IReadOnlyList<(string FileName, string Content)> Files);
private sealed record ProductBundles(ProductVersions Versions, IReadOnlyList<(string FileName, string Content)> Files)
{
public Product Product => Versions.Product;
}

public async Task<bool> Build(IDiagnosticsCollector collector, UnifiedReleaseNotesBuildOptions options, Cancel ctx = default)
{
Expand All @@ -78,7 +82,7 @@ public async Task<bool> Build(IDiagnosticsCollector collector, UnifiedReleaseNot
products = [.. bundles.Select(b => b.Product)];

var docsRoot = Path.Join(Paths.WorkingDirectoryRoot.FullName, "unified-release-notes", "docs");
var mockFs = CreateVirtualDocset(docsRoot, products, bundles);
var mockFs = CreateVirtualDocset(docsRoot, bundles);
PrepareOutput(realFs, options.Output.FullName);

var docFs = DocumentationFileSystem.Resolve(
Expand Down Expand Up @@ -155,7 +159,9 @@ UnifiedReleaseNotesBuildOptions options
.Order(StringComparer.Ordinal)
.Select(file => (FileName: realFs.Path.GetFileName(file), Content: realFs.File.ReadAllText(file)))
.ToArray();
result.Add(new ProductBundles(product, files));
result.Add(
new ProductBundles(new ProductVersions(product, NewestFirst(loaded.Select(b => NormalizeVersion(b.Version)))), files)
Comment thread
infra-vault-gh-plugin-prod[bot] marked this conversation as resolved.
);
}

return result;
Expand All @@ -169,22 +175,21 @@ private static IReadOnlyList<string> MissingVersions(IEnumerable<string> bundleV

private static string NormalizeVersion(string version) => version.TrimStart('v');

private static MockFileSystem CreateVirtualDocset(
string docsRoot,
IReadOnlyList<Product> products,
IReadOnlyList<ProductBundles> bundles
)
private static IReadOnlyList<string> NewestFirst(IEnumerable<string> versions) =>
[.. versions.Distinct(StringComparer.OrdinalIgnoreCase).OrderByDescending(VersionOrDate.Parse)];

private static MockFileSystem CreateVirtualDocset(string docsRoot, IReadOnlyList<ProductBundles> bundles)
{
var mockFs = new MockFileSystem(new MockFileSystemOptions { CurrentDirectory = docsRoot, CreateDefaultTempDir = false });

foreach (var (relativePath, content) in UnifiedReleaseNotesDocset.Generate(products))
foreach (var (relativePath, content) in UnifiedReleaseNotesDocset.Generate([.. bundles.Select(b => b.Versions)]))
mockFs.AddFile(mockFs.Path.Join(docsRoot, relativePath), new MockFileData(content));

foreach (var (product, files) in bundles)
foreach (var (productVersions, files) in bundles)
{
foreach (var (fileName, content) in files)
{
var path = mockFs.Path.Join(docsRoot, UnifiedReleaseNotesDocset.BundlesPath(product.Id), fileName);
var path = mockFs.Path.Join(docsRoot, UnifiedReleaseNotesDocset.BundlesPath(productVersions.Product.Id), fileName);
mockFs.AddFile(path, new MockFileData(content));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,25 @@

namespace Elastic.Documentation.Isolated;

/// <summary>A product and the versions it has bundles for, newest first.</summary>
public sealed record ProductVersions(Product Product, IReadOnlyList<string> Versions);

/// <summary>
/// Synthesizes the markdown and navigation files of the unified release notes docset in memory.
/// Every product with <c>release-notes: dra</c> gets the same four pages, so onboarding a product
/// is a one-line change in <c>products.yml</c>.
/// Every product with <c>release-notes: dra</c> gets the same pages, so onboarding a product is a
/// one-line change in <c>products.yml</c>. A version has a page of its own, so its URL can be shared.
/// </summary>
public static class UnifiedReleaseNotesDocset
{
public const string BundlesFolder = "changelog/bundles";

private const string UnreleasedNotice =
":::{warning}\nThis is a preview of an unreleased version. The release notes can change until the version ships.\n:::";

private const string VersionOptions = ":highlights:\n:subsections:";

private static readonly (string FileName, string NavigationTitle, string Heading, string Options)[] Pages =
[
("index.md", "Release notes", "release notes", ":highlights:"),
("known-issues.md", "Known issues", "known issues", ":type: known-issue"),
("breaking-changes.md", "Breaking changes", "breaking changes", ":type: breaking-change\n:dropdowns:"),
("deprecations.md", "Deprecations", "deprecations", ":type: deprecation")
Expand All @@ -32,35 +39,42 @@ public static IReadOnlyList<Product> SelectProducts(IEnumerable<Product> product
public static string BundlesPath(string productId) => $"{BundlesFolder}/{productId}";

/// <summary>Generates every file of the docset, keyed by its path relative to the docset root.</summary>
public static IReadOnlyDictionary<string, string> Generate(IReadOnlyList<Product> products)
public static IReadOnlyDictionary<string, string> Generate(IReadOnlyList<ProductVersions> products)
{
var files = new Dictionary<string, string>(StringComparer.Ordinal)
{
["docset.yml"] = "project: unified-release-notes\n" + GenerateToc(products),
["release-notes/index.md"] = GenerateRootIndex(products)
};

foreach (var product in products)
foreach (var (product, versions) in products)
{
var folder = $"release-notes/{product.Id}";
files[$"{folder}/toc.yml"] = GenerateProductToc();
files[$"{folder}/toc.yml"] = GenerateProductToc(versions);
files[$"{folder}/index.md"] = GenerateProductIndex(product, versions);
foreach (var version in versions)
files[$"{folder}/{version}.md"] = GenerateVersionPage(product, version);
foreach (var page in Pages)
files[$"{folder}/{page.FileName}"] = GeneratePage(product, page);
}

return files;
}

private static string GenerateToc(IReadOnlyList<Product> products)
private static string GenerateToc(IReadOnlyList<ProductVersions> products)
{
var lines = new List<string> { "toc:", " - folder: release-notes", " children:", " - file: index.md" };
lines.AddRange(products.Select(p => $" - toc: {p.Id}"));
lines.AddRange(products.Select(p => $" - toc: {p.Product.Id}"));
return string.Join('\n', lines) + "\n";
}

private static string GenerateProductToc() => string.Join('\n', Pages.Select(p => $" - file: {p.FileName}").Prepend("toc:")) + "\n";
private static string GenerateProductToc(IReadOnlyList<string> versions)
{
IEnumerable<string> files = ["index.md", .. versions.Select(v => $"{v}.md"), .. Pages.Select(p => p.FileName)];
return string.Join('\n', files.Select(f => $" - file: {f}").Prepend("toc:")) + "\n";
}

private static string GenerateRootIndex(IReadOnlyList<Product> products)
private static string GenerateRootIndex(IReadOnlyList<ProductVersions> products)
{
var lines = new List<string>
{
Expand All @@ -74,13 +88,51 @@ private static string GenerateRootIndex(IReadOnlyList<Product> products)
"",
"A scheduled workflow rebuilds it whenever a new build candidate is cut or a SNAPSHOT advances.",
"",
UnreleasedNotice,
"",
"## Products",
""
};
lines.AddRange(products.Select(p => $"- [{p.DisplayName}]({p.Id}/index.md)"));
lines.AddRange(products.Select(p => $"- [{p.Product.DisplayName}]({p.Product.Id}/index.md)"));
return string.Join('\n', lines) + "\n";
}

private static string GenerateProductIndex(Product product, IReadOnlyList<string> versions) =>
string.Join(
'\n',
new[]
{
"---",
$"navigation_title: {product.DisplayName}",
"---",
"",
$"# {product.DisplayName} release notes",
"",
UnreleasedNotice,
"",
"## Versions",
""
}.Concat(versions.Select(v => $"- [{v}]({v}.md)")).Append("")
);

private static string GenerateVersionPage(Product product, string version) =>
string.Join(
'\n',
"---",
$"navigation_title: {version}",
"---",
"",
$"# {product.DisplayName} {version}",
"",
UnreleasedNotice,
"",
":::{changelog} /" + BundlesPath(product.Id),
$":version: {version}",
VersionOptions,
":::",
""
);

private static string GeneratePage(Product product, (string FileName, string NavigationTitle, string Heading, string Options) page) =>
string.Join(
'\n',
Expand All @@ -90,6 +142,8 @@ private static string GeneratePage(Product product, (string FileName, string Nav
"",
$"# {product.DisplayName} {page.Heading}",
"",
UnreleasedNotice,
"",
":::{changelog} /" + BundlesPath(product.Id),
page.Options,
":::",
Expand Down
Loading
Loading