Skip to content

Commit b09c93c

Browse files
authored
Add inbound link validation to our smoke tests (#1360)
* Add inbound link validation to our smoke tests * Ensure file is scoped to path and log both * remove duplicate step id * Skip validate-link-reference on ci if its run against a repo with no docs * Smarter CI checks * pass -p to smoke-test * missing -- arg separator * ensure the default output folder is scoped to source folder if specified * Fallback to GITHUB_REPOSITORY as repo name instead of starting with it in order of preference * remove bad TrimEnd call
1 parent ec432ba commit b09c93c

File tree

7 files changed

+126
-62
lines changed

7 files changed

+126
-62
lines changed

.github/workflows/smoke-test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,8 @@ jobs:
3939
4040
- name: Verify landing-page-path output
4141
run: test ${{ steps.docs-build.outputs.landing-page-path }} == ${{ matrix.landing-page-path-output }}
42+
43+
- name: Verify link validation
44+
run: |
45+
dotnet run --project src/tooling/docs-builder -- inbound-links validate-link-reference -p test-repo
46+

src/Elastic.Documentation.Configuration/BuildContext.cs

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ public BuildContext(
7777
? ReadFileSystem.DirectoryInfo.New(source)
7878
: ReadFileSystem.DirectoryInfo.New(Path.Combine(Paths.WorkingDirectoryRoot.FullName));
7979

80-
(DocumentationSourceDirectory, ConfigurationPath) = FindDocsFolderFromRoot(rootFolder);
80+
(DocumentationSourceDirectory, ConfigurationPath) = Paths.FindDocsFolderFromRoot(ReadFileSystem, rootFolder);
8181

8282
DocumentationCheckoutDirectory = Paths.DetermineSourceDirectoryRoot(DocumentationSourceDirectory);
8383

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

8888
if (ConfigurationPath.FullName != DocumentationSourceDirectory.FullName)
8989
DocumentationSourceDirectory = ConfigurationPath.Directory!;
@@ -96,29 +96,4 @@ public BuildContext(
9696
};
9797
}
9898

99-
private (IDirectoryInfo, IFileInfo) FindDocsFolderFromRoot(IDirectoryInfo rootPath)
100-
{
101-
string[] files = ["docset.yml", "_docset.yml"];
102-
string[] knownFolders = [rootPath.FullName, Path.Combine(rootPath.FullName, "docs")];
103-
var mostLikelyTargets =
104-
from file in files
105-
from folder in knownFolders
106-
select Path.Combine(folder, file);
107-
108-
var knownConfigPath = mostLikelyTargets.FirstOrDefault(ReadFileSystem.File.Exists);
109-
var configurationPath = knownConfigPath is null ? null : ReadFileSystem.FileInfo.New(knownConfigPath);
110-
if (configurationPath is not null)
111-
return (configurationPath.Directory!, configurationPath);
112-
113-
configurationPath = rootPath
114-
.EnumerateFiles("*docset.yml", SearchOption.AllDirectories)
115-
.FirstOrDefault()
116-
?? throw new Exception($"Can not locate docset.yml file in '{rootPath}'");
117-
118-
var docsFolder = configurationPath.Directory
119-
?? throw new Exception($"Can not locate docset.yml file in '{rootPath}'");
120-
121-
return (docsFolder, configurationPath);
122-
}
123-
12499
}

src/Elastic.Documentation.Configuration/Paths.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Diagnostics.CodeAnalysis;
56
using System.IO.Abstractions;
67

78
namespace Elastic.Documentation.Configuration;
@@ -50,4 +51,47 @@ private static DirectoryInfo GetApplicationFolder()
5051
return new DirectoryInfo(elasticPath);
5152
}
5253

54+
public static (IDirectoryInfo, IFileInfo) FindDocsFolderFromRoot(IFileSystem readFileSystem, IDirectoryInfo rootPath)
55+
{
56+
string[] files = ["docset.yml", "_docset.yml"];
57+
string[] knownFolders = [rootPath.FullName, Path.Combine(rootPath.FullName, "docs")];
58+
var mostLikelyTargets =
59+
from file in files
60+
from folder in knownFolders
61+
select Path.Combine(folder, file);
62+
63+
var knownConfigPath = mostLikelyTargets.FirstOrDefault(readFileSystem.File.Exists);
64+
var configurationPath = knownConfigPath is null ? null : readFileSystem.FileInfo.New(knownConfigPath);
65+
if (configurationPath is not null)
66+
return (configurationPath.Directory!, configurationPath);
67+
68+
configurationPath = rootPath
69+
.EnumerateFiles("*docset.yml", SearchOption.AllDirectories)
70+
.FirstOrDefault()
71+
?? throw new Exception($"Can not locate docset.yml file in '{rootPath}'");
72+
73+
var docsFolder = configurationPath.Directory ?? throw new Exception($"Can not locate docset.yml file in '{rootPath}'");
74+
75+
return (docsFolder, configurationPath);
76+
}
77+
78+
public static bool TryFindDocsFolderFromRoot(
79+
IFileSystem readFileSystem,
80+
IDirectoryInfo rootPath,
81+
[NotNullWhen(true)] out IDirectoryInfo? docDirectory,
82+
[NotNullWhen(true)] out IFileInfo? configurationPath
83+
)
84+
{
85+
docDirectory = null;
86+
configurationPath = null;
87+
try
88+
{
89+
(docDirectory, configurationPath) = FindDocsFolderFromRoot(readFileSystem, rootPath);
90+
return true;
91+
}
92+
catch
93+
{
94+
return false;
95+
}
96+
}
5397
}

src/Elastic.Documentation/GitCheckoutInformation.cs

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
using System.IO.Abstractions;
66
using System.Text.Json.Serialization;
7+
using System.Text.RegularExpressions;
78
using Microsoft.Extensions.Logging;
89
using SoftCircuits.IniFileParser;
910

1011
namespace Elastic.Documentation;
1112

12-
public record GitCheckoutInformation
13+
public partial record GitCheckoutInformation
1314
{
1415
public static GitCheckoutInformation Unavailable { get; } = new()
1516
{
@@ -74,31 +75,33 @@ public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem
7475
using var streamReader = new StreamReader(stream);
7576
ini.Load(streamReader);
7677

77-
var remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY");
78+
var remote = BranchTrackingRemote(branch, ini);
79+
logger?.LogInformation("Remote from branch: {GitRemote}", remote);
7880
if (string.IsNullOrEmpty(remote))
7981
{
80-
remote = BranchTrackingRemote(branch, ini);
81-
logger?.LogInformation("Remote from branch: {GitRemote}", remote);
82-
if (string.IsNullOrEmpty(remote))
83-
{
84-
remote = BranchTrackingRemote("main", ini);
85-
logger?.LogInformation("Remote from main branch: {GitRemote}", remote);
86-
}
82+
remote = BranchTrackingRemote("main", ini);
83+
logger?.LogInformation("Remote from main branch: {GitRemote}", remote);
84+
}
8785

88-
if (string.IsNullOrEmpty(remote))
89-
{
90-
remote = BranchTrackingRemote("master", ini);
91-
logger?.LogInformation("Remote from master branch: {GitRemote}", remote);
92-
}
86+
if (string.IsNullOrEmpty(remote))
87+
{
88+
remote = BranchTrackingRemote("master", ini);
89+
logger?.LogInformation("Remote from master branch: {GitRemote}", remote);
90+
}
9391

94-
if (string.IsNullOrEmpty(remote))
95-
{
96-
remote = "elastic/docs-builder-unknown";
97-
logger?.LogInformation("Remote from fallback: {GitRemote}", remote);
98-
}
99-
remote = remote.AsSpan().TrimEnd("git").TrimEnd('.').ToString();
92+
if (string.IsNullOrEmpty(remote))
93+
{
94+
remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY");
95+
logger?.LogInformation("Remote from GITHUB_REPOSITORY: {GitRemote}", remote);
10096
}
10197

98+
if (string.IsNullOrEmpty(remote))
99+
{
100+
remote = "elastic/docs-builder-unknown";
101+
logger?.LogInformation("Remote from fallback: {GitRemote}", remote);
102+
}
103+
remote = CutOffGitExtension().Replace(remote, string.Empty);
104+
102105
var info = new GitCheckoutInformation
103106
{
104107
Ref = gitRef,
@@ -137,4 +140,7 @@ string BranchTrackingRemote(string b, IniFile c)
137140
return remote ?? string.Empty;
138141
}
139142
}
143+
144+
[GeneratedRegex(@"\.git$", RegexOptions.IgnoreCase, "en-US")]
145+
private static partial Regex CutOffGitExtension();
140146
}

src/tooling/docs-assembler/Cli/InboundLinkCommands.cs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ namespace Documentation.Assembler.Cli;
1717
internal sealed class InboundLinkCommands(ILoggerFactory logger, ICoreService githubActionsService)
1818
{
1919
private readonly LinkIndexLinkChecker _linkIndexLinkChecker = new(logger);
20+
private readonly ILogger<Program> _log = logger.CreateLogger<Program>();
2021

2122
[SuppressMessage("Usage", "CA2254:Template should be a static expression")]
2223
private void AssignOutputLogger()
2324
{
24-
var log = logger.CreateLogger<Program>();
25-
ConsoleApp.Log = msg => log.LogInformation(msg);
26-
ConsoleApp.LogError = msg => log.LogError(msg);
25+
ConsoleApp.Log = msg => _log.LogInformation(msg);
26+
ConsoleApp.LogError = msg => _log.LogError(msg);
2727
}
2828

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

79+
var resolvedFile = fs.FileInfo.New(Path.Combine(root.FullName, file));
80+
81+
var runningOnCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"));
82+
if (runningOnCi && !resolvedFile.Exists)
83+
{
84+
_log.LogInformation("Running on CI after a build that produced no {File}, skipping the validation", resolvedFile.FullName);
85+
return 0;
86+
}
87+
if (runningOnCi && !Paths.TryFindDocsFolderFromRoot(fs, root, out _, out _))
88+
{
89+
_log.LogInformation("Running on CI, {Directory} has no documentation, skipping the validation", root.FullName);
90+
return 0;
91+
}
92+
93+
_log.LogInformation("Validating {File} in {Directory}", file, root.FullName);
94+
7895
await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService).StartAsync(ctx);
79-
await _linkIndexLinkChecker.CheckWithLocalLinksJson(collector, repository, file, ctx);
96+
await _linkIndexLinkChecker.CheckWithLocalLinksJson(collector, repository, resolvedFile.FullName, ctx);
8097
await collector.StopAsync(ctx);
8198
return collector.Errors;
8299
}

src/tooling/docs-builder/Cli/Commands.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ public async Task<int> Generate(
125125
CanonicalBaseUrl = canonicalBaseUri
126126
};
127127
}
128-
// On CI, we are running on merge commit which may have changes against an older
129-
// docs folder (this can happen on out of date PR's).
128+
// On CI, we are running on a merge commit which may have changes against an older
129+
// docs folder (this can happen on out-of-date PR's).
130130
// At some point in the future we can remove this try catch
131131
catch (Exception e) when (runningOnCi && e.Message.StartsWith("Can not locate docset.yml file in"))
132132
{

src/tooling/docs-builder/Cli/InboundLinkCommands.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ namespace Documentation.Builder.Cli;
1818
internal sealed class InboundLinkCommands(ILoggerFactory logger, ICoreService githubActionsService)
1919
{
2020
private readonly LinkIndexLinkChecker _linkIndexLinkChecker = new(logger);
21+
private readonly ILogger<Program> _log = logger.CreateLogger<Program>();
2122

2223
[SuppressMessage("Usage", "CA2254:Template should be a static expression")]
2324
private void AssignOutputLogger()
2425
{
25-
var log = logger.CreateLogger<Program>();
26-
ConsoleApp.Log = msg => log.LogInformation(msg);
27-
ConsoleApp.LogError = msg => log.LogError(msg);
26+
ConsoleApp.Log = msg => _log.LogInformation(msg);
27+
ConsoleApp.LogError = msg => _log.LogError(msg);
2828
}
2929

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

86+
var resolvedFile = fs.FileInfo.New(Path.Combine(root.FullName, file));
87+
88+
var runningOnCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS"));
89+
if (runningOnCi && !resolvedFile.Exists)
90+
{
91+
_log.LogInformation("Running on CI after a build that produced no {File}, skipping the validation", resolvedFile.FullName);
92+
return 0;
93+
}
94+
if (runningOnCi && !Paths.TryFindDocsFolderFromRoot(fs, root, out _, out _))
95+
{
96+
_log.LogInformation("Running on CI, {Directory} has no documentation, skipping the validation", root.FullName);
97+
return 0;
98+
}
99+
100+
_log.LogInformation("Validating {File} in {Directory}", file, root.FullName);
101+
85102
await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService).StartAsync(ctx);
86-
await _linkIndexLinkChecker.CheckWithLocalLinksJson(collector, repository, file, ctx);
103+
await _linkIndexLinkChecker.CheckWithLocalLinksJson(collector, repository, resolvedFile.FullName, ctx);
87104
await collector.StopAsync(ctx);
88105
return collector.Errors;
89106
}

0 commit comments

Comments
 (0)