Skip to content

Commit 67ec691

Browse files
authored
Add inbound links validation (#587)
* Add inbound links validation * dotnet format * dotnet format
1 parent a853a3b commit 67ec691

24 files changed

+431
-71
lines changed

.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ dotnet_diagnostic.IDE0057.severity = none
227227
dotnet_diagnostic.IDE0051.severity = suggestion
228228
dotnet_diagnostic.IDE0059.severity = suggestion
229229

230+
230231
[DocumentationWebHost.cs]
231232
dotnet_diagnostic.IL3050.severity = none
232233
dotnet_diagnostic.IL2026.severity = none

src/docs-builder/Diagnostics/Console/ConsoleDiagnosticsCollector.cs renamed to Elastic.Documentation.Tooling/Diagnostics/Console/ConsoleDiagnosticsCollector.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
using Spectre.Console;
99
using Diagnostic = Elastic.Markdown.Diagnostics.Diagnostic;
1010

11-
namespace Documentation.Builder.Diagnostics.Console;
11+
namespace Elastic.Documentation.Tooling.Diagnostics.Console;
1212

1313
public class ConsoleDiagnosticsCollector(ILoggerFactory loggerFactory, ICoreService? githubActions = null)
1414
: DiagnosticsCollector([new Log(loggerFactory.CreateLogger<Log>()), new GithubAnnotationOutput(githubActions)]

src/docs-builder/Diagnostics/Console/ErrataFileSourceRepository.cs renamed to Elastic.Documentation.Tooling/Diagnostics/Console/ErrataFileSourceRepository.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
using Spectre.Console;
1111
using Diagnostic = Elastic.Markdown.Diagnostics.Diagnostic;
1212

13-
namespace Documentation.Builder.Diagnostics.Console;
13+
namespace Elastic.Documentation.Tooling.Diagnostics.Console;
1414

1515
public class ErrataFileSourceRepository : ISourceRepository
1616
{

src/docs-builder/Diagnostics/Console/GithubAnnotationOutput.cs renamed to Elastic.Documentation.Tooling/Diagnostics/Console/GithubAnnotationOutput.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using Actions.Core.Services;
77
using Elastic.Markdown.Diagnostics;
88

9-
namespace Documentation.Builder.Diagnostics.Console;
9+
namespace Elastic.Documentation.Tooling.Diagnostics.Console;
1010

1111
public class GithubAnnotationOutput(ICoreService? githubActions) : IDiagnosticsOutput
1212
{

src/docs-builder/Diagnostics/Log.cs renamed to Elastic.Documentation.Tooling/Diagnostics/Log.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using Elastic.Markdown.Diagnostics;
66
using Microsoft.Extensions.Logging;
77

8-
namespace Documentation.Builder.Diagnostics;
8+
namespace Elastic.Documentation.Tooling.Diagnostics;
99

1010
// named Log for terseness on console output
1111
public class Log(ILogger logger) : IDiagnosticsOutput

Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.2"/>
1313
<PackageReference Include="Github.Actions.Core" Version="9.0.0"/>
1414
<PackageReference Include="Crayon" Version="2.0.69"/>
15+
<PackageReference Include="Errata" Version="0.13.0" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="..\src\Elastic.Markdown\Elastic.Markdown.csproj" />
1520
</ItemGroup>
1621

1722
</Project>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: 'Validate Inbound Links'
2+
description: 'Validates all published cross links from all known repositories against local links.json'
3+
4+
runs:
5+
using: "composite"
6+
steps:
7+
- name: Validate Inbound Links
8+
uses: elastic/docs-builder/actions/assembler@main
9+
with:
10+
command: "link validate-inbound-local"

docs-builder.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assembler", "assembler", "{
5555
EndProject
5656
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Documentation.Tooling", "Elastic.Documentation.Tooling\Elastic.Documentation.Tooling.csproj", "{4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}"
5757
EndProject
58+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "validate-inbound-local", "validate-inbound-local", "{6E2ED6CC-AFC1-4E58-965D-6AEC500EBB46}"
59+
ProjectSection(SolutionItems) = preProject
60+
actions\validate-inbound-local\action.yml = actions\validate-inbound-local\action.yml
61+
EndProjectSection
62+
EndProject
5863
Global
5964
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6065
Debug|Any CPU = Debug|Any CPU
@@ -113,5 +118,6 @@ Global
113118
{7D36DDDA-9E0B-4D2C-8033-5D62FF8B6166} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
114119
{CFEE9FAD-9E0C-4C0E-A0C2-B97D594C14B5} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
115120
{4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
121+
{6E2ED6CC-AFC1-4E58-965D-6AEC500EBB46} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
116122
EndGlobalSection
117123
EndGlobal
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Collections.Frozen;
6+
using Elastic.Markdown.IO.Configuration;
7+
using Elastic.Markdown.IO.State;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace Elastic.Markdown.CrossLinks;
11+
12+
public class ConfigurationCrossLinkFetcher(ConfigurationFile configuration, ILoggerFactory logger) : CrossLinkFetcher(logger)
13+
{
14+
public override async Task<FetchedCrossLinks> Fetch()
15+
{
16+
var dictionary = new Dictionary<string, LinkReference>();
17+
var declaredRepositories = new HashSet<string>();
18+
foreach (var repository in configuration.CrossLinkRepositories)
19+
{
20+
_ = declaredRepositories.Add(repository);
21+
try
22+
{
23+
var linkReference = await Fetch(repository);
24+
dictionary.Add(repository, linkReference);
25+
}
26+
catch when (repository == "docs-content")
27+
{
28+
throw;
29+
}
30+
catch when (repository != "docs-content")
31+
{
32+
// TODO: ignored for now while we wait for all links.json files to populate
33+
}
34+
}
35+
36+
return new FetchedCrossLinks
37+
{
38+
DeclaredRepositories = declaredRepositories,
39+
LinkReferences = dictionary.ToFrozenDictionary()
40+
};
41+
}
42+
43+
44+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Collections.Frozen;
6+
using System.Text.Json;
7+
using Elastic.Markdown.IO;
8+
using Elastic.Markdown.IO.State;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace Elastic.Markdown.CrossLinks;
12+
13+
public record FetchedCrossLinks
14+
{
15+
public required FrozenDictionary<string, LinkReference> LinkReferences { get; init; }
16+
public required HashSet<string> DeclaredRepositories { get; init; }
17+
18+
public static FetchedCrossLinks Empty { get; } = new()
19+
{
20+
DeclaredRepositories = [],
21+
LinkReferences = new Dictionary<string, LinkReference>().ToFrozenDictionary()
22+
};
23+
}
24+
25+
public abstract class CrossLinkFetcher(ILoggerFactory logger) : IDisposable
26+
{
27+
private readonly ILogger _logger = logger.CreateLogger(nameof(CrossLinkFetcher));
28+
private readonly HttpClient _client = new();
29+
private LinkIndex? _linkIndex;
30+
31+
public static LinkReference Deserialize(string json) =>
32+
JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LinkReference)!;
33+
34+
public abstract Task<FetchedCrossLinks> Fetch();
35+
36+
protected async Task<LinkIndex> FetchLinkIndex()
37+
{
38+
if (_linkIndex is not null)
39+
{
40+
_logger.LogInformation("Using cached link index");
41+
return _linkIndex;
42+
}
43+
var url = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json";
44+
_logger.LogInformation("Fetching {Url}", url);
45+
var json = await _client.GetStringAsync(url);
46+
_linkIndex = LinkIndex.Deserialize(json);
47+
return _linkIndex;
48+
}
49+
50+
protected async Task<LinkReference> Fetch(string repository)
51+
{
52+
var linkIndex = await FetchLinkIndex();
53+
if (!linkIndex.Repositories.TryGetValue(repository, out var repositoryLinks))
54+
throw new Exception($"Repository {repository} not found in link index");
55+
56+
if (!repositoryLinks.TryGetValue("main", out var linkIndexEntry))
57+
throw new Exception($"Repository {repository} not found in link index");
58+
59+
return await FetchLinkIndexEntry(repository, linkIndexEntry);
60+
}
61+
62+
protected async Task<LinkReference> FetchLinkIndexEntry(string repository, LinkIndexEntry linkIndexEntry)
63+
{
64+
var linkReference = await TryGetCachedLinkReference(repository, linkIndexEntry);
65+
if (linkReference is not null)
66+
return linkReference;
67+
68+
var url = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{repository}/main/links.json";
69+
_logger.LogInformation("Fetching links.json for '{Repository}': {Url}", repository, url);
70+
var json = await _client.GetStringAsync(url);
71+
linkReference = Deserialize(json);
72+
WriteLinksJsonCachedFile(repository, linkIndexEntry, json);
73+
return linkReference;
74+
}
75+
76+
private void WriteLinksJsonCachedFile(string repository, LinkIndexEntry linkIndexEntry, string json)
77+
{
78+
var cachedFileName = $"links-elastic-{repository}-main-{linkIndexEntry.ETag}.json";
79+
var cachedPath = Path.Combine(Paths.ApplicationData.FullName, "links", cachedFileName);
80+
if (File.Exists(cachedPath))
81+
return;
82+
try
83+
{
84+
_ = Directory.CreateDirectory(Path.GetDirectoryName(cachedPath)!);
85+
File.WriteAllText(cachedPath, json);
86+
}
87+
catch (Exception e)
88+
{
89+
_logger.LogError(e, "Failed to write cached link reference {CachedPath}", cachedPath);
90+
}
91+
}
92+
93+
private async Task<LinkReference?> TryGetCachedLinkReference(string repository, LinkIndexEntry linkIndexEntry)
94+
{
95+
var cachedFileName = $"links-elastic-{repository}-main-{linkIndexEntry.ETag}.json";
96+
var cachedPath = Path.Combine(Paths.ApplicationData.FullName, "links", cachedFileName);
97+
if (File.Exists(cachedPath))
98+
{
99+
try
100+
{
101+
var json = await File.ReadAllTextAsync(cachedPath);
102+
var linkReference = Deserialize(json);
103+
return linkReference;
104+
}
105+
catch (Exception e)
106+
{
107+
_logger.LogError(e, "Failed to read cached link reference {CachedPath}", cachedPath);
108+
return null;
109+
}
110+
}
111+
return null;
112+
113+
}
114+
115+
public void Dispose()
116+
{
117+
_client.Dispose();
118+
logger.Dispose();
119+
GC.SuppressFinalize(this);
120+
}
121+
}

0 commit comments

Comments
 (0)