Skip to content

Export canonical TypeScript API data from the CLI - #19032

Draft
Adam Ratzman (adamint) wants to merge 70 commits into
microsoft:mainfrom
adamint:adamint/versioned-ts-api-docs
Draft

Export canonical TypeScript API data from the CLI#19032
Adam Ratzman (adamint) wants to merge 70 commits into
microsoft:mainfrom
adamint:adamint/versioned-ts-api-docs

Conversation

@adamint

@adamint Adam Ratzman (adamint) commented Aug 5, 2026

Copy link
Copy Markdown
Member

Description

Adds aspire sdk export, which emits a deterministic TypeScript API reference as JSON. The command exports Aspire.Hosting at the running CLI SDK version by default, or an exact PackageName@Version supplied with --package.

This is the producer side of fixing #17608: generated TypeScript signatures and the exported documentation model now share the same projector, so optional-parameter shaping, promise wrappers, entry points, DTOs, enums, and declaration fragments cannot drift independently.

The focused implementation includes:

  • TypeScript API projection, schema serialization, and a discovered IApiReferenceExporter.
  • An authenticated RemoteHost RPC and CLI command with JSON on stdout and diagnostics on stderr.
  • Export-only package ownership resolution from restored lib, ref, and runtimes/<rid>/lib assets.
  • Exact requested-package restoration without changing normal AppHost build behavior.

The reduction intentionally excludes source selection, output-file handling, checkout provenance/substitution machinery, compatibility shims, and normal-build manifest expansion.

Contributes to #17608.

Validation

  • Affected TypeScript, RemoteHost, and CLI projects build with zero errors.
  • Focused tests: 15 TypeScript, 27 RemoteHost, 19 CLI command, and 1 exact-package test passed.
  • Direct NuGet-backed scanner proof is blocked in the current execution sandbox because access to /etc/resolv.conf is denied.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

Adam Ratzman (adamint) and others added 6 commits August 5, 2026 16:51
Extract every TypeScript-specific resolution decision out of
AtsTypeScriptCodeGenerator into TypeScriptApiProjector: type mapping,
options flattening, callback shaping, promise wrapping, and fluent return
selection. The generator consumes the resolved model instead of
recomputing those decisions inline, and TypeScriptApiExportWriter
serializes the same model into a schema version 1 canonical export.

Documentation that reconstructs signatures from raw ATS drifts from the
SDK that actually ships (microsoft#17608). Sharing one projection
makes that drift impossible: the generated aspire.mts snapshots are
byte-identical, and a new test asserts every exported declaration appears
verbatim in the corresponding generated public interface.

Ownership is resolved per capability rather than per type, mirroring
AtsContextFilter, so a package that extends another package's resource
documents its own members without republishing the referenced type.
Referenced types contribute opaque declaration fragments keyed by their
real owner, which lets a manifest concatenate packages and type-check
without site-authored shims.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Adds IApiReferenceExporter as an optional companion to ICodeGenerator, so a
language provider can describe the surface it generates without every provider
being forced to. AtsTypeScriptCodeGenerator implements it by building the same
TypeScriptApiProjector code generation uses, which is what keeps documentation
from drifting from emitted source.

RemoteHost exposes it as an authenticated exportApi RPC that resolves the
existing code generator and then requires the optional interface, rather than
introducing a second discovery mechanism. The provider's JSON document is
returned verbatim; the payload schema belongs to the language provider.

Also fixes a reference-closure bug this uncovered. Types owned by the selected
assembly were seeded into AtsContextFilter's included sets directly, so the
"was this newly added?" guard refused to walk their own members. An owned DTO
exposing an enum from a non-Aspire dependency kept the DTO and dropped the
enum, and code generation then failed on the dangling reference -- generateCode
for Aspire.Hosting hit this too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Adds a hidden `aspire sdk export` that asks a scanner AppHost for the canonical
API reference of one package in one language and writes it to stdout. Because
documentation pipelines consume it, stdout carries exactly one JSON document and
every status message goes to stderr, so `aspire sdk export ... > api.json`
produces a usable file.

The package version must be exact. A document published under a range would
describe a different SDK after the next restore, so floating versions are
rejected before restore rather than resolved. With no --package, the command
defaults to Aspire.Hosting at this CLI's own identity version, which is the
whole point: the docs describe the SDK this CLI generates against.

SdkCommandPreparation holds only what dump and export genuinely share --
argument parsing, scanner AppHost setup, exporting-assembly discovery. Dump keeps
its own serialization and is deliberately not routed through the canonical
exporter; a test asserts its payload still has the capabilities shape and no
schemaVersion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
The export contract promises that concatenating a manifest's declaration
fragments type-checks without site-authored shims. Running TypeScript over
real Aspire.Hosting and Aspire.Hosting.Redis exports showed it did not.

Handle types with no generated wrapper class surface in signatures under
their raw handle alias name, but the fragment pass derived a class name
instead, so it declared a symbol nothing referenced and left the referenced
alias undefined. Emit the same alias the generator emits.

The runtime fragment was also missing Handle, InputType and AbortSignal,
and MarshalledHandle was missing $type.

Keep sdk export's own --help reachable: Hidden on a subcommand suppresses
its help output, and the parent sdk command is already hidden.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
The scanner AppHost does not reference the language's code generation
package by default, so the server loaded no generators and every export
failed with "No code generator found for language: typescript". sdk
generate already adds the package for exactly this reason.

Verified end to end against the repo-local AppHost server: exporting
Aspire.Hosting and Aspire.Hosting.Redis now writes schema version 1
documents to stdout with nothing on stderr, and TypeScript type-checks
their combined declaration fragments with no errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
A package that extends a type another package owns emitted its
contribution as a normal interface item: same "interface:{name}" stable
ID as the owning package's item, and owningAssembly pointing at the
extending package. Across a manifest that collides with the real page and
misattributes the type, which the export contract explicitly forbids.

Give these items an augmentation kind, an "augmentation:{name}" ID, and
the type's real owning assembly. The declaration fragments already used
this split; the items now match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19032

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19032"

@github-actions github-actions Bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Aug 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds canonical TypeScript API export through aspire sdk export, sharing projection logic with generated SDK output.

Changes:

  • Adds the export model, serializer, RPC, and CLI command.
  • Fixes ATS reference-closure handling.
  • Adds schema, CLI, RPC, and regression tests.
Show a summary per file
File Description
src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs Uses shared projection and implements export.
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs Centralizes TypeScript API projection.
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiModel.cs Defines the export model.
src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiExportWriter.cs Serializes schema v1.
src/Aspire.TypeSystem/IApiReferenceExporter.cs Adds the exporter contract.
src/Aspire.TypeSystem/ApiReferenceExportOptions.cs Adds package export options.
src/Aspire.TypeSystem/api/Aspire.TypeSystem.cs Updates the generated API baseline.
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs Exposes the export RPC.
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGeneratorResolver.cs Resolves exporters.
src/Aspire.Hosting.RemoteHost/Diagnostics/RemoteHostProfilingTelemetry.cs Adds export telemetry.
src/Aspire.Hosting.RemoteHost/AtsContextFilter.cs Expands referenced type closure.
src/Aspire.Hosting.RemoteHost/Aspire.Hosting.RemoteHost.csproj Grants test internals access.
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs Implements sdk export.
src/Aspire.Cli/Commands/Sdk/SdkCommandPreparation.cs Shares scanner preparation.
src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs Uses shared preparation.
src/Aspire.Cli/Commands/Sdk/SdkCommand.cs Registers the subcommand.
src/Aspire.Cli/Projects/IAppHostRpcClient.cs Adds the client contract.
src/Aspire.Cli/Projects/AppHostRpcClient.cs Implements export RPC invocation.
src/Aspire.Cli/Program.cs Registers command services.
tests/Aspire.Hosting.RemoteHost.Tests/CodeGeneration/ApiReferenceExportTests.cs Tests RPC behavior.
tests/Aspire.Hosting.RemoteHost.Tests/AtsContextFilterTests.cs Covers closure expansion.
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs Tests export parity and declarations.
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/AtsTypeScriptCodeGeneratorTests.ApiExport.verified.json Captures schema output.
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/AtsTypeScriptCodeGeneratorTests.ApiDeclarations.verified.txt Captures declaration fragments.
tests/Aspire.Cli.Tests/Commands/Sdk/SdkExportCommandTests.cs Tests CLI behavior.
tests/Aspire.Cli.Tests/TestServices/FakeAppHostServerSession.cs Extends the fake RPC client.
tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs Registers the command in tests.

Review details

Suppressed comments (3)

src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:128

  • The code generator is restored at the running CLI version rather than the requested package version. Exporting an older/newer package can therefore apply a different release's projection rules (or fail when --source contains only the requested release), so the result is not canonical for the requested Name@Version. Restore the generator at packageVersion, as normal AppHost generation does for its effective SDK version.
            integrations.Add(IntegrationReference.FromPackage(codeGenPackage, ExecutionContext.IdentityVersion));

src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:76

  • This command promises that every human-readable message goes to stderr, but it never switches InteractionService.Console from its default stdout route. As a result, preparation diagnostics and the --output success message can be written to stdout. Route the command's human output to stderr before any discovery or preparation work.
        var language = parseResult.GetValue(s_languageOption)!;

src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:119

  • An explicit Aspire.Hosting@Version is never restored here. Both scanner implementations ignore the sdkVersion argument, so a CLI running a different version scans its bundled/repository Aspire.Hosting assembly and then labels that surface with the requested version. The command therefore cannot honor its exact Name@Version contract for the core package; the scanner must load the requested core package version rather than skipping it.
            if (!string.Equals(packageName, CorePackageName, StringComparison.OrdinalIgnoreCase))
            {
                integrations.Add(reference);
            }
  • Files reviewed: 25/27 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment thread src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs Outdated
Comment thread src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs Outdated
Comment thread src/Aspire.Cli/Commands/Sdk/SdkCommandPreparation.cs Outdated
Adam Ratzman (adamint) and others added 4 commits August 5, 2026 18:44
The generated capability scanner writes a Directory.Packages.props that turns central package
management on, which rejects an inline Version attribute with NU1008. Any integration outside
the repo failed to build, so sdk export could not be pointed at a third-party package.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Ownership only transfers to PreparedSdkSession once one is returned. A throw from StartAsync or
GetRpcClientAsync left the spawned scanner running and holding the temp directory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Fragments built from raw string literals carry whatever line endings the source was checked out
with, so a Windows build disagreed with a Linux build about identical declarations. Consumers
deduplicate a manifest by comparing content for the same ID, so the text has to be stable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Every integration that extends DistributedApplicationBuilder produced the same augmentation item
ID, which recreated the cross-package collision the augmentation kind exists to avoid, so the
contributing package is now part of the ID.

CreateBuilderOptions also gained its client-only throwOnPendingRejections property from the module
emitter alone, so the export described a smaller interface than the one we ship. That list moved
to the projector and both paths read it from there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Copilot AI review requested due to automatic review settings August 5, 2026 22:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (3)

src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs:276

  • This hardcodes the generated options-bag parameter name to options, while the source emitter calls GetPublicOptionsParameterName to avoid collisions. A capability that already has a parameter named options is therefore exported with a different—or even duplicate and invalid—signature from the generated .mts, defeating the canonical-source contract. Resolve and pass the collision-safe name here too.
        var parameterList = BuildPublicParameterList(
            requiredParams,
            hasOptionals,
            optionsTypeName,
            trailingCancellationToken: GetTrailingCancellationTokenParameter(optionalParams));

src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs:744

  • The structured parameters array is still built from raw ATS parameters instead of the resolved public signature. For example, the checked-in export declares withOptionalString(options?: WithOptionalStringOptions) but reports value and enabled as its parameters. Any consumer rendering the structured fields will reconstruct the same stale positional API this PR is intended to eliminate. Build this list from the required parameters plus the resolved options-bag and trailing cancellation-token parameters.
        var parameters = capability.Parameters
            .Where(p => builderModel is null || p.Name != targetParamName)
            .Select(p => new TypeScriptApiParameter
            {
                Name = p.Name,
                DeclaredType = MapParameterToTypeScript(p),
                IsOptional = p.IsOptional || p.IsNullable,
                Summary = p.Documentation?.Summary
            })

src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:194

  • The new CLI command has only fake project/session/RPC tests; there is no CLI end-to-end test that starts the real scanner and validates the machine-output contract. That leaves package/codegen restoration, RPC registration, and stdout/stderr separation untested—the exact integration points this command introduces. Add a Hex1b CLI E2E test using a local-hive package that invokes sdk export, redirects stdout, parses the JSON, and verifies stderr remains separate.
        await using var session = await SdkCommandPreparation.PrepareSessionAsync(
            _appHostServerProjectFactory,
            _serverSessionFactory,
            InteractionService,
            _logger,
            "aspire-sdk-export-",
            sdkVersion,
            integrations,
            packageSource,
            cancellationToken);
  • Files reviewed: 27/29 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs Outdated
The scanner never honored a requested core version. Both PrepareAsync
implementations accept sdkVersion and ignore it, so
`sdk export --package Aspire.Hosting@13.0.0` returned byte-identical JSON
to a 13.5.0-dev export, relabelled 13.0.0. That is the stale-signature
problem this command exists to fix.

Honoring the request is not possible: the prebuilt server bundles core and
the RPC host compiled together, so loading a foreign core would break the
RPC contract. Reject the skew instead, with a message pointing at the CLI
that can produce the requested export.

Integration packages are unaffected: they become real PackageReferences and
already resolve to the requested version.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Copilot AI review requested due to automatic review settings August 5, 2026 23:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (3)

src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:135

  • A non-core Aspire package can request any version here, but the scanner and TypeScript code-generation package are still pinned to the running CLI's IdentityVersion. Thus a 13.6 CLI exporting Aspire.Hosting.Redis@13.5.0 applies the 13.6 signature-shaping generator and labels the result as 13.5.0, recreating the version drift this command is intended to prevent. Either restore/run the matching SDK and generator or reject official Aspire.Hosting.* versions that differ from IdentitySdkVersion, as is already done for core.
            else
            {
                integrations.Add(reference);
            }

src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs:276

  • ResolveMethodSignature always names the synthesized optional-parameter bag options, but the source emitter calls GetPublicOptionsParameterName to avoid collisions. Real generated APIs such as addCSharpApp and promptProgress therefore use optionsBag, while this export reports options, reintroducing the docs/generated-signature drift this PR is intended to remove. Compute and pass the same collision-safe name here.
        var parameterList = BuildPublicParameterList(
            requiredParams,
            hasOptionals,
            optionsTypeName,
            trailingCancellationToken: GetTrailingCancellationTokenParameter(optionalParams));

src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.cs:740

  • The structured parameters array is built from the original ATS parameters rather than the public signature. For example, the snapshot declares addTestRedis(name, options?: AddTestRedisOptions) but serializes name and port, with no options parameter. Consumers using these schema fields will reconstruct the same stale positional API this export is meant to eliminate. Build this list from the required public parameters plus the synthesized options bag (and any separately emitted cancellation token).
        var parameters = capability.Parameters
            .Where(p => builderModel is null || p.Name != targetParamName)
            .Select(p => new TypeScriptApiParameter
            {
                Name = p.Name,
  • Files reviewed: 27/29 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs
The command always emits JSON, so it has no --format option for BaseCommand's
json redirect to key off. Preparation diagnostics and the --output success
message therefore went to stdout and corrupted the document when a caller
piped it. DisplaySuccess takes no per-call override, so it could not opt out.

Route the interaction service to stderr at command entry. The JSON write
already overrides back to stdout explicitly, and an explicit override wins
over the service setting.

SdkExportSendsProgressToStderrOnly asserted on the per-call override, which is
null for these calls, so it passed vacuously. It now resolves the effective
destination and covers the --output path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 819baaf4-91c3-44ce-8004-3dbc9110f810
Copilot AI review requested due to automatic review settings August 5, 2026 23:39
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (5)

tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/PriorContractBindingTests.cs:1

  • This test records only the simple type name (TypeReference.Name) and member name. If Aspire.TypeSystem ever contains two types with the same simple name in different namespaces, the guard can produce false negatives/positives. Consider including the namespace (or full name) in TypeSystemMemberReference and matching against a baseline representation that’s also unambiguous (e.g., parsing namespace blocks + type names, or using a GenAPI-like full identifier format).
    tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/WithDataVolumeOptionsMerged.verified.ts:1
  • This line appears to include a leading UTF-8 BOM character (U+FEFF) before export. If this is accidental (e.g., introduced by an editor/encoding change), it can create hard-to-spot diffs and inconsistent snapshot behavior across environments. Consider normalizing snapshot encodings (no BOM) and ensuring generated output snapshots don’t depend on BOM presence.
    src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGeneratorResolver.cs:169
  • GetLoadableTypes(...) logs a Warning on ReflectionTypeLoadException. It’s called from both generator discovery and exporter discovery, so a single type-load failure will be logged twice (once per pass), contradicting the comment that the failure “was already reported by DiscoverGenerators.” Consider caching the loadable type list per assembly (or adding a logFailures/alreadyLogged flag) so exporter discovery can reuse the prior result without re-logging.
        {
            var assemblyName = assembly.GetName().Name;

            // An assembly with no exporter is the normal case (most languages generate code they
            // cannot yet describe), so unlike generator discovery this pass never warns about
            // finding nothing. A type-load failure was already reported by DiscoverGenerators.
            foreach (var type in GetLoadableTypes(assembly, assemblyName, out _))
            {
                if (type.IsAbstract || type.IsInterface || !typeof(IApiReferenceExporter).IsAssignableFrom(type))
                {
                    continue;
                }

src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs:586

  • GetLocalProjectSubstitution enumerates all src/* directories on every call. In CreateProjectFile, this is invoked per integration, making substitution checks O(integrations × srcDirs). Consider caching a case-insensitive map of { packageName -> projectPath } (or caching just the src directory list) for the lifetime of the DotNetBasedAppHostServerProject instance to avoid repeated enumeration.
    public LocalProjectSubstitution? GetLocalProjectSubstitution(string packageName)
    {
        if (!packageName.StartsWith("Aspire.Hosting", StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }

        var srcPath = Path.Combine(_repoRoot, "src");
        if (!Directory.Exists(srcPath))
        {
            return null;
        }

        // The directory listing settles the spelling on every platform. Probing
        // Path.Combine(src, packageName) instead would hand back the caller's spelling wherever
        // File.Exists is case-insensitive, so the same request produced two different project paths
        // depending on the filesystem. Enumerate and compare rather than passing the caller's name
        // as a search pattern, so a package id that happens to contain a wildcard cannot match a
        // directory it does not name.
        //
        // Enumerating can throw where the old File.Exists probe could not, and this runs on the
        // `aspire run` path via CreateProjectFile, so an unreadable or concurrently removed src/
        // reports "no substitution" the same way GetRepositoryVersionPrefix does rather than
        // costing the caller its scanner.
        try
        {
            foreach (var directory in Directory.EnumerateDirectories(srcPath))
            {
                var canonicalName = Path.GetFileName(directory);
                if (!string.Equals(canonicalName, packageName, StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                var projectPath = Path.Combine(directory, $"{canonicalName}.csproj");
                return File.Exists(projectPath)
                    ? new LocalProjectSubstitution(projectPath, GetRepositoryVersionPrefix())
                    : null;
            }

src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiExportWriter.cs:18

  • The remarks say the writer “drops empty collections,” but the implementation always emits the top-level modules and declarations arrays (even if empty). If the schema requires these properties, the comment should be narrowed (e.g., “drops empty optional collections like members, examples, …”) to avoid misleading contract readers.
/// The document is written by hand rather than through reflection-based serialization because the
/// shape is a published contract that documentation sites bind to. Writing it explicitly makes the
/// contract reviewable in one place, keeps property order stable so exports diff cleanly between SDK
/// versions, and drops empty collections and null strings so a version bump only shows real API
/// changes.
/// </remarks>
  • Files reviewed: 65/68 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Adam Ratzman added 3 commits August 10, 2026 16:20
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (4)

src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs:385

  • This package-to-assembly lookup is unavailable in repository/dev mode: DotNetBasedAppHostServerProject neither creates a probe manifest nor sets ASPIRE_INTEGRATION_PROBE_MANIFEST_PATH. Consequently this branch is always skipped there, and packages whose ID differs from their assembly name—or that own multiple assemblies—still fail or export only the ID-matching assembly. Generate equivalent ownership metadata for the dotnet-based scanner, not only the prebuilt scanner.
        if (_assemblyLoader.TryGetPackageAssemblyNamesFromProbePaths(
            packageName,
            packageVersion,
            out var manifestAssemblyNames))

src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs:404

  • The probe-path branch preserves the caller's casing as the exported package identity. Thus --package contoso.aspire.widgets@... successfully maps case-insensitively but emits contoso.aspire.widgets, despite this code's stated requirement that consumers receive the canonical package ID. Carry the restored package ID through the probe metadata and assign that canonical value here.
            canonicalPackageName = packageName;
            return exportingAssemblyNames;

src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:107

  • C# is returned by language discovery with an empty code-generation package name. This condition accepts the empty string and IntegrationReference.FromPackage then throws before the RPC can report that C# has no API exporter. Treat an empty package name as absent so unsupported C# requests reach the structured InvalidParams path.
            if (codeGenerationPackage is not null &&
                !integrations.Any(integration =>
                    integration.Name.Equals(codeGenerationPackage, StringComparison.OrdinalIgnoreCase)))

src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:273

  • SemVersion is not the NuGet version grammar, so this rejects valid exact NuGet versions such as 1.2.3.4. NuGet explicitly supports a fourth Revision segment and recommends NuGetVersion.Parse for package versions. Parse and normalize with NuGet.Versioning.NuGetVersion so every exact version accepted by restore is accepted by this command.
        if (requestedVersion.Any(char.IsWhiteSpace) ||
            !SemVersion.TryParse(requestedVersion, SemVersionStyles.Any, out var parsedVersion))
        {
            errorMessage = $"Invalid version '{requestedVersion}'. Expected an exact NuGet version.";
  • Files reviewed: 25/27 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (3)

src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs:187

  • Routing an exact range through this branch still adds it to otherPackages, which is emitted as PackageReference Version="[x]". This generated project enables central package management, so repository-mode restores reject the reference with NU1008 before the export RPC can run. Use VersionOverride for these explicit versions and make the regression test perform a restore rather than only inspecting the XML.
            // An exact range is an explicit request to restore that package, used by sdk export so
            // the document cannot be labelled with a package version while describing checkout code.
            else if (integration.Name.StartsWith("Aspire.Hosting", StringComparison.OrdinalIgnoreCase) &&
                     !IsExactVersionRange(integration.Version))

src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs:345

  • Filtering to one package before constructing the projector makes options-interface collision resolution package-local. For example, Event Hubs and Service Bus can each export an incompatible runAsEmulator bag as RunAsEmulatorOptions, while full TypeScript generation sees both and suffixes one; concatenating these exports then conflicts and the documented signatures no longer match generated bindings. Options names must be deterministic from assembly/capability identity, or be resolved against the full manifest before documentation is scoped.
            var context = AtsContextFilter.FilterForApiExport(
                fullContext,
                exportingAssemblyNames);

src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs:403

  • This manifest-backed path preserves the caller's casing instead of a canonical package ID. NuGet package IDs are case-insensitive and the global-packages path used for ownership is lowercased, so --package aspire.hosting.redis@... succeeds but emits package.name = "aspire.hosting.redis"; consumers keyed by the published Aspire.Hosting.Redis identity will not find it. Carry the canonical package ID from restore metadata into the probe manifest and use it here.
            canonicalPackageName = packageName;
  • Files reviewed: 25/27 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c235246b-d021-4d8e-b041-cc4984674ebe

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs:403

  • This branch preserves the caller's package-ID casing even though the fallback immediately below canonicalizes it to avoid publishing an identity consumers cannot find. For example, --package contoso.aspire.widgets@1.2.3 maps successfully because all comparisons are case-insensitive, but the JSON still records contoso.aspire.widgets. Carry the canonical package ID from restore/probe metadata and use it here; the assembly name is not a safe substitute because package and assembly names may differ.
            canonicalPackageName = packageName;

src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:274

  • This validates a NuGet package version with a SemVer parser, but NuGet's version grammar is broader: for example, 1.2.3.4 is a valid exact NuGet version (Revision is the fourth component) and is rejected here before restore. Parse and normalize with NuGet.Versioning.NuGetVersion so every exact version accepted by NuGet can be exported.
        if (requestedVersion.Any(char.IsWhiteSpace) ||
            !SemVersion.TryParse(requestedVersion, SemVersionStyles.Any, out var parsedVersion))
        {
            errorMessage = $"Invalid version '{requestedVersion}'. Expected an exact NuGet version.";
            return false;
  • Files reviewed: 25/27 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Adam Ratzman and others added 4 commits August 10, 2026 22:57
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs:187

  • Using an exact NuGet range as the signal for export mode changes normal repository behavior too. aspire.config.json and legacy settings pass arbitrary explicit version strings through IntegrationReference.FromPackage (AspireConfigFile.cs:356-383, AspireJsonConfiguration.cs:196-223), so a normal package entry such as "Aspire.Hosting.Redis": "[13.1.0]" now bypasses the checkout project and restores NuGet instead. This contradicts the PR's stated export-only behavior. Carry an explicit “disable local project substitution” flag from SdkExportCommand rather than inferring the command mode from version syntax.
            // An exact range is an explicit request to restore that package, used by sdk export so
            // the document cannot be labelled with a package version while describing checkout code.
            else if (integration.Name.StartsWith("Aspire.Hosting", StringComparison.OrdinalIgnoreCase) &&
                     !IsExactVersionRange(integration.Version))

src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:122

  • The new command's only CLI tests replace both IAppHostServerProject and IAppHostRpcClient, so they never exercise the behavior introduced here: exact NuGet restore, launching the real RemoteHost, discovering the exporter, and preserving the stdout/stderr contract across the subprocess boundary. There is also no sdk export coverage in Aspire.Cli.EndToEnd.Tests. Add a CLI end-to-end test using a local/offline package source that invokes this command and parses stdout as JSON; otherwise the primary user path can fail while all current tests pass.
        var exitCode = await ExportApiAsync(
            languageInfo?.CodeGenerator ?? language,
            packageName,
            packageVersion,
            integrations,
            cancellationToken);
  • Files reviewed: 31/33 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Adam Ratzman added 2 commits August 14, 2026 18:02
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821
Preserve normal repository substitution for exact version ranges and cover the real export subprocess against the installed package hive.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptApiReferenceExporter.cs:51

  • A pre-canceled request still constructs TypeScriptApiProjector, whose constructor resolves the entire ATS context, before BuildApiModel observes the token. Large exports therefore do substantial work after cancellation and can delay RPC shutdown. Check the token before starting projection; cancellation during Resolve may also need to be threaded through separately.
        // Build the projector from the same context the generator would use, so the exported
        // documentation describes the exact signatures generation would emit rather than a
        // second, independently derived reading of the ATS context.
        var projector = new TypeScriptApiProjector(context);
  • Files reviewed: 33/35 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Check cancellation before resolving the TypeScript projection and cover the ordering explicitly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs:72

  • IdentitySdkVersion can be supplied by ASPIRE_CLI_VERSION, but both scanner project implementations ignore the sdkVersion passed to PrepareAsync and load the physical CLI/checkout core assemblies. An override therefore makes the default command export the current surface while labeling it as another SDK version—the stale-signature failure this command is meant to prevent. Validate against the physical bundled SDK version (while allowing an equivalent install-sidecar identity) before exporting core.
        var packageName = CorePackageName;
        var packageVersion = ExecutionContext.IdentitySdkVersion;
  • Files reviewed: 33/35 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/Aspire.Hosting.RemoteHost/AssemblyLoader.cs Outdated
Comment thread src/Aspire.Hosting.RemoteHost/AtsContextFilter.cs
Adam Ratzman added 3 commits August 14, 2026 19:29
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d65b598f-72dd-4fca-bdc4-8debbbd7f821

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 34/36 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

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

Labels

needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants