Skip to content
5 changes: 5 additions & 0 deletions .github/workflows/smoke-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,8 @@ jobs:

- name: Verify landing-page-path output
run: test ${{ steps.docs-build.outputs.landing-page-path }} == ${{ matrix.landing-page-path-output }}

- name: Verify link validation
run: |
dotnet run --project src/tooling/docs-builder -- inbound-links validate-link-reference -p test-repo

29 changes: 2 additions & 27 deletions src/Elastic.Documentation.Configuration/BuildContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ public BuildContext(
? ReadFileSystem.DirectoryInfo.New(source)
: ReadFileSystem.DirectoryInfo.New(Path.Combine(Paths.WorkingDirectoryRoot.FullName));

(DocumentationSourceDirectory, ConfigurationPath) = FindDocsFolderFromRoot(rootFolder);
(DocumentationSourceDirectory, ConfigurationPath) = Paths.FindDocsFolderFromRoot(ReadFileSystem, rootFolder);

DocumentationCheckoutDirectory = Paths.DetermineSourceDirectoryRoot(DocumentationSourceDirectory);

DocumentationOutputDirectory = !string.IsNullOrWhiteSpace(output)
? WriteFileSystem.DirectoryInfo.New(output)
: WriteFileSystem.DirectoryInfo.New(Path.Combine(Paths.WorkingDirectoryRoot.FullName, Path.Combine(".artifacts", "docs", "html")));
: WriteFileSystem.DirectoryInfo.New(Path.Combine(rootFolder.FullName, Path.Combine(".artifacts", "docs", "html")));

if (ConfigurationPath.FullName != DocumentationSourceDirectory.FullName)
DocumentationSourceDirectory = ConfigurationPath.Directory!;
Expand All @@ -96,29 +96,4 @@ public BuildContext(
};
}

private (IDirectoryInfo, IFileInfo) FindDocsFolderFromRoot(IDirectoryInfo rootPath)
{
string[] files = ["docset.yml", "_docset.yml"];
string[] knownFolders = [rootPath.FullName, Path.Combine(rootPath.FullName, "docs")];
var mostLikelyTargets =
from file in files
from folder in knownFolders
select Path.Combine(folder, file);

var knownConfigPath = mostLikelyTargets.FirstOrDefault(ReadFileSystem.File.Exists);
var configurationPath = knownConfigPath is null ? null : ReadFileSystem.FileInfo.New(knownConfigPath);
if (configurationPath is not null)
return (configurationPath.Directory!, configurationPath);

configurationPath = rootPath
.EnumerateFiles("*docset.yml", SearchOption.AllDirectories)
.FirstOrDefault()
?? throw new Exception($"Can not locate docset.yml file in '{rootPath}'");

var docsFolder = configurationPath.Directory
?? throw new Exception($"Can not locate docset.yml file in '{rootPath}'");

return (docsFolder, configurationPath);
}

}
44 changes: 44 additions & 0 deletions src/Elastic.Documentation.Configuration/Paths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;

namespace Elastic.Documentation.Configuration;
Expand Down Expand Up @@ -50,4 +51,47 @@ private static DirectoryInfo GetApplicationFolder()
return new DirectoryInfo(elasticPath);
}

public static (IDirectoryInfo, IFileInfo) FindDocsFolderFromRoot(IFileSystem readFileSystem, IDirectoryInfo rootPath)
{
string[] files = ["docset.yml", "_docset.yml"];
string[] knownFolders = [rootPath.FullName, Path.Combine(rootPath.FullName, "docs")];
var mostLikelyTargets =
from file in files
from folder in knownFolders
select Path.Combine(folder, file);

var knownConfigPath = mostLikelyTargets.FirstOrDefault(readFileSystem.File.Exists);
var configurationPath = knownConfigPath is null ? null : readFileSystem.FileInfo.New(knownConfigPath);
if (configurationPath is not null)
return (configurationPath.Directory!, configurationPath);

configurationPath = rootPath
.EnumerateFiles("*docset.yml", SearchOption.AllDirectories)
.FirstOrDefault()
?? throw new Exception($"Can not locate docset.yml file in '{rootPath}'");

var docsFolder = configurationPath.Directory ?? throw new Exception($"Can not locate docset.yml file in '{rootPath}'");

return (docsFolder, configurationPath);
}

public static bool TryFindDocsFolderFromRoot(
IFileSystem readFileSystem,
IDirectoryInfo rootPath,
[NotNullWhen(true)] out IDirectoryInfo? docDirectory,
[NotNullWhen(true)] out IFileInfo? configurationPath
)
{
docDirectory = null;
configurationPath = null;
try
{
(docDirectory, configurationPath) = FindDocsFolderFromRoot(readFileSystem, rootPath);
return true;
}
catch
{
return false;
}
}
}
46 changes: 26 additions & 20 deletions src/Elastic.Documentation/GitCheckoutInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

using System.IO.Abstractions;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using SoftCircuits.IniFileParser;

namespace Elastic.Documentation;

public record GitCheckoutInformation
public partial record GitCheckoutInformation
{
public static GitCheckoutInformation Unavailable { get; } = new()
{
Expand Down Expand Up @@ -74,31 +75,33 @@ public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem
using var streamReader = new StreamReader(stream);
ini.Load(streamReader);

var remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY");
var remote = BranchTrackingRemote(branch, ini);
logger?.LogInformation("Remote from branch: {GitRemote}", remote);
if (string.IsNullOrEmpty(remote))
{
remote = BranchTrackingRemote(branch, ini);
logger?.LogInformation("Remote from branch: {GitRemote}", remote);
if (string.IsNullOrEmpty(remote))
{
remote = BranchTrackingRemote("main", ini);
logger?.LogInformation("Remote from main branch: {GitRemote}", remote);
}
remote = BranchTrackingRemote("main", ini);
logger?.LogInformation("Remote from main branch: {GitRemote}", remote);
}

if (string.IsNullOrEmpty(remote))
{
remote = BranchTrackingRemote("master", ini);
logger?.LogInformation("Remote from master branch: {GitRemote}", remote);
}
if (string.IsNullOrEmpty(remote))
{
remote = BranchTrackingRemote("master", ini);
logger?.LogInformation("Remote from master branch: {GitRemote}", remote);
}

if (string.IsNullOrEmpty(remote))
{
remote = "elastic/docs-builder-unknown";
logger?.LogInformation("Remote from fallback: {GitRemote}", remote);
}
remote = remote.AsSpan().TrimEnd("git").TrimEnd('.').ToString();
if (string.IsNullOrEmpty(remote))
{
remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY");
logger?.LogInformation("Remote from GITHUB_REPOSITORY: {GitRemote}", remote);
}

if (string.IsNullOrEmpty(remote))
{
remote = "elastic/docs-builder-unknown";
logger?.LogInformation("Remote from fallback: {GitRemote}", remote);
}
remote = CutOffGitExtension().Replace(remote, string.Empty);

var info = new GitCheckoutInformation
{
Ref = gitRef,
Expand Down Expand Up @@ -137,4 +140,7 @@ string BranchTrackingRemote(string b, IniFile c)
return remote ?? string.Empty;
}
}

[GeneratedRegex(@"\.git$", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex CutOffGitExtension();
}
31 changes: 24 additions & 7 deletions src/tooling/docs-assembler/Cli/InboundLinkCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ namespace Documentation.Assembler.Cli;
internal sealed class InboundLinkCommands(ILoggerFactory logger, ICoreService githubActionsService)
{
private readonly LinkIndexLinkChecker _linkIndexLinkChecker = new(logger);
private readonly ILogger<Program> _log = logger.CreateLogger<Program>();

[SuppressMessage("Usage", "CA2254:Template should be a static expression")]
private void AssignOutputLogger()
{
var log = logger.CreateLogger<Program>();
ConsoleApp.Log = msg => log.LogInformation(msg);
ConsoleApp.LogError = msg => log.LogError(msg);
ConsoleApp.Log = msg => _log.LogInformation(msg);
ConsoleApp.LogError = msg => _log.LogError(msg);
}

/// <summary> Validate all published cross_links in all published links.json files. </summary>
Expand Down Expand Up @@ -64,19 +64,36 @@ public async Task<int> ValidateRepoInboundLinks(string? from = null, string? to
/// Validate a locally published links.json file against all published links.json files in the registry
/// </summary>
/// <param name="file">Path to `links.json` defaults to '.artifacts/docs/html/links.json'</param>
/// <param name="path"> -p, Defaults to the `{pwd}` folder</param>
/// <param name="ctx"></param>
[Command("validate-link-reference")]
public async Task<int> ValidateLocalLinkReference([Argument] string? file = null, Cancel ctx = default)
public async Task<int> ValidateLocalLinkReference(string? file = null, string? path = null, Cancel ctx = default)
{
AssignOutputLogger();
file ??= ".artifacts/docs/html/links.json";
var fs = new FileSystem();
var root = fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
var repository = GitCheckoutInformation.Create(root, new FileSystem(), logger.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName
var root = !string.IsNullOrEmpty(path) ? fs.DirectoryInfo.New(path) : fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
var repository = GitCheckoutInformation.Create(root, fs, logger.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName
?? throw new Exception("Unable to determine repository name");

var resolvedFile = fs.FileInfo.New(Path.Combine(root.FullName, file));

var runningOnCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"));
if (runningOnCi && !resolvedFile.Exists)
{
_log.LogInformation("Running on CI after a build that produced no {File}, skipping the validation", resolvedFile.FullName);
return 0;
}
if (runningOnCi && !Paths.TryFindDocsFolderFromRoot(fs, root, out _, out _))
{
_log.LogInformation("Running on CI, {Directory} has no documentation, skipping the validation", root.FullName);
return 0;
}

_log.LogInformation("Validating {File} in {Directory}", file, root.FullName);

await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService).StartAsync(ctx);
await _linkIndexLinkChecker.CheckWithLocalLinksJson(collector, repository, file, ctx);
await _linkIndexLinkChecker.CheckWithLocalLinksJson(collector, repository, resolvedFile.FullName, ctx);
await collector.StopAsync(ctx);
return collector.Errors;
}
Expand Down
4 changes: 2 additions & 2 deletions src/tooling/docs-builder/Cli/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ public async Task<int> Generate(
CanonicalBaseUrl = canonicalBaseUri
};
}
// On CI, we are running on merge commit which may have changes against an older
// docs folder (this can happen on out of date PR's).
// On CI, we are running on a merge commit which may have changes against an older
// docs folder (this can happen on out-of-date PR's).
// At some point in the future we can remove this try catch
catch (Exception e) when (runningOnCi && e.Message.StartsWith("Can not locate docset.yml file in"))
{
Expand Down
29 changes: 23 additions & 6 deletions src/tooling/docs-builder/Cli/InboundLinkCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ namespace Documentation.Builder.Cli;
internal sealed class InboundLinkCommands(ILoggerFactory logger, ICoreService githubActionsService)
{
private readonly LinkIndexLinkChecker _linkIndexLinkChecker = new(logger);
private readonly ILogger<Program> _log = logger.CreateLogger<Program>();

[SuppressMessage("Usage", "CA2254:Template should be a static expression")]
private void AssignOutputLogger()
{
var log = logger.CreateLogger<Program>();
ConsoleApp.Log = msg => log.LogInformation(msg);
ConsoleApp.LogError = msg => log.LogError(msg);
ConsoleApp.Log = msg => _log.LogInformation(msg);
ConsoleApp.LogError = msg => _log.LogError(msg);
}

/// <summary> Validate all published cross_links in all published links.json files. </summary>
Expand Down Expand Up @@ -69,21 +69,38 @@ public async Task<int> ValidateRepoInboundLinks(string? from = null, string? to
/// Validate a locally published links.json file against all published links.json files in the registry
/// </summary>
/// <param name="file">Path to `links.json` defaults to '.artifacts/docs/html/links.json'</param>
/// <param name="path"> -p, Defaults to the `{pwd}` folder</param>
/// <param name="ctx"></param>
[Command("validate-link-reference")]
[ConsoleAppFilter<StopwatchFilter>]
[ConsoleAppFilter<CatchExceptionFilter>]
public async Task<int> ValidateLocalLinkReference([Argument] string? file = null, Cancel ctx = default)
public async Task<int> ValidateLocalLinkReference(string? file = null, string? path = null, Cancel ctx = default)
{
AssignOutputLogger();
file ??= ".artifacts/docs/html/links.json";
var fs = new FileSystem();
var root = fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
var root = !string.IsNullOrEmpty(path) ? fs.DirectoryInfo.New(path) : fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
var repository = GitCheckoutInformation.Create(root, new FileSystem(), logger.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName
?? throw new Exception("Unable to determine repository name");

var resolvedFile = fs.FileInfo.New(Path.Combine(root.FullName, file));

var runningOnCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"));
if (runningOnCi && !resolvedFile.Exists)
{
_log.LogInformation("Running on CI after a build that produced no {File}, skipping the validation", resolvedFile.FullName);
return 0;
}
if (runningOnCi && !Paths.TryFindDocsFolderFromRoot(fs, root, out _, out _))
{
_log.LogInformation("Running on CI, {Directory} has no documentation, skipping the validation", root.FullName);
return 0;
}

_log.LogInformation("Validating {File} in {Directory}", file, root.FullName);

await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService).StartAsync(ctx);
await _linkIndexLinkChecker.CheckWithLocalLinksJson(collector, repository, file, ctx);
await _linkIndexLinkChecker.CheckWithLocalLinksJson(collector, repository, resolvedFile.FullName, ctx);
await collector.StopAsync(ctx);
return collector.Errors;
}
Expand Down
Loading