Skip to content

feat(tooling): migrate docs-builder CLI from ConsoleAppFramework to Nullean.Argh#3202

Open
Mpdreamz wants to merge 18 commits intomainfrom
feature/argh
Open

feat(tooling): migrate docs-builder CLI from ConsoleAppFramework to Nullean.Argh#3202
Mpdreamz wants to merge 18 commits intomainfrom
feature/argh

Conversation

@Mpdreamz
Copy link
Copy Markdown
Member

Why

docs-builder used ConsoleAppFramework as its CLI layer. Two fundamental problems made it worth replacing:

1. Help output was nearly useless.
CAF rendered a flat list of flags with no descriptions, no namespace grouping, and no validation hints. There was no way to express that --endpoint requires an http/https URI, that --plan-file must exist on disk, or that --exporters Html,Elasticsearch is a comma-separated collection. Contributors had to read source code to understand what a command accepted.

2. Namespace support was a string hack.
Commands were registered with prefixes like "assembler content-source" — a flat simulation of hierarchy. There was no real assembler --help page that showed only assembler sub-commands, no per-namespace help, and no way to document what the assembler namespace is.


What

This PR replaces ConsoleAppFramework with Nullean.Argh (0.11.0), a Roslyn source-generator CLI framework that is AOT/trim-safe by design.

Real namespace hierarchy

assembler, assembler deploy, assembler navigation, codex, changelog etc. are now first-class namespaces. docs-builder assembler --help shows only assembler commands. Each namespace has a one-liner summary visible in the parent listing.

Help text driven by XML docs

<summary> becomes the terse one-liner. <remarks> becomes the explanatory block. Param docs flow directly into flag descriptions. The migration invested in complete XML documentation for every command, with concepts like assembler, codex, link registry, and bloom filter explained inline for contributors unfamiliar with the infrastructure.

--help no longer emits host startup logs — argh 0.9.0+ detects intrinsic commands (--help, --version, __schema, __completion) at registration time and exits before the host is built.

Typed, validated parameters

URIs--endpoint and --proxy-address are Uri? with [Url] (DataAnnotations); argh rejects non-http/https values at parse time.

Timeouts--bootstrap-timeout is TimeSpan? with [TimeSpanRange("1s","60m")]. Users pass 4m or 90s, not a raw integer.

Thread / buffer counts[Range(1,128)] on thread counts, [Range(1,10_000)] on buffer size, [Range(0,20)] on retries.

File and directory paths — path parameters are FileInfo or DirectoryInfo (not string). Help shows <file> or <dir>. Where a file must exist before the command runs (config files, cert paths, plan files, links.json), [Existing] is applied and argh rejects at parse time rather than deep inside service code. [RejectSymbolicLinks] is on every path parameter. [ExpandUserProfile] means ~/path works everywhere.

File extensions[FileExtensions(Extensions="yml,yaml")] on config file arguments, [FileExtensions(Extensions="json")] on JSON inputs, [FileExtensions(Extensions="pem,der,crt,cer")] on certificate paths.

Exporters — previously a custom ExporterParser with its own vocabulary (es, config, links…). Now native IReadOnlySet<Exporter> binding — argh parses and validates the values, and lists the allowed set in help. Enum parsing is case-insensitive.

DTOs replace flat parameter lists

Service methods previously took 12–22 individual string?/bool?/int? parameters. This PR introduces IsolatedBuildOptions, AssemblerBuildOptions, AssemblerCloneOptions, and the already-existing ElasticsearchIndexOptions as proper records that:

  • Are bound from CLI flags via argh [AsParameters] (one line in the command method)
  • Are passed directly to service methods — no tuple unpacking, no 20-item lambda state
  • Can be constructed in unit tests as plain record initialisers, enabling real service-layer testing without mocking the CLI layer

docs-builder build as a named command

docs-builder (no sub-command) still builds the current documentation set. docs-builder build now also works as an explicit named command (MapAndRootAlias).

assemble consolidation

The old assemble (one-shot) and assembler (individual steps) namespace split is clarified: docs-builder assemble runs config-init + clone + build in one shot; docs-builder assembler clone/build/serve/index/sitemap are the individual steps.


Breaking changes

Before After
--no-ai-enrichment --no-ai-enrichment (via --ai-enrichment / --no-ai-enrichment)
--no-eis --eis / --no-eis
--bootstrap-timeout 4 (minutes as int) --bootstrap-timeout 4m (TimeSpan string)
docs-builder assemble (old one-shot) docs-builder assemble (unchanged behaviour, new name in help)
docs-builder codex <config> positional was string now FileInfo — path must resolve to a real file

Exporter names (html, es, config, links) are not breaking — Enum.TryParse is case-insensitive so html and Html both work.


argh bugs found and fixed upstream

During this migration five bugs were found and fixed in Nullean.Argh (0.7→0.11):

  1. Non-nullable enum global option emitting a required-flag error on --help
  2. {identifier} in XML doc text emitted as C# interpolation holes in generated code
  3. bool? + bool no<Name> generating duplicate --no-* switch arms (CS8510)
  4. __ev reused across multiple enum properties in the same options type (CS0136)
  5. [AsParameters] DTO property <summary> XML docs not rendered in --help for types in referenced assemblies

One known bug remains open: [Existing] on a nullable FileInfo?/DirectoryInfo? fires its existence check even when the value is absent (null), causing ArgumentNullException inside the generated runner.


Test plan

  • dotnet build — 0 errors, 0 warnings
  • docs-builder --help — root help, no startup logs, (default: build) alias shown
  • docs-builder build --help — all options have descriptions, [existing] on --path
  • docs-builder assembler --help — namespace summaries for all sub-namespaces
  • docs-builder assembler index --help — ES options show [schemes: http|https], [time-span-range], [range] hints
  • docs-builder assembler deploy apply --help--plan-file shows [existing]
  • docs-builder assembler deploy apply staging bucket missing.json — argh rejects with "path does not exist"
  • docs-builder index --endpoint not-a-url — argh rejects with scheme error
  • docs-builder build --bootstrap-timeout bad — argh rejects with time-span error
  • docs-builder codex --help — codex concept explained in namespace summary
  • docs-builder changelog --help — all sub-command summaries are one terse line

Mpdreamz and others added 12 commits April 29, 2026 08:58
…rk to Nullean.Argh 0.7.0

Replaces ConsoleAppFramework with Nullean.Argh.Hosting in docs-builder and
Nullean.Argh (standalone) in the aspire AppHost. Key improvements: proper
namespace hierarchy (assembler, codex, etc.), record binding via [AsParameters]
for the shared ElasticsearchIndexOptions, ICommandMiddleware replacing filter
chain, GlobalCliOptions with --log-level/--config-source/--skip-private-repositories
as typed global flags, and docs-builder assemble as a hoisted root command.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Create IsolatedBuildOptions, AssemblerBuildOptions, and AssemblerCloneOptions
records in the service layer. Update all six affected service methods to accept
these DTOs and the existing ElasticsearchIndexOptions directly, eliminating the
tuple-based state unpacking in ServiceInvoker lambdas. Commands are now thin
wrappers that construct a DTO and pass it straight through.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…m> in DTOs

- Bump Nullean.Argh + Nullean.Argh.Hosting to 0.8.0
- Delete ExporterParser: IReadOnlySet<Exporter>? now bound natively by argh
- IsolatedBuildOptions and AssemblerBuildOptions include Exporters via repeated
  flags (--exporters Html --exporters Elasticsearch); [CollectionSyntax(Separator=",")]
  deferred until argh fixes [AsParameters] + IReadOnlySet<Enum> interaction
- IsolatedBuildCommand.Build and AssemblerCommands.Build use [AsParameters] DTO
- [CommandName("build")] + [DefaultCommand] ready for MapAndRootAlias once the
  generator bug (missing { on subsequent methods) is fixed in argh
- Map<IsolatedBuildCommand>() used in place of MapAndRootAlias<> for now

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…s-builder build

- Bump to 0.8.1 (fixes MapAndRootAlias generator bug — missing { on subsequent methods)
- Switch Map<IsolatedBuildCommand>() → MapAndRootAlias<IsolatedBuildCommand>():
  docs-builder build is now both a named command and the root default alias
- IsolatedBuildOptions and AssemblerBuildOptions include IReadOnlySet<Exporter>?
  (repeated flags: --exporters Html --exporters Elasticsearch)
- [CollectionSyntax(Separator=",")] deferred: [AsParameters] + [CollectionSyntax]
  still emits a stray closing brace in 0.8.1 generated code

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Bump to 0.9.0 (fixes [AsParameters] + [CollectionSyntax] stray-brace generator bug)
- Re-enable [CollectionSyntax(Separator=",")] on IsolatedBuildOptions.Exporters
  and AssemblerBuildOptions.Exporters: --exporters Html,Elasticsearch now works
- Add ArghApp.TryArghIntrinsicCommand(args) pre-host fast path: --help, --version,
  __schema and __completion no longer trigger host construction or startup logs
- Nullean.Argh.Interfaces added to Isolated and Assembler service projects

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ElasticsearchIndexOptions:
- Endpoint/ProxyAddress: string? -> Uri? with [UriScheme("http","https")]
- BootstrapTimeout: int? (minutes) -> TimeSpan? with [TimeSpanRange("1s","60m")]
- [Range] on SearchNumThreads, IndexNumThreads, BufferSize, MaxRetries
- Rename NoAiEnrichment -> AiEnrichment, NoEis -> Eis (positive flags,
  clean --no-ai-enrichment / --no-eis negation; removes double-negative)
- Full XML docs on all properties

XML documentation:
- IsolatedBuildOptions, AssemblerBuildOptions: XML docs on all properties
- Namespace class summaries and remarks: assembler (navigation.yml unified site),
  codex (independent per-set navigation), inbound-links (link registry concept)
- All assembler, codex, and inbound-links commands: complete <summary> and
  <remarks> explaining concepts (assembler, codex, link registry, bloom filter),
  ordering requirements, and usage examples
- Root commands (build, index, diff): <summary> tightened, <remarks> added

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…pace summaries

- ChangelogCommands: add class-level summary; rewrite all method summaries to
  one terse sentence; move detail to <remarks>; remove <code> invocation blocks
- All commands: remove <code> CLI invocation examples from <remarks> throughout
- GlobalCliOptions: remove manual enum listing from --log-level summary (argh
  renders allowed values automatically)
- AssemblerBuildOptions, IsolatedBuildOptions: add XML docs to properties
  (visible in IDE; argh cross-assembly doc limitation tracked separately)
- Nested assembler namespaces: add class-level <summary> to BloomFilterCommands,
  ConfigurationCommand, ContentSourceCommands, DeployCommands, NavigationCommands
  so they appear in assembler --help listings
- ServeCommand, MoveCommand, FormatCommand, IndexCommand: tighten summaries and
  move detail to <remarks> without code blocks

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
… docs from referenced projects

PR #25 in 0.9.1 adds XML doc fallback for [AsParameters] DTO members in
referenced assemblies. Enable GenerateDocumentationFile on Isolated,
Assembler, and Configuration service projects so argh's generator can
read property summaries from the produced .xml files.

Result: docs-builder build --help, assembler build --help, assembler
index --help etc. now show full descriptions for every [AsParameters]
DTO property (IsolatedBuildOptions, AssemblerBuildOptions,
ElasticsearchIndexOptions).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…alBaseUrl as Uri?

- IsolatedBuildOptions.CanonicalBaseUrl: string? -> Uri? with [Url]
  (argh maps [Url] on Uri? to [UriScheme("http","https")] constraint)
- IsolatedBuildService.Build: remove manual Uri.TryCreate; use the
  already-validated Uri? from argh binding with ?? default fallback
- ElasticsearchIndexOptions.Endpoint/ProxyAddress: [UriScheme("http","https")]
  -> [Url] for brevity (equivalent per argh generator)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ectoryInfo

- ElasticsearchIndexOptions.CertificatePath: string? -> FileInfo? [FileExtensions("pem,der,crt,cer")]
- IsolatedBuildOptions.Path, Output: string? -> DirectoryInfo?
- Codex config params (all four commands): string -> FileInfo [FileExtensions("yml,yaml")]
- Codex output params: string? -> DirectoryInfo?
- DeployCommands: planFile -> FileInfo [json], @out -> FileInfo?, redirectsFile -> FileInfo? [json]
- CodexUpdateRedirectsCommand.redirectsFile: string? -> FileInfo? [json]
- NavigationCommands.ValidateLinkReference file: string? -> FileInfo? [json]
- InboundLinkCommands.ValidateLinkReference file: string? -> FileInfo? [json]
- BloomFilterCommands.Create builtDocsDir: string -> DirectoryInfo
- AssemblerCommands.Serve path: string? -> DirectoryInfo?
- ServeCommand path: string? -> DirectoryInfo?
- ChangelogCommand.Init path/changelogDir/bundlesDir: string? -> DirectoryInfo?
- ChangelogCommand config params (Add, Bundle, Remove, Render, GhRelease, Upload, EvaluatePr): string? -> FileInfo? [yml,yaml]
- ChangelogCommand directory params: string? -> DirectoryInfo?
- ChangelogCommand.BundleAmend bundlePath: string -> FileInfo [yml,yaml]
- Help text: path/file/dir params now show as <dir>, <file>, <path> with extension hints

Tilde expansion report (params needing future argh [ExpandTilde] attribute):
changelog init path/changelogDir/bundlesDir, all config params, all directory
params, assembler deploy planFile/@out, CertificatePath, build Path/Output.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…tSymbolicLinks], [ExpandUserProfile]

- Bump Nullean.Argh, Nullean.Argh.Hosting, Nullean.Argh.Interfaces to 0.11.0
- [Existing] on all input FileInfo/DirectoryInfo params that must be present:
  CertificatePath, all codex config files, changelog config/bundlePath, deploy
  planFile/redirectsFile, navigation/inbound-links file, bloom-filter builtDocsDir,
  serve path params, IsolatedBuildOptions.Path (source dir must exist)
- [RejectSymbolicLinks] on every FileInfo/DirectoryInfo param and property
- [ExpandUserProfile] on every FileInfo/DirectoryInfo param and property
  (fixes ~/path expansion for all path arguments)
- IsolatedBuildOptions.Output intentionally omitted from [Existing] (created by build)
- Codex and changelog output directories omitted from [Existing] (created by commands)

Help output now shows: [existing] [no symlinks] [expand ~ profile] on appropriate args.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Fixes:
- PR #28: DTO XML summaries now render in --help; global options show short
  flag first (e.g. -l, --log-level)
- PR #29: enum choices listed lowercase in help (html, elasticsearch, ...)
- PR #30: [Existing] on optional nullable FileInfo?/DirectoryInfo? no longer
  throws ArgumentNullException when the flag is omitted
- PR #31: MapAndRootAlias root help and leading flag handling fixed

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…Parameters]

Argh 0.12.0 correctly handles IReadOnlySet<T> in [AsParameters] DTOs but
initialises the collection to an empty HashSet (not null) when no --exporters
flags are supplied. The previous `??=` null-coalesce never fired on an empty
set, so IsolatedBuildService and AssemblerBuildService ran with zero exporters
(producing no output, no links.json).

Replace `??=` with an explicit count-based guard in both services.

Also enable [CollectionSyntax(Separator=",")] on IsolatedBuildOptions.Exporters
now that the [AsParameters] + [CollectionSyntax] generator bug is fixed in 0.12.0.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…ed code

0.12.1 (PR #33) fixed IReadOnlySet<T>? defaulting to null when omitted in
[AsParameters] DTOs, but the generator emits the nullable null-return into a
non-nullable typed local variable (CS8600 / missing ? on the declaration),
making the project unbuildable. 0.12.2 does not fix this.

Pin to 0.12.0 and keep the is not { Count: > 0 } guard as a workaround until
the generator bug is resolved upstream. Remove [CollectionSyntax] comments
from DTOs (they were only relevant to the broken [AsParameters]+[CollectionSyntax]
combination which is now fixed in 0.12.0 anyway).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Fixes the CS8600 generator bug introduced in 0.12.1 where IReadOnlySet<T>? in
[AsParameters] DTOs was emitted with a non-nullable local type. Remove the
is not { Count: > 0 } workaround — ??= is correct again. Re-enable
[CollectionSyntax(Separator=",")] on IsolatedBuildOptions and AssemblerBuildOptions.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@Mpdreamz Mpdreamz requested a review from a team as a code owner April 29, 2026 16:06
@Mpdreamz Mpdreamz requested a review from cotti April 29, 2026 16:06
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

Warning

Rate limit exceeded

@Mpdreamz has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 22 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ad8f01ff-cc47-4d1a-aae3-d5a9bb02bae3

📥 Commits

Reviewing files that changed from the base of the PR and between 6045e51 and f01c04b.

📒 Files selected for processing (51)
  • Directory.Packages.props
  • aspire/AppHost.cs
  • aspire/aspire.csproj
  • src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj
  • src/Elastic.Documentation.Configuration/ElasticsearchEndpointConfigurator.cs
  • src/Elastic.Documentation.ServiceDefaults/AppDefaultsExtensions.cs
  • src/Elastic.Documentation/GlobalCommandLine.cs
  • src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildOptions.cs
  • src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs
  • src/services/Elastic.Documentation.Assembler/Building/AssemblerSitemapService.cs
  • src/services/Elastic.Documentation.Assembler/Elastic.Documentation.Assembler.csproj
  • src/services/Elastic.Documentation.Assembler/Indexing/AssemblerIndexService.cs
  • src/services/Elastic.Documentation.Assembler/Sourcing/AssemblerCloneOptions.cs
  • src/services/Elastic.Documentation.Assembler/Sourcing/AssemblerCloneService.cs
  • src/services/Elastic.Documentation.Isolated/Elastic.Documentation.Isolated.csproj
  • src/services/Elastic.Documentation.Isolated/IsolatedBuildOptions.cs
  • src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs
  • src/services/Elastic.Documentation.Isolated/IsolatedIndexService.cs
  • src/tooling/docs-builder/Arguments/ExportOption.cs
  • src/tooling/docs-builder/Arguments/ProductInfoParser.cs
  • src/tooling/docs-builder/Commands/Assembler/AssemblerCommands.cs
  • src/tooling/docs-builder/Commands/Assembler/AssemblerIndexCommand.cs
  • src/tooling/docs-builder/Commands/Assembler/AssemblerSitemapCommand.cs
  • src/tooling/docs-builder/Commands/Assembler/BloomFilterCommands.cs
  • src/tooling/docs-builder/Commands/Assembler/ConfigurationCommands.cs
  • src/tooling/docs-builder/Commands/Assembler/ContentSourceCommands.cs
  • src/tooling/docs-builder/Commands/Assembler/DeployCommands.cs
  • src/tooling/docs-builder/Commands/Assembler/NavigationCommands.cs
  • src/tooling/docs-builder/Commands/ChangelogCommand.cs
  • src/tooling/docs-builder/Commands/Codex/CodexCommands.cs
  • src/tooling/docs-builder/Commands/Codex/CodexIndexCommand.cs
  • src/tooling/docs-builder/Commands/Codex/CodexUpdateRedirectsCommand.cs
  • src/tooling/docs-builder/Commands/DiffCommands.cs
  • src/tooling/docs-builder/Commands/FormatCommand.cs
  • src/tooling/docs-builder/Commands/InboundLinkCommands.cs
  • src/tooling/docs-builder/Commands/IndexCommand.cs
  • src/tooling/docs-builder/Commands/IsolatedBuildCommand.cs
  • src/tooling/docs-builder/Commands/MoveCommand.cs
  • src/tooling/docs-builder/Commands/ServeCommand.cs
  • src/tooling/docs-builder/DocumentationTooling.cs
  • src/tooling/docs-builder/Filters/InfoLoggerFilter.cs
  • src/tooling/docs-builder/Filters/ReplaceLogFilter.cs
  • src/tooling/docs-builder/Filters/StopwatchFilter.cs
  • src/tooling/docs-builder/GlobalCliOptions.cs
  • src/tooling/docs-builder/Http/InMemoryBuildState.cs
  • src/tooling/docs-builder/Middleware/CatchExceptionMiddleware.cs
  • src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs
  • src/tooling/docs-builder/Middleware/InfoLoggerMiddleware.cs
  • src/tooling/docs-builder/Middleware/StopwatchMiddleware.cs
  • src/tooling/docs-builder/Program.cs
  • src/tooling/docs-builder/docs-builder.csproj
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feature/argh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 6 minutes and 22 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant