Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
210ad09
feat(tooling): migrate docs-builder and aspire from ConsoleAppFramewo…
Mpdreamz Apr 24, 2026
2ce4dac
refactor(services): introduce DTOs to replace flat parameter lists
Mpdreamz Apr 24, 2026
acf6d0a
feat(tooling): upgrade to Nullean.Argh 0.8.0; native IReadOnlySet<Enu…
Mpdreamz Apr 28, 2026
01ce9f8
feat(tooling): upgrade to Nullean.Argh 0.8.1; MapAndRootAlias for doc…
Mpdreamz Apr 28, 2026
0a67952
feat(tooling): upgrade to Nullean.Argh 0.9.0
Mpdreamz Apr 28, 2026
e0d8641
docs(cli): help text overhaul, validation pass, and flag renames
Mpdreamz Apr 28, 2026
3bcb952
docs(cli): help text pass — summaries, remarks, no code blocks, names…
Mpdreamz Apr 28, 2026
be87f0c
feat(tooling): upgrade to Nullean.Argh 0.9.1; [AsParameters] DTO help…
Mpdreamz Apr 28, 2026
e7d3111
refactor(cli): use [Url] from DataAnnotations for URI fields; Canonic…
Mpdreamz Apr 28, 2026
cb4bc5b
refactor(cli): migrate file/directory path params to FileInfo and Dir…
Mpdreamz Apr 28, 2026
ad18d2b
feat(tooling): upgrade to Nullean.Argh 0.11.0; add [Existing], [Rejec…
Mpdreamz Apr 28, 2026
d75f096
chore: upgrade Nullean.Argh to 0.12.0
Mpdreamz Apr 29, 2026
ff01791
fix(services): guard exporter default against empty set from argh [As…
Mpdreamz Apr 29, 2026
1ade848
fix: pin to Nullean.Argh 0.12.0; 0.12.1+ introduces CS8600 in generat…
Mpdreamz Apr 29, 2026
2e2cc27
chore: upgrade to Nullean.Argh 0.12.3
Mpdreamz Apr 29, 2026
0f6ed28
fix: correct import ordering in ChangelogCommand.cs (dotnet format)
Mpdreamz Apr 29, 2026
2c70eb8
chore: upgrade Nullean.Argh to 0.12.4 (fixes global bool short option…
Mpdreamz Apr 29, 2026
f01c04b
chore: upgrade Nullean.Argh to 0.12.5 (fixes global short aliases aft…
Mpdreamz Apr 29, 2026
9706e56
Merge remote-tracking branch 'origin/main' into feature/argh
Copilot Apr 30, 2026
67f4535
refactor: remove IsMcpMode dead code and simplify AddElasticDocumenta…
Mpdreamz Apr 30, 2026
19bdd9c
refactor: promote GlobalCliOptions to Elastic.Documentation, remove G…
Mpdreamz May 4, 2026
60c30b4
refactor: add configure-only overload to AddDocumentationServiceDefaults
Mpdreamz May 4, 2026
47619dc
refactor: make GlobalCliOptions.LogLevel nullable
Mpdreamz May 4, 2026
3b47b50
chore: upgrade argh to 0.13.1, restore non-nullable LogLevel default
Mpdreamz May 6, 2026
7a4ad55
chore: upgrade argh to 0.14.0, enable XML doc gen for Elastic.Documen…
Mpdreamz May 6, 2026
bfb386c
chore: upgrade argh to 0.15.0
Mpdreamz May 6, 2026
9c775e9
chore: upgrade argh to 0.15.1, restore --skip-private-repositories in…
Mpdreamz May 6, 2026
62fecfa
fix: honor both --eis/--no-eis and skip update check on failure
Mpdreamz May 6, 2026
7b24b94
Merge branch 'main' into feature/argh
Mpdreamz May 6, 2026
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
7 changes: 4 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@
<PackageVersion Include="FSharp.Core" Version="10.1.201" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="ConsoleAppFramework" Version="5.7.13" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
<PackageVersion Include="ConsoleAppFramework.Abstractions" Version="5.7.2" />
<PackageVersion Include="Nullean.Argh" Version="0.15.1" />
<PackageVersion Include="Nullean.Argh.Hosting" Version="0.15.1" />
<PackageVersion Include="Nullean.Argh.Interfaces" Version="0.15.1" />
<PackageVersion Include="Crayon" Version="2.0.69" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="Errata" Version="0.16.0" />
Expand All @@ -87,7 +88,7 @@
<PackageVersion Include="Proc" Version="0.13.0" />
<PackageVersion Include="RazorSlices" Version="0.9.5" />
<PackageVersion Include="Samboy063.Tomlet" Version="6.0.0" />
<PackageVersion Include="Sep" Version="0.11.0" />
<PackageVersion Include="Sep" Version="0.12.5" />
<PackageVersion Include="Slugify.Core" Version="4.0.1" />
<PackageVersion Include="SoftCircuits.IniFileParser" Version="2.7.0" />
<PackageVersion Include="System.IO.Abstractions" Version="22.1.1" />
Expand Down
274 changes: 161 additions & 113 deletions aspire/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,125 +2,173 @@
// 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 ConsoleAppFramework;
using Elastic.Documentation;
using Nullean.Argh;
using static Elastic.Documentation.Aspire.ResourceNames;

GlobalCli.Process(ref args, out _, out var globalArguments);
// Extract global doc-builder flags before argh routing so they can be forwarded
// to docs-builder sub-process invocations (--log-level, --config-source, etc.).
AspireHost.GlobalArguments = AspireHost.ExtractGlobalArgs(ref args);

await ConsoleApp.RunAsync(args, BuildAspireHost);
return;
var app = new ArghApp();
app.MapRoot(AspireHost.Run);
return await app.RunAsync(args);

// ReSharper disable once RedundantLambdaParameterType
// ReSharper disable once VariableHidesOuterVariable
async Task BuildAspireHost(bool startElasticsearch, bool assumeCloned, bool assumeBuild, bool skipPrivateRepositories, Cancel ctx)
{
var builder = DistributedApplication.CreateBuilder(args);

var llmUrl = builder.AddParameter("LlmGatewayUrl", secret: true);
var llmServiceAccountPath = builder.AddParameter("LlmGatewayServiceAccountPath", secret: true);

var elasticsearchUrl = builder.AddParameter("DocumentationElasticUrl", secret: true);
var elasticsearchApiKey = builder.AddParameter("DocumentationElasticApiKey", secret: true);

var cloneAll = builder.AddProject<Projects.docs_builder>(AssemblerClone);
string[] cloneArgs = assumeCloned ? ["--assume-cloned"] : [];
cloneAll = cloneAll.WithArgs(["assembler", "clone", .. globalArguments, .. cloneArgs]);

var buildAll = builder.AddProject<Projects.docs_builder>(AssemblerBuild);
string[] buildArgs = assumeBuild ? ["--assume-build"] : [];
buildAll = buildAll
.WithArgs(["assembler", "build", .. globalArguments, .. buildArgs])
.WaitForCompletion(cloneAll)
.WithParentRelationship(cloneAll);

var elasticsearchLocal = builder.AddElasticsearch(ElasticsearchLocal)
.WithEnvironment("LICENSE", "trial");
if (!startElasticsearch)
elasticsearchLocal = elasticsearchLocal.WithExplicitStart();

var elasticsearchRemote = builder.AddExternalService(ElasticsearchRemote, elasticsearchUrl);

var api = builder.AddProject<Projects.Elastic_Documentation_Api_App>(Api)
.WithArgs(globalArguments)
.WithEnvironment("ENVIRONMENT", "dev")
.WithEnvironment("LLM_GATEWAY_FUNCTION_URL", llmUrl)
.WithEnvironment("LLM_GATEWAY_SERVICE_ACCOUNT_KEY_PATH", llmServiceAccountPath);

// ReSharper disable once RedundantAssignment
api = startElasticsearch
? api
.WithReference(elasticsearchLocal)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
.WithParentRelationship(elasticsearchLocal)
.WaitFor(elasticsearchLocal)
.WithExplicitStart()
: api.WithReference(elasticsearchRemote)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey)
.WithExplicitStart();
// ── Aspire host command ───────────────────────────────────────────────────────────────────────────

var mcp = builder.AddProject<Projects.Elastic_Documentation_Mcp_Remote>(RemoteMcp)
.WithArgs(globalArguments)
.WithEnvironment("ENVIRONMENT", "dev");

// ReSharper disable once RedundantAssignment
mcp = startElasticsearch
? mcp
.WithReference(elasticsearchLocal)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
.WithParentRelationship(elasticsearchLocal)
.WaitFor(elasticsearchLocal)
.WithExplicitStart()
: mcp.WithReference(elasticsearchRemote)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey)
internal static class AspireHost
{
internal static string[] GlobalArguments = [];

/// <summary>
/// Starts the Elastic documentation Aspire AppHost.
/// </summary>
/// <param name="startElasticsearch">Start a local Elasticsearch container</param>
/// <param name="assumeCloned">Skip cloning; assume repositories are already present on disk</param>
/// <param name="assumeBuild">Skip building; assume build output already exists</param>
/// <param name="skipPrivateRepositories">Skip cloning private repositories</param>
[NoOptionsInjection]
internal static async Task Run(
bool startElasticsearch = false,
bool assumeCloned = false,
bool assumeBuild = false,
bool skipPrivateRepositories = false,
CancellationToken ct = default)
{
var builder = DistributedApplication.CreateBuilder();

var llmUrl = builder.AddParameter("LlmGatewayUrl", secret: true);
var llmServiceAccountPath = builder.AddParameter("LlmGatewayServiceAccountPath", secret: true);

var elasticsearchUrl = builder.AddParameter("DocumentationElasticUrl", secret: true);
var elasticsearchApiKey = builder.AddParameter("DocumentationElasticApiKey", secret: true);

var cloneAll = builder.AddProject<Projects.docs_builder>(AssemblerClone);
string[] cloneArgs = assumeCloned ? ["--assume-cloned"] : [];
cloneAll = cloneAll.WithArgs(["assembler", "clone", .. GlobalArguments, .. cloneArgs]);

var buildAll = builder.AddProject<Projects.docs_builder>(AssemblerBuild);
string[] buildArgs = assumeBuild ? ["--assume-build"] : [];
buildAll = buildAll
.WithArgs(["assembler", "build", .. GlobalArguments, .. buildArgs])
.WaitForCompletion(cloneAll)
.WithParentRelationship(cloneAll);

var elasticsearchLocal = builder.AddElasticsearch(ElasticsearchLocal)
.WithEnvironment("LICENSE", "trial");
if (!startElasticsearch)
elasticsearchLocal = elasticsearchLocal.WithExplicitStart();

var elasticsearchRemote = builder.AddExternalService(ElasticsearchRemote, elasticsearchUrl);

var api = builder.AddProject<Projects.Elastic_Documentation_Api_App>(Api)
.WithArgs(GlobalArguments)
.WithEnvironment("ENVIRONMENT", "dev")
.WithEnvironment("LLM_GATEWAY_FUNCTION_URL", llmUrl)
.WithEnvironment("LLM_GATEWAY_SERVICE_ACCOUNT_KEY_PATH", llmServiceAccountPath);

// ReSharper disable once RedundantAssignment
api = startElasticsearch
? api
.WithReference(elasticsearchLocal)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
.WithParentRelationship(elasticsearchLocal)
.WaitFor(elasticsearchLocal)
.WithExplicitStart()
: api.WithReference(elasticsearchRemote)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey)
.WithExplicitStart();

var mcp = builder.AddProject<Projects.Elastic_Documentation_Mcp_Remote>(RemoteMcp)
.WithArgs(GlobalArguments)
.WithEnvironment("ENVIRONMENT", "dev");

// ReSharper disable once RedundantAssignment
mcp = startElasticsearch
? mcp
.WithReference(elasticsearchLocal)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
.WithParentRelationship(elasticsearchLocal)
.WaitFor(elasticsearchLocal)
.WithExplicitStart()
: mcp.WithReference(elasticsearchRemote)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey)
.WithExplicitStart();

var indexElasticsearch = builder.AddProject<Projects.docs_builder>(ElasticsearchIngest)
.WithArgs(["assembler", "index", .. GlobalArguments])
.WaitForCompletion(cloneAll)
.WithExplicitStart();

var indexElasticsearch = builder.AddProject<Projects.docs_builder>(ElasticsearchIngest)
.WithArgs(["assembler", "index", .. globalArguments])
.WaitForCompletion(cloneAll)
.WithExplicitStart();

// ReSharper disable once RedundantAssignment
indexElasticsearch = startElasticsearch
? indexElasticsearch
.WaitFor(elasticsearchLocal)
.WithReference(elasticsearchLocal)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
.WithParentRelationship(elasticsearchLocal)
: indexElasticsearch
.WithReference(elasticsearchRemote)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey)
.WithParentRelationship(elasticsearchRemote);

var serveStatic = builder.AddProject<Projects.docs_builder>(AssemblerServe)
.WithEnvironment("LLM_GATEWAY_FUNCTION_URL", llmUrl)
.WithEnvironment("LLM_GATEWAY_SERVICE_ACCOUNT_KEY_PATH", llmServiceAccountPath)
.WithHttpEndpoint(port: 4000, isProxied: false)
.WithArgs(["assembler", "serve", .. globalArguments])
.WithHttpHealthCheck("/", 200)
.WaitForCompletion(buildAll)
.WithParentRelationship(cloneAll);

serveStatic = startElasticsearch
? serveStatic
.WithReference(elasticsearchLocal)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
: serveStatic
.WithReference(elasticsearchRemote)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey);


// ReSharper disable once RedundantAssignment
serveStatic = startElasticsearch ? serveStatic.WaitFor(elasticsearchLocal) : serveStatic.WaitFor(buildAll);

await builder.Build().RunAsync(ctx);
// ReSharper disable once RedundantAssignment
indexElasticsearch = startElasticsearch
? indexElasticsearch
.WaitFor(elasticsearchLocal)
.WithReference(elasticsearchLocal)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
.WithParentRelationship(elasticsearchLocal)
: indexElasticsearch
.WithReference(elasticsearchRemote)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey)
.WithParentRelationship(elasticsearchRemote);

var serveStatic = builder.AddProject<Projects.docs_builder>(AssemblerServe)
.WithEnvironment("LLM_GATEWAY_FUNCTION_URL", llmUrl)
.WithEnvironment("LLM_GATEWAY_SERVICE_ACCOUNT_KEY_PATH", llmServiceAccountPath)
.WithHttpEndpoint(port: 4000, isProxied: false)
.WithArgs(["assembler", "serve", .. GlobalArguments])
.WithHttpHealthCheck("/", 200)
.WaitForCompletion(buildAll)
.WithParentRelationship(cloneAll);

serveStatic = startElasticsearch
? serveStatic
.WithReference(elasticsearchLocal)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
: serveStatic
.WithReference(elasticsearchRemote)
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey);

// ReSharper disable once RedundantAssignment
serveStatic = startElasticsearch ? serveStatic.WaitFor(elasticsearchLocal) : serveStatic.WaitFor(buildAll);

await builder.Build().RunAsync(ct);
}

/// <summary>
/// Extracts global doc-builder flags (--log-level, --config-source, --skip-private-repositories)
/// from <paramref name="args"/> in-place, returning them for forwarding to docs-builder sub-processes.
/// </summary>
internal static string[] ExtractGlobalArgs(ref string[] args)
{
var global = new List<string>();
var remaining = new List<string>();
for (var i = 0; i < args.Length; i++)
{
if (args[i] == "--log-level" && i + 1 < args.Length)
{
global.Add("--log-level");
global.Add(args[++i]);
}
else if (args[i] is "--config-source" or "--configuration-source" or "-c" && i + 1 < args.Length)
{
global.Add("--config-source");
global.Add(args[++i]);
}
else if (args[i] == "--skip-private-repositories")
global.Add("--skip-private-repositories");
else
remaining.Add(args[i]);
}
args = [.. remaining];
return [.. global];
}
}
7 changes: 2 additions & 5 deletions aspire/aspire.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,11 @@
<UserSecretsId>72f50f33-6fb9-4d08-bff3-39568fe370b3</UserSecretsId>
<IsTestProject>false</IsTestProject>
<RootNamespace>Elastic.Documentation.Aspire</RootNamespace>
<NoWarn>IDE0350</NoWarn>
<NoWarn>IDE0350;IDE0060</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ConsoleAppFramework">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Nullean.Argh" />
<PackageReference Include="Aspire.Hosting.AppHost"/>
<PackageReference Include="Elastic.Aspire.Hosting.Elasticsearch"/>
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public class ElasticsearchEndpoint
public bool DisableSslVerification { get; set; }
public X509Certificate? Certificate { get; set; }
public bool CertificateIsNotRoot { get; set; }
public int? BootstrapTimeout { get; set; }
public TimeSpan? BootstrapTimeout { get; set; }
public bool ForceReindex { get; set; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<!-- CS0618: Obsolete members - DisplayName is deprecated but still used by YAML deserialization -->
<NoWarn>$(NoWarn);CS0618</NoWarn>
<IsAotCompatible>true</IsAotCompatible>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;CS1573;CS1572;CS1571;CS1570;CS1574</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand All @@ -16,6 +18,7 @@
<ItemGroup>
<PackageReference Include="DotNet.Glob" />
<PackageReference Include="NetEscapades.EnumGenerators" />
<PackageReference Include="Nullean.Argh.Interfaces" />
<PackageReference Include="Nullean.ScopedFileSystem" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" />
<PackageReference Include="Samboy063.Tomlet" />
Expand Down
Loading
Loading