From 11856a23a992640699dadc97431c18a12ababfb0 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 21 May 2025 19:37:27 +0200 Subject: [PATCH] Add an action that validates if `assembler.yml` content-source mapping is fully published to the link registry --- actions/assembler-config-validate/action.yml | 12 +++++ docs-builder.sln | 6 +++ .../Links/CrossLinks/CrossLinkFetcher.cs | 9 ++-- .../Building/AssemblerCrossLinkFetcher.cs | 3 +- .../Cli/ContentSourceCommands.cs | 50 +++++++++++++++++++ 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 actions/assembler-config-validate/action.yml diff --git a/actions/assembler-config-validate/action.yml b/actions/assembler-config-validate/action.yml new file mode 100644 index 000000000..deeed952f --- /dev/null +++ b/actions/assembler-config-validate/action.yml @@ -0,0 +1,12 @@ +name: 'Documentation Assembler Configuration Validator' +description: 'Ensures the assembler configuration is valid' + +branding: + icon: 'filter' + color: 'blue' + +runs: + using: 'docker' + image: "docker://ghcr.io/elastic/docs-assembler:edge" + env: + INPUT_COMMAND: "content-source validate" diff --git a/docs-builder.sln b/docs-builder.sln index dc53d6c91..15fada31a 100644 --- a/docs-builder.sln +++ b/docs-builder.sln @@ -94,6 +94,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assembler-match", "assemble actions\assembler-match\action.yml = actions\assembler-match\action.yml EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assembler-config-validate", "assembler-config-validate", "{E20FEEF9-1D1A-4CDA-A546-7FDC573BE399}" + ProjectSection(SolutionItems) = preProject + actions\assembler-config-validate\action.yml = actions\assembler-config-validate\action.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -178,5 +183,6 @@ Global {059E787F-85C1-43BE-9DD6-CE319E106383} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A} {7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166} = {059E787F-85C1-43BE-9DD6-CE319E106383} {FB1C1954-D8E2-4745-BA62-04DD82FB4792} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7} + {E20FEEF9-1D1A-4CDA-A546-7FDC573BE399} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7} EndGlobalSection EndGlobal diff --git a/src/Elastic.Markdown/Links/CrossLinks/CrossLinkFetcher.cs b/src/Elastic.Markdown/Links/CrossLinks/CrossLinkFetcher.cs index f5ee11c51..e914b49f8 100644 --- a/src/Elastic.Markdown/Links/CrossLinks/CrossLinkFetcher.cs +++ b/src/Elastic.Markdown/Links/CrossLinks/CrossLinkFetcher.cs @@ -33,6 +33,7 @@ public record FetchedCrossLinks public abstract class CrossLinkFetcher(ILoggerFactory logger) : IDisposable { + public const string RegistryUrl = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json"; private readonly ILogger _logger = logger.CreateLogger(nameof(CrossLinkFetcher)); private readonly HttpClient _client = new(); private LinkReferenceRegistry? _linkIndex; @@ -42,16 +43,16 @@ public static LinkReference Deserialize(string json) => public abstract Task Fetch(Cancel ctx); - protected async Task FetchLinkIndex(Cancel ctx) + public async Task FetchLinkIndex(Cancel ctx) { if (_linkIndex is not null) { _logger.LogTrace("Using cached link index"); return _linkIndex; } - var url = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json"; - _logger.LogInformation("Fetching {Url}", url); - var json = await _client.GetStringAsync(url, ctx); + + _logger.LogInformation("Fetching {Url}", RegistryUrl); + var json = await _client.GetStringAsync(RegistryUrl, ctx); _linkIndex = LinkReferenceRegistry.Deserialize(json); return _linkIndex; } diff --git a/src/tooling/docs-assembler/Building/AssemblerCrossLinkFetcher.cs b/src/tooling/docs-assembler/Building/AssemblerCrossLinkFetcher.cs index b8a1661a8..b590e8444 100644 --- a/src/tooling/docs-assembler/Building/AssemblerCrossLinkFetcher.cs +++ b/src/tooling/docs-assembler/Building/AssemblerCrossLinkFetcher.cs @@ -11,7 +11,8 @@ namespace Documentation.Assembler.Building; -public class AssemblerCrossLinkFetcher(ILoggerFactory logger, AssemblyConfiguration configuration, PublishEnvironment publishEnvironment) : CrossLinkFetcher(logger) +public class AssemblerCrossLinkFetcher(ILoggerFactory logger, AssemblyConfiguration configuration, PublishEnvironment publishEnvironment) + : CrossLinkFetcher(logger) { public override async Task Fetch(Cancel ctx) { diff --git a/src/tooling/docs-assembler/Cli/ContentSourceCommands.cs b/src/tooling/docs-assembler/Cli/ContentSourceCommands.cs index 8a5d04977..54c27339e 100644 --- a/src/tooling/docs-assembler/Cli/ContentSourceCommands.cs +++ b/src/tooling/docs-assembler/Cli/ContentSourceCommands.cs @@ -6,8 +6,10 @@ using System.IO.Abstractions; using Actions.Core.Services; using ConsoleAppFramework; +using Documentation.Assembler.Building; using Elastic.Documentation.Configuration.Assembler; using Elastic.Documentation.Tooling.Diagnostics.Console; +using Elastic.Markdown.Links.CrossLinks; using Microsoft.Extensions.Logging; namespace Documentation.Assembler.Cli; @@ -22,6 +24,54 @@ private void AssignOutputLogger() ConsoleApp.LogError = msg => log.LogError(msg); } + [Command("validate")] + public async Task Validate(Cancel ctx = default) + { + AssignOutputLogger(); + await using var collector = new ConsoleDiagnosticsCollector(logFactory, githubActionsService) + { + NoHints = true + }; + + _ = collector.StartAsync(ctx); + + // environment does not matter to check the configuration, defaulting to dev + var context = new AssembleContext("dev", collector, new FileSystem(), new FileSystem(), null, null) + { + Force = false, + AllowIndexing = false + }; + var fetcher = new AssemblerCrossLinkFetcher(logFactory, context.Configuration, context.Environment); + var links = await fetcher.FetchLinkIndex(ctx); + var repositories = context.Configuration.ReferenceRepositories.Values.Concat([context.Configuration.Narrative]).ToList(); + + foreach (var repository in repositories) + { + if (!links.Repositories.TryGetValue(repository.Name, out var registryMapping)) + { + collector.EmitError(context.ConfigurationPath, $"'{repository}' does not exist in {CrossLinkFetcher.RegistryUrl}"); + continue; + } + + var current = repository.GetBranch(ContentSource.Current); + var next = repository.GetBranch(ContentSource.Next); + if (!registryMapping.TryGetValue(next, out _)) + { + collector.EmitError(context.ConfigurationPath, + $"'{repository.Name}' has not yet published links.json for configured 'next' content source: '{next}' see {CrossLinkFetcher.RegistryUrl}"); + } + if (!registryMapping.TryGetValue(current, out _)) + { + collector.EmitError(context.ConfigurationPath, + $"'{repository.Name}' has not yet published links.json for configured 'current' content source: '{current}' see {CrossLinkFetcher.RegistryUrl}"); + } + } + + + await collector.StopAsync(ctx); + return collector.Errors == 0 ? 0 : 1; + } + /// /// ///