Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,6 @@ DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
Expand Down
23 changes: 19 additions & 4 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,25 @@ branding:

inputs:
prefix:
description: 'The relative location of the documentation'
description: 'Path prefix for all urls'
required: false

runs:
using: 'docker'
image: "docker://ghcr.io/elastic/docs-builder:edge"

- id: repo-basename
run: 'echo "value=`basename ${{ github.repository }}`" >> $GITHUB_OUTPUT'
- uses: actions/checkout@v4
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5.0.0
- name: Build documentation
uses: elastic/docs-builder@main
with:
prefix: '${{ steps.repo-basename.outputs.value }}'
- name: Upload artifact
uses: actions/upload-pages-artifact@v3.0.1
with:
path: .artifacts/docs/html

- name: Deploy artifact
id: deployment
uses: actions/deploy-pages@v4.0.5
16 changes: 16 additions & 0 deletions actions/generator/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: 'Documentation Generator'
description: 'Generates a random yet deterministic documentation set'

branding:
icon: 'filter'
color: 'red'

inputs:
output:
description: 'Path to output the documentation'
required: false

runs:
using: 'docker'
image: "docker://ghcr.io/elastic/docs-generator:edge"

35 changes: 35 additions & 0 deletions actions/publish/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: 'Documentation Publisher'
description: 'Builds and publishes documentation to github pages'

branding:
icon: 'filter'
color: 'red'

outputs:
page_url:
description: "The github actions url"
value: ${{steps.deployment.outputs.page_url}}

runs:
using: "composite"
steps:
- id: repo-basename
run: 'echo "value=`basename ${{ github.repository }}`" >> $GITHUB_OUTPUT'
- uses: actions/checkout@v4
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5.0.0
- name: Build documentation
uses: elastic/docs-builder@main
with:
prefix: '${{ steps.repo-basename.outputs.value }}'
- name: Upload artifact
uses: actions/upload-pages-artifact@v3.0.1
with:
path: .artifacts/docs/html

- name: Deploy artifact
id: deployment
uses: actions/deploy-pages@v4.0.5


14 changes: 14 additions & 0 deletions docs-builder.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Markdown.Tests", "t
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs-generator", "src\docs-generator\docs-generator.csproj", "{61904527-9753-4379-B546-56B6A29073AC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "actions", "actions", "{245023D2-D3CA-47B9-831D-DAB91A2FFDC7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "generator", "generator", "{1C340CCF-9AAC-4163-A7BB-60528076E98B}"
ProjectSection(SolutionItems) = preProject
actions\generator\action.yml = actions\generator\action.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "publish", "publish", "{CD2887E3-BDA9-434B-A5BF-9ED38DE20332}"
ProjectSection(SolutionItems) = preProject
actions\publish\action.yml = actions\publish\action.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -72,5 +84,7 @@ Global
{01F05AD0-E0E0-401F-A7EC-905928E1E9F0} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
{B27C5107-128B-465A-B8F8-8985399E4CFB} = {67B576EE-02FA-4F9B-94BC-3630BC09ECE5}
{61904527-9753-4379-B546-56B6A29073AC} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A}
{1C340CCF-9AAC-4163-A7BB-60528076E98B} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
{CD2887E3-BDA9-434B-A5BF-9ED38DE20332} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7}
EndGlobalSection
EndGlobal
34 changes: 34 additions & 0 deletions src/docs-generator/Cli/ArgsFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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 Documentation.Generator.Cli;

/// <summary>
/// This exists temporarily for .NET 8.
/// The container builds prepends `dotnet [app].dll` as arguments
/// Fixed in .NET 9: https://github.com/dotnet/sdk-container-builds/issues/559
/// </summary>
public class Arguments
{
public required string[] Args { get; init; }
public required bool IsHelp { get; init; }

public static Arguments Filter(string[] args) =>
new Arguments { Args = Enumerate(args).ToArray(), IsHelp = args.Contains("-h") || args.Contains("--help") };

private static IEnumerable<string> Enumerate(string[] args)
{
for (var i = 0; i < args.Length; i++)
{
switch (i)
{
case 0 when args[i] == "dotnet":
case 1 when args[i].EndsWith(".dll"):
continue;
default:
yield return args[i];
break;
}
}
}
}
160 changes: 160 additions & 0 deletions src/docs-generator/Cli/Commands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// 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 Actions.Core.Services;
using ConsoleAppFramework;
using Documentation.Generator.Domain;
using Microsoft.Extensions.Logging;

namespace Documentation.Generator.Cli;

internal class Commands(ILoggerFactory logger, ICoreService githubActionsService)
{
private readonly ILogger _logger = logger.CreateLogger<Commands>();

[Command("")]
public async Task<int> Generate(
int? seedFileSystem = null,
int? seedContent = null,
string? output = null,
bool? clear = null,
Cancel ctx = default
)
{
output ??= githubActionsService.GetInput("output");
var cleanOutputDirectory = clear ?? true;
var outputFolder = !string.IsNullOrWhiteSpace(output)
? new DirectoryInfo(output)
: new DirectoryInfo(Path.Combine(Paths.Root.FullName, ".artifacts/docs/markdown"));
var stateFile = new FileInfo(Path.Combine(outputFolder.FullName, "generator.state"));

LoadStateFromFile(stateFile, clear, ref seedFileSystem, ref cleanOutputDirectory);

Determinism.Random = new Determinism(seedFileSystem, seedContent);

_logger.LogInformation(
$"Running generator with file seed: {Determinism.Random.SeedFileSystem} and content seed: {Determinism.Random.SeedContent}");

Generators.FolderName.UseSeed(Determinism.Random.SeedFileSystem);
Generators.File.UseSeed(Determinism.Random.SeedFileSystem);
Generators.Section.UseSeed(Determinism.Random.SeedContent);

Generators.FolderNames = Generators.FolderName
.Generate(Determinism.Random.FileSystem.Number(3, 15))
.SelectMany(p => Generators.CreateSubPaths(p.Folder, Determinism.Random.FileSystem.Number(0, 3), 0))
.Distinct()
.ToArray();

var folders = new List<Folder>();
foreach (var folder in Generators.FolderNames)
{
var mdFolder = new Folder
{
Path = folder,
Files = Generators.File
.Generate(Determinism.Random.FileSystem.Number(1, 4))
.Select(f =>
{
f.Directory = folder;
return f;
})
.ToArray()
};
folders.Add(mdFolder);
}

var files = folders.SelectMany(f => f.Files).ToArray();
foreach (var folder in folders)
{
foreach (var file in folder.Files)
{
var length = Determinism.Random.Contents.Number(1, 10);
file.Links = Enumerable.Range(0, length)
.Select(i => files[Determinism.Random.Contents.Number(0, files.Length - 1)])
.Select(f => f.GetRandomLink())
.ToList();
file.RewriteLinksIntoSections();
}
}

_logger.LogInformation($"Writing to {outputFolder.FullName}");

if (outputFolder.Exists && cleanOutputDirectory)
Directory.Delete(outputFolder.FullName, true);

var updateFiles = files
.Where(f => cleanOutputDirectory || f.IncludeInUpdate)
.ToArray();
foreach (var file in updateFiles)
{
var directory = Path.Combine(outputFolder.FullName, file.Directory);
_logger.LogInformation($"Writing to {directory}");
Directory.CreateDirectory(directory);

WriteMarkdownFile(outputFolder, file);
}

var name = $"random-docset-{seedContent}-{seedFileSystem}";
WriteIndexMarkdownFile(name, outputFolder);

var docset = Path.Combine(outputFolder.FullName, "docset.yml");
File.WriteAllText(docset, $"project: {name}{Environment.NewLine}");
File.AppendAllText(docset, $"toc:{Environment.NewLine}");
foreach (var folder in folders)
File.AppendAllText(docset, $" - folder: {folder.Path}{Environment.NewLine}");

File.AppendAllText(docset, $" - file: index.md{Environment.NewLine}");
File.AppendAllText(docset, Environment.NewLine);

File.WriteAllText(stateFile.FullName, $"{Determinism.Random.SeedFileSystem}|{Determinism.Random.SeedContent}");

return await Task.FromResult(0);
}

private void WriteIndexMarkdownFile(string name, DirectoryInfo directoryInfo)
{
var filePath = Path.Combine(directoryInfo.FullName, "index.md");
File.WriteAllText(filePath,
$"""
---
title: {name} Documentation Set
---

""");
File.AppendAllText(filePath, "This docset is generated using docs-generator");
File.AppendAllText(filePath, Environment.NewLine);
}

private void WriteMarkdownFile(DirectoryInfo directoryInfo, MarkdownFile markdownFile)
{
var filePath = Path.Combine(directoryInfo.FullName, markdownFile.RelativePath);
File.WriteAllText(filePath,
$"""
---
title: {markdownFile.Title}
---

""");
foreach (var section in markdownFile.Sections)
{
File.AppendAllText(filePath, Environment.NewLine);
var header = new string('#', section.Level);
File.AppendAllText(filePath, $"{header} {section.Header}{Environment.NewLine}");
File.AppendAllText(filePath, Environment.NewLine);

File.AppendAllText(filePath, section.Paragraphs);
File.AppendAllText(filePath, Environment.NewLine);
}
}

void LoadStateFromFile(FileInfo fileInfo, bool? clear, ref int? seedFs, ref bool cleanOutput)
{
if (!fileInfo.Exists) return;
var state = File.ReadAllText(fileInfo.FullName).Split("|");
if (state.Length != 2) return;
seedFs ??= int.TryParse(state[0], out var seed) ? seed : seedFs;
_logger.LogInformation($"Seeding with {seedFs} from previous run {fileInfo.FullName}");
cleanOutput = clear ?? false;
}
}
Loading
Loading