diff --git a/.gitignore b/.gitignore index c87b8d0..72c21b0 100644 --- a/.gitignore +++ b/.gitignore @@ -180,9 +180,6 @@ DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html -# Click-Once directory -publish/ - # Publish Web Output *.[Pp]ublish.xml *.azurePubxml diff --git a/action.yml b/action.yml index a9b25fc..9ac58b1 100644 --- a/action.yml +++ b/action.yml @@ -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" - \ No newline at end of file + - 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 diff --git a/actions/generator/action.yml b/actions/generator/action.yml new file mode 100644 index 0000000..6eec713 --- /dev/null +++ b/actions/generator/action.yml @@ -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" + diff --git a/actions/publish/action.yml b/actions/publish/action.yml new file mode 100644 index 0000000..385f85b --- /dev/null +++ b/actions/publish/action.yml @@ -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 + + diff --git a/docs-builder.sln b/docs-builder.sln index 50e4344..46d70b8 100644 --- a/docs-builder.sln +++ b/docs-builder.sln @@ -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 @@ -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 diff --git a/src/docs-generator/Cli/ArgsFilter.cs b/src/docs-generator/Cli/ArgsFilter.cs new file mode 100644 index 0000000..896987e --- /dev/null +++ b/src/docs-generator/Cli/ArgsFilter.cs @@ -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; + +/// +/// 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 +/// +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 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; + } + } + } +} diff --git a/src/docs-generator/Cli/Commands.cs b/src/docs-generator/Cli/Commands.cs new file mode 100644 index 0000000..409d175 --- /dev/null +++ b/src/docs-generator/Cli/Commands.cs @@ -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(); + + [Command("")] + public async Task 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(); + 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; + } +} diff --git a/src/docs-generator/Program.cs b/src/docs-generator/Program.cs index 248565a..7a73f7f 100644 --- a/src/docs-generator/Program.cs +++ b/src/docs-generator/Program.cs @@ -7,150 +7,39 @@ // ReSharper disable RedundantLambdaParameterType -using System.Security.Cryptography.X509Certificates; -using Bogus; +using Actions.Core.Extensions; using ConsoleAppFramework; +using Documentation.Generator.Cli; using Documentation.Generator.Domain; -using Soenneker.Utils.AutoBogus; -using Soenneker.Utils.AutoBogus.Config; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; -await ConsoleApp.RunAsync(args, async Task ( - int? seedFileSystem = null, - int? seedContent = null, - string? output = null, - bool? clear = null -) => -{ - 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); - - Console.WriteLine($"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(); - 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 arguments = Arguments.Filter(args); - 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(); - } - } - - Console.WriteLine($"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 services = new ServiceCollection(); +services.AddGitHubActionsCore(); +services.AddLogging(x => +{ + x.ClearProviders(); + x.SetMinimumLevel(LogLevel.Information); + x.AddSimpleConsole(c => { - var directory = Path.Combine(outputFolder.FullName, file.Directory); - Console.WriteLine($"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); + c.SingleLine = true; + c.IncludeScopes = true; + c.UseUtcTimestamp = true; + c.TimestampFormat = Environment.UserInteractive ? ":: " : "[yyyy-MM-ddTHH:mm:ss] "; + }); }); -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); -} - -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); +await using var serviceProvider = services.BuildServiceProvider(); +var logger = serviceProvider.GetRequiredService>(); +ConsoleApp.ServiceProvider = serviceProvider; +if (!arguments.IsHelp) + ConsoleApp.Log = msg => logger.LogInformation(msg); +ConsoleApp.LogError = msg => logger.LogError(msg); - File.AppendAllText(filePath, section.Paragraphs); - File.AppendAllText(filePath, Environment.NewLine); - } -} +var app = ConsoleApp.Create(); +app.Add(); -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; - Console.WriteLine($"Seeding with {seedFs} from previous run {fileInfo.FullName}"); - cleanOutput = clear ?? false; -} +await app.RunAsync(arguments.Args).ConfigureAwait(false);