diff --git a/.editorconfig b/.editorconfig index d20b3a1a3..08879dff7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -195,6 +195,7 @@ resharper_csharp_braces_for_foreach=required_for_multiline resharper_csharp_braces_for_for=required_for_multiline resharper_csharp_braces_for_fixed=required_for_multiline resharper_csharp_braces_for_ifelse=required_for_multiline +resharper_csharp_keep_existing_attribute_arrangement=true resharper_csharp_accessor_owner_body=expression_body @@ -233,6 +234,11 @@ dotnet_diagnostic.CA1859.severity = none dotnet_diagnostic.IDE0305.severity = none +# https://github.com/dotnet/roslyn/issues/60784 +# CS8509 already warns +dotnet_diagnostic.IDE0072.severity = none + + [DocumentationWebHost.cs] dotnet_diagnostic.IL3050.severity = none diff --git a/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs b/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs index 2313dac1e..e559dc5be 100644 --- a/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs +++ b/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs @@ -23,13 +23,17 @@ public static string Serialize(LinkIndex index) => public record LinkIndexEntry { - [JsonPropertyName("repository")] public required string Repository { get; init; } + [JsonPropertyName("repository")] + public required string Repository { get; init; } - [JsonPropertyName("path")] public required string Path { get; init; } + [JsonPropertyName("path")] + public required string Path { get; init; } - [JsonPropertyName("branch")] public required string Branch { get; init; } + [JsonPropertyName("branch")] + public required string Branch { get; init; } - [JsonPropertyName("etag")] public required string ETag { get; init; } + [JsonPropertyName("etag")] + public required string ETag { get; init; } } public interface ICrossLinkResolver @@ -38,9 +42,10 @@ public interface ICrossLinkResolver bool TryResolve(Action errorEmitter, Action warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri); } -public class CrossLinkResolver(CrossLinkFetcher fetcher) : ICrossLinkResolver +public class CrossLinkResolver(CrossLinkFetcher fetcher, IUriEnvironmentResolver? uriResolver = null) : ICrossLinkResolver { private FetchedCrossLinks _crossLinks = FetchedCrossLinks.Empty; + private readonly IUriEnvironmentResolver _uriResolver = uriResolver ?? new PreviewEnvironmentUriResolver(); public async Task FetchLinks() { @@ -49,9 +54,7 @@ public async Task FetchLinks() } public bool TryResolve(Action errorEmitter, Action warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) => - TryResolve(errorEmitter, warningEmitter, _crossLinks, crossLinkUri, out resolvedUri); - - private static Uri BaseUri { get; } = new("https://docs-v3-preview.elastic.dev"); + TryResolve(errorEmitter, warningEmitter, _crossLinks, _uriResolver, crossLinkUri, out resolvedUri); public FetchedCrossLinks UpdateLinkReference(string repository, LinkReference linkReference) { @@ -68,6 +71,7 @@ public static bool TryResolve( Action errorEmitter, Action warningEmitter, FetchedCrossLinks fetchedCrossLinks, + IUriEnvironmentResolver uriResolver, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri ) @@ -75,7 +79,7 @@ public static bool TryResolve( resolvedUri = null; var lookup = fetchedCrossLinks.LinkReferences; if (crossLinkUri.Scheme != "asciidocalypse" && lookup.TryGetValue(crossLinkUri.Scheme, out var linkReference)) - return TryFullyValidate(errorEmitter, linkReference, crossLinkUri, out resolvedUri); + return TryFullyValidate(errorEmitter, uriResolver, linkReference, crossLinkUri, out resolvedUri); // TODO this is temporary while we wait for all links.json to be published // Here we just silently rewrite the cross_link to the url @@ -95,13 +99,13 @@ public static bool TryResolve( if (!string.IsNullOrEmpty(crossLinkUri.Fragment)) path += crossLinkUri.Fragment; - var branch = GetBranch(crossLinkUri); - resolvedUri = new Uri(BaseUri, $"elastic/{crossLinkUri.Scheme}/tree/{branch}/{path}"); + resolvedUri = uriResolver.Resolve(crossLinkUri, path); return true; } private static bool TryFullyValidate( Action errorEmitter, + IUriEnvironmentResolver uriResolver, LinkReference linkReference, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri @@ -134,8 +138,7 @@ private static bool TryFullyValidate( path += "#" + lookupFragment.TrimStart('#'); } - var branch = GetBranch(crossLinkUri); - resolvedUri = new Uri(BaseUri, $"elastic/{crossLinkUri.Scheme}/tree/{branch}/{path}"); + resolvedUri = uriResolver.Resolve(crossLinkUri, path); return true; } @@ -224,19 +227,6 @@ private static bool ResolveLinkRedirect( return false; } - /// Hardcoding these for now, we'll have an index.json pointing to all links.json files - /// at some point from which we can query the branch soon. - private static string GetBranch(Uri crossLinkUri) - { - var branch = crossLinkUri.Scheme switch - { - "docs-content" => "main", - _ => "main" - }; - return branch; - } - - private static string ToTargetUrlPath(string lookupPath) { //https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/cloud-account/change-your-password diff --git a/src/Elastic.Markdown/CrossLinks/IUriEnvironmentResolver.cs b/src/Elastic.Markdown/CrossLinks/IUriEnvironmentResolver.cs new file mode 100644 index 000000000..f6fd1d228 --- /dev/null +++ b/src/Elastic.Markdown/CrossLinks/IUriEnvironmentResolver.cs @@ -0,0 +1,35 @@ +// 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 + +namespace Elastic.Markdown.CrossLinks; + +public interface IUriEnvironmentResolver +{ + Uri Resolve(Uri crossLinkUri, string path); +} + +public class PreviewEnvironmentUriResolver : IUriEnvironmentResolver +{ + private static Uri BaseUri { get; } = new("https://docs-v3-preview.elastic.dev"); + + public Uri Resolve(Uri crossLinkUri, string path) + { + var branch = GetBranch(crossLinkUri); + return new Uri(BaseUri, $"elastic/{crossLinkUri.Scheme}/tree/{branch}/{path}"); + } + + /// Hardcoding these for now, we'll have an index.json pointing to all links.json files + /// at some point from which we can query the branch soon. + private static string GetBranch(Uri crossLinkUri) + { + var branch = crossLinkUri.Scheme switch + { + "docs-content" => "main", + _ => "main" + }; + return branch; + } + + +} diff --git a/src/docs-assembler/Building/AssemblerBuilder.cs b/src/docs-assembler/Building/AssemblerBuilder.cs index e01c44532..e4f883604 100644 --- a/src/docs-assembler/Building/AssemblerBuilder.cs +++ b/src/docs-assembler/Building/AssemblerBuilder.cs @@ -14,9 +14,11 @@ public class AssemblerBuilder(ILoggerFactory logger, AssembleContext context) { private readonly ILogger _logger = logger.CreateLogger(); - public async Task BuildAllAsync(IReadOnlyCollection checkouts, Cancel ctx) + public async Task BuildAllAsync(IReadOnlyCollection checkouts, string environment, Cancel ctx) { - var crossLinkResolver = new CrossLinkResolver(new AssemblerCrossLinkFetcher(logger, context.Configuration)); + var crossLinkFetcher = new AssemblerCrossLinkFetcher(logger, context.Configuration); + var uriResolver = new PublishEnvironmentUriResolver(context.Configuration, environment); + var crossLinkResolver = new CrossLinkResolver(crossLinkFetcher, uriResolver); foreach (var checkout in checkouts) { diff --git a/src/docs-assembler/Building/PublishEnvironmentUriResolver.cs b/src/docs-assembler/Building/PublishEnvironmentUriResolver.cs new file mode 100644 index 000000000..4049973dc --- /dev/null +++ b/src/docs-assembler/Building/PublishEnvironmentUriResolver.cs @@ -0,0 +1,57 @@ +// 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 System.Collections.Frozen; +using Documentation.Assembler.Configuration; +using Elastic.Markdown.CrossLinks; + +namespace Documentation.Assembler.Building; + +public class PublishEnvironmentUriResolver : IUriEnvironmentResolver +{ + private Uri BaseUri { get; } + + private PublishEnvironment PublishEnvironment { get; } + + private PreviewEnvironmentUriResolver PreviewResolver { get; } + + private FrozenDictionary AllRepositories { get; } + + public PublishEnvironmentUriResolver(AssemblyConfiguration configuration, string environment) + { + if (!configuration.Environments.TryGetValue(environment, out var e)) + throw new Exception($"Could not find environment {environment}"); + if (!Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri)) + throw new Exception($"Could not parse uri {e.Uri} in environment {environment}"); + + BaseUri = uri; + PublishEnvironment = e; + PreviewResolver = new PreviewEnvironmentUriResolver(); + AllRepositories = configuration.ReferenceRepositories.Values.Concat([configuration.Narrative]) + .ToFrozenDictionary(e => e.Name, e => e); + RepositoryLookup = AllRepositories.GetAlternateLookup>(); + } + + private FrozenDictionary.AlternateLookup> RepositoryLookup { get; } + + public Uri Resolve(Uri crossLinkUri, string path) + { + if (PublishEnvironment.Name == "preview") + return PreviewResolver.Resolve(crossLinkUri, path); + + var repositoryPath = crossLinkUri.Scheme; + if (RepositoryLookup.TryGetValue(crossLinkUri.Scheme, out var repository)) + repositoryPath = repository.PathPrefix; + + var fullPath = (PublishEnvironment.PathPrefix, repositoryPath) switch + { + (null or "", null or "") => path, + (null or "", var p) => $"{p}/{path}", + (var p, null or "") => $"{p}/{path}", + var (p, pp) => $"{p}/{pp}/{path}" + }; + + return new Uri(BaseUri, fullPath); + } +} diff --git a/src/docs-assembler/Cli/RepositoryCommands.cs b/src/docs-assembler/Cli/RepositoryCommands.cs index 6fec9d5b1..172267ff5 100644 --- a/src/docs-assembler/Cli/RepositoryCommands.cs +++ b/src/docs-assembler/Cli/RepositoryCommands.cs @@ -48,15 +48,19 @@ public async Task CloneAll(bool? strict = null, Cancel ctx = default) /// Force a full rebuild of the destination folder /// Treat warnings as errors and fail the build on warnings /// Allow indexing and following of html files + /// The environment to resolve links to /// [Command("build-all")] public async Task BuildAll( bool? force = null, bool? strict = null, bool? allowIndexing = null, + string? environment = null, Cancel ctx = default) { AssignOutputLogger(); + var githubEnvironmentInput = githubActionsService.GetInput("environment"); + environment ??= !string.IsNullOrEmpty(githubEnvironmentInput) ? githubEnvironmentInput : "production"; await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); _ = collector.StartAsync(ctx); @@ -72,7 +76,7 @@ public async Task BuildAll( throw new Exception("No checkouts found"); var builder = new AssemblerBuilder(logger, assembleContext); - await builder.BuildAllAsync(checkouts, ctx); + await builder.BuildAllAsync(checkouts, environment, ctx); if (strict ?? false) return collector.Errors + collector.Warnings; diff --git a/src/docs-assembler/Configuration/AssemblyConfiguration.cs b/src/docs-assembler/Configuration/AssemblyConfiguration.cs index 11a037eca..315b79516 100644 --- a/src/docs-assembler/Configuration/AssemblyConfiguration.cs +++ b/src/docs-assembler/Configuration/AssemblyConfiguration.cs @@ -10,6 +10,7 @@ namespace Documentation.Assembler.Configuration; [YamlSerializable(typeof(AssemblyConfiguration))] [YamlSerializable(typeof(Repository))] [YamlSerializable(typeof(NarrativeRepository))] +[YamlSerializable(typeof(PublishEnvironment))] public partial class YamlStaticContext; public record AssemblyConfiguration @@ -30,6 +31,8 @@ public static AssemblyConfiguration Deserialize(string yaml) var repository = RepositoryDefaults(r, name); config.ReferenceRepositories[name] = repository; } + foreach (var (name, env) in config.Environments) + env.Name = name; config.Narrative = RepositoryDefaults(config.Narrative, NarrativeRepository.RepositoryName); return config; } @@ -71,4 +74,19 @@ private static TRepository RepositoryDefaults(TRepository r, string [YamlMember(Alias = "references")] public Dictionary ReferenceRepositories { get; set; } = []; + + [YamlMember(Alias = "environments")] + public Dictionary Environments { get; set; } = []; +} + +public record PublishEnvironment +{ + [YamlIgnore] + public string Name { get; set; } = string.Empty; + + [YamlMember(Alias = "uri")] + public string Uri { get; set; } = string.Empty; + + [YamlMember(Alias = "path_prefix")] + public string? PathPrefix { get; set; } = string.Empty; } diff --git a/src/docs-assembler/assembler.yml b/src/docs-assembler/assembler.yml index 607c65028..37f340d2c 100644 --- a/src/docs-assembler/assembler.yml +++ b/src/docs-assembler/assembler.yml @@ -1,3 +1,13 @@ +environments: + production: + uri: https://elastic.co + path_prefix: docs + staging: + uri: https://staging-website.elastic.co + path_prefix: docs + preview: + uri: https://docs-v3-preview.elastic.dev + path_prefix: narrative: checkout_strategy: full references: diff --git a/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs b/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs index bda2b497c..e1e9e326b 100644 --- a/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs +++ b/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs @@ -12,6 +12,7 @@ namespace Elastic.Markdown.Tests; public class TestCrossLinkResolver : ICrossLinkResolver { + private readonly IUriEnvironmentResolver _uriResolver = new PreviewEnvironmentUriResolver(); private FetchedCrossLinks _crossLinks = FetchedCrossLinks.Empty; private Dictionary LinkReferences { get; } = []; private HashSet DeclaredRepositories { get; } = []; @@ -56,5 +57,5 @@ public Task FetchLinks() } public bool TryResolve(Action errorEmitter, Action warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) => - CrossLinkResolver.TryResolve(errorEmitter, warningEmitter, _crossLinks, crossLinkUri, out resolvedUri); + CrossLinkResolver.TryResolve(errorEmitter, warningEmitter, _crossLinks, _uriResolver, crossLinkUri, out resolvedUri); } diff --git a/tests/authoring/Framework/TestCrossLinkResolver.fs b/tests/authoring/Framework/TestCrossLinkResolver.fs index 9598de485..9989cdcac 100644 --- a/tests/authoring/Framework/TestCrossLinkResolver.fs +++ b/tests/authoring/Framework/TestCrossLinkResolver.fs @@ -17,6 +17,7 @@ type TestCrossLinkResolver (config: ConfigurationFile) = let references = Dictionary() let declared = HashSet() + let uriResolver = PreviewEnvironmentUriResolver() member this.LinkReferences = references member this.DeclaredRepositories = declared @@ -77,6 +78,6 @@ type TestCrossLinkResolver (config: ConfigurationFile) = LinkReferences=this.LinkReferences.ToFrozenDictionary(), FromConfiguration=true ) - CrossLinkResolver.TryResolve(errorEmitter, warningEmitter, crossLinks, crossLinkUri, &resolvedUri); + CrossLinkResolver.TryResolve(errorEmitter, warningEmitter, crossLinks, uriResolver, crossLinkUri, &resolvedUri);