Skip to content

Add Java hosting integration - #18033

Open
Marshal Hayes (marshalhayes) wants to merge 169 commits into
microsoft:mainfrom
marshalhayes:java-integration
Open

Add Java hosting integration#18033
Marshal Hayes (marshalhayes) wants to merge 169 commits into
microsoft:mainfrom
marshalhayes:java-integration

Conversation

@marshalhayes

@marshalhayes Marshal Hayes (marshalhayes) commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Description

This pull request adds Aspire.Hosting.Java, ported from CommunityToolkit.Aspire.Hosting.Java and then reworked for parity with the other resource languages (Rust, Go, Node, Python).

The following features have been implemented:

  • Running an existing jar (e.g. java -jar app.jar)
  • Building a jar and running it (e.g. java -jar ...)
  • Building and running with mvnw (e.g. mvnw spring-boot:run)
  • Building and running with gradlew (e.g. gradlew bootRun)
  • VS Code debugger support
  • Certificate trust
  • Publishing to Docker Compose

Added beyond the original scope:

  • AddSpringBootApp and AddQuarkusApp, which detect the build tool from the build file and wire the framework's own port and OpenTelemetry conventions
  • AddJavaContainer, for running an image built elsewhere
  • Container publishing with a generated multi-stage Dockerfile — JDK release detected from pom.xml/Gradle, unprivileged USER 999:999, JVM as PID 1, BuildKit dependency cache
  • A first-class Java AppHost: flat AppHost.java, or Maven/Gradle at src/main/java/AppHost.java, with AppHost debugging
  • OpenTelemetry Java agent attachment, including an agent produced by the build itself
  • A Java AppHost starter template, and VS Code extension parity (debugging, code lenses, launch configuration)
  • Playground samples for both Spring Boot (playground/JavaSpringBoot) and Quarkus (playground/JavaQuarkus)

Closes #17993

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
    • 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

The security-relevant choices are confined to the generated container image and the build tool it runs, and are documented in the README: the image runs as USER 999:999 with no passwd entry rather than root; a Maven or Gradle wrapper checked into the repository is required and a globally installed mvn/gradle is deliberately never used as a fallback, so the tool version cannot vary with the machine; and paths that would escape the build context are rejected rather than silently copied.

@github-actions

github-actions Bot commented Jun 9, 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 -- 18033

Or

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

@marshalhayes Marshal Hayes (marshalhayes) changed the title Port Community Toolkit Java integration Add Java hosting integration Jun 10, 2026
@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.

# Conflicts:
#	Aspire.slnx
#	extension/src/capabilities.ts
#	extension/src/dcp/types.ts
#	extension/src/debugger/debuggerExtensions.ts
#	src/Aspire.Hosting/Aspire.Hosting.csproj
- ExecutableLaunchConfiguration moved from Aspire.Hosting.Dcp.Model to
  Aspire.Hosting.ApplicationModel and is now public + experimental.
- TestTempDirectory was replaced in main (microsoft#18683); add a project-level
  TempJavaAppDirectory mirroring Aspire.Hosting.Rust.Tests.TempCrateDirectory.
- Drop the InternalsVisibleTo on Aspire.Hosting for Aspire.Hosting.Java: the
  launch configuration base type is public now, so the package builds against
  the public surface only, matching Aspire.Hosting.Rust.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d
…ect command, safe JAVA_TOOL_OPTIONS

- Replace the public mutable JarPath property with a JavaJarPathAnnotation so run,
  publish and debug read one source of truth.
- Set the build tool wrapper as the command in every execution context. Setting it
  only in run mode left publish emitting the uninvokable 'java spring-boot:run'.
- Apply the JAR/build-tool mutual exclusion guard to WithMavenGoal as well as
  WithGradleTask, and reject configuring two build tools.
- Make WithWrapperPath order-independent by re-pointing the command and any build
  step that already resolved the default wrapper.
- Constrain WithJvmArgs/WithOtelAgent to JavaAppResource so they no longer appear
  on every resource builder, and require an agent path for WithOtelAgent.
- Quote JAVA_TOOL_OPTIONS values containing whitespace and merge non-string values
  through ReferenceExpression instead of discarding them.
- Seal the PKCS#12 trust store with an empty password so no secret reaches
  JAVA_TOOL_OPTIONS, and load/dispose certificates with X509CertificateLoader.
- Add WithMainClass and WithJarArtifact, and emit main_class/build_tool in the
  launch configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d
Publishing:
- Add JavaDockerfileGenerator: multi-stage Maven/Gradle container build driven by
  the project's own wrapper, BuildKit dependency cache mounts, JRE runtime stage,
  non-root user, and exec-form ENTRYPOINT so the JVM receives SIGTERM.
- Add JavaVersionDetector: reads the target release from pom.xml or a Gradle build
  script so the build and runtime images match what the project compiles against.
- Deterministic JAR selection that filters -plain/-sources/-javadoc and fails the
  container build on ambiguity rather than producing an image that cannot start.
- Wire PublishAsDockerFile into AddJavaApp, leaving an authored Dockerfile or
  .dockerignore alone.
- JavaBuildStepAnnotation now records the tool and arguments in every execution
  context so the container build reuses what WithMavenBuild/WithGradleBuild
  configured, and repeated calls no longer collide on the build resource name.

VS Code extension:
- java.ts rewritten: workspace-relative session names, validated launch config,
  honours the request kind, sets mainClass, forwards application arguments, keeps
  the merged environment, and treats the Java language server as optional so a
  failed classpath refresh cannot abort the launch.
- isJavaInstalled now requires both redhat.java and vscjava.vscode-java-debug.
- Localize javaDisplayName/javaLabel and add javaDebugger.test.ts (26 tests).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d
The agent path reached JAVA_TOOL_OPTIONS exactly as authored, which is wrong
in both execution modes.

In run mode JAVA_TOOL_OPTIONS is inherited by every JVM started beneath the
resource, and build tools start JVMs from directories other than the
application directory. The Gradle daemon starts from its own distribution
directory, so a relative -javaagent: path failed to resolve and the daemon
died during VM initialization rather than reporting a build failure. Relative
paths are now resolved against the application directory.

In publish mode a relative path names a file the build produces, so it only
existed in the Dockerfile's build stage. The published container started a JVM
pointing at an agent JAR that was not in the image. The runtime stage now
copies the agent forward and the environment variable points at it. Absolute
paths are left alone in publish, since they cannot have come from the build
context.

Reworks playground/JavaSpringBoot into three services covering Maven with
spring-boot:run, Gradle with bootRun, and a plain JAR, and removes 23 MB of
committed build output. Registers it in the solution and gives it a Docker
Compose environment so `aspire publish` has somewhere to publish to.

Verified locally: all six resources reach Running from a clean checkout, the
orders to catalog call produces a distributed trace spanning both services,
and all three published images build and run with the agent loaded and the
process running as uid 999.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d
Replace the 13-line placeholder README with full documentation covering
prerequisites, the three launch modes, build steps, telemetry, VS Code
debugging, and container publishing.

The C# and TypeScript samples were both checked against the real API
surface -- the TypeScript one type-checks against the generated bindings,
where `params string[]` projects as a required array (matching Rust).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d
Both branches of QuoteIfNeeded were verified against a real JVM: an
unquoted -javaagent: path containing a space aborts startup with
"Unrecognized option", and quoting only the value after '=' is what lets
the JVM actually load a trust store from a path with spaces.

Neither behavior was covered, so add tests for both and verify they fail
when the quoting is removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d
Copilot AI balanced review requested due to automatic review settings August 15, 2026 02:57
The scaffolded AppHost is a compact source file with an instance main
method. That form was previewed in Java 21-24 (JEP 445, 463, 477, 495)
and finalized in Java 25 by JEP 512, so on the Java 25 floor the runner
already targets, --enable-preview compiles no preview feature at all.

Verified against the real polyglot fixture: the AppHost plus all 224
generated SDK sources compile with --release 25 and no preview flag, the
class files carry minor version 0 rather than the preview marker 65535,
and the compiled AppHost still resolves its instance main at run time
without --enable-preview on the java command.

Passing the flag is not merely redundant. Any future preview usage would
silently stamp the class files with minor version 65535, which binds them
to one exact JDK release and makes the runtime flag mandatory.

Also switch --source to --release. --source only selects the language
level, leaving the compiler linked against the class library of the
running JDK, so an AppHost could bind to APIs missing from a conforming
Java 25 runtime and fail there. --release constrains the API surface too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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 first-party Java hosting support, including Maven/Gradle/JAR execution, container publishing, certificate trust, polyglot APIs, and VS Code debugging.

Changes:

  • Adds the Aspire.Hosting.Java resource model and publishing support.
  • Adds unit, snapshot, polyglot, and playground coverage.
  • Extends the VS Code extension with Java debugging.
Show a summary per file
File Description
Aspire-Core.slnf Includes Java projects.
Aspire.slnx Registers integration, tests, and playground.
eng/github-ci/test-trigger-map.yml Maps Java integration tests.
src/Aspire.Hosting/Aspire.Hosting.csproj Grants test internals access.
src/Aspire.Hosting.Java/Aspire.Hosting.Java.csproj Defines the package.
src/Aspire.Hosting.Java/JavaAppResource.cs Defines Java resources.
src/Aspire.Hosting.Java/JavaBuildToolAnnotation.cs Tracks build configuration.
src/Aspire.Hosting.Java/JavaDockerfileGenerator.cs Generates deployment Dockerfiles.
src/Aspire.Hosting.Java/JavaHostingExtensions.cs Implements public hosting APIs.
src/Aspire.Hosting.Java/JavaJarPathAnnotation.cs Tracks JAR and main-class settings.
src/Aspire.Hosting.Java/JavaLaunchConfiguration.cs Defines IDE launch metadata.
src/Aspire.Hosting.Java/JavaVersionDetector.cs Detects target Java versions.
src/Aspire.Hosting.Java/GradleBuildResource.cs Models Gradle builds.
src/Aspire.Hosting.Java/MavenBuildResource.cs Models Maven builds.
src/Aspire.Hosting.Java/WrapperAnnotation.cs Tracks wrapper overrides.
src/Aspire.Hosting.Java/README.md Documents the integration.
extension/src/capabilities.ts Advertises Java capability.
extension/src/dcp/types.ts Adds Java launch types.
extension/src/debugger/debuggerExtensions.ts Registers Java debugging.
extension/src/debugger/languages/java.ts Implements Java debugger configuration.
extension/src/loc/strings.ts Adds localized Java labels.
extension/src/test/javaDebugger.test.ts Tests Java debugging.
extension/src/test/strings.test.ts Validates Java localization.
extension/package.nls.json Defines Java localization strings.
extension/loc/xlf/aspire-vscode.xlf Updates generated localization catalog.
playground/JavaSpringBoot/.gitattributes Normalizes wrapper line endings.
playground/JavaSpringBoot/JavaSpringBoot.AppHost/AppHost.cs Orchestrates sample Java apps.
playground/JavaSpringBoot/JavaSpringBoot.AppHost/JavaSpringBoot.AppHost.csproj Defines sample AppHost.
playground/JavaSpringBoot/JavaSpringBoot.AppHost/aspire.config.json Configures sample AppHost.
playground/JavaSpringBoot/JavaSpringBoot.AppHost/Properties/launchSettings.json Configures sample launch profiles.
playground/JavaSpringBoot/catalog/pom.xml Defines Maven catalog app.
playground/JavaSpringBoot/catalog/mvnw Adds POSIX Maven wrapper.
playground/JavaSpringBoot/catalog/mvnw.cmd Adds Windows Maven wrapper.
playground/JavaSpringBoot/catalog/.mvn/wrapper/maven-wrapper.properties Pins Maven distribution.
playground/JavaSpringBoot/catalog/src/main/java/com/example/catalog/CatalogApplication.java Starts catalog service.
playground/JavaSpringBoot/catalog/src/main/java/com/example/catalog/CatalogController.java Implements catalog endpoints.
playground/JavaSpringBoot/orders/build.gradle Defines Gradle orders app.
playground/JavaSpringBoot/orders/settings.gradle Configures Gradle toolchains.
playground/JavaSpringBoot/orders/gradlew Adds POSIX Gradle wrapper.
playground/JavaSpringBoot/orders/gradlew.bat Adds Windows Gradle wrapper.
playground/JavaSpringBoot/orders/gradle/wrapper/gradle-wrapper.jar Supplies Gradle wrapper runtime.
playground/JavaSpringBoot/orders/gradle/wrapper/gradle-wrapper.properties Pins Gradle distribution.
playground/JavaSpringBoot/orders/src/main/java/com/example/orders/OrdersApplication.java Starts orders service.
playground/JavaSpringBoot/orders/src/main/java/com/example/orders/OrdersController.java Implements orders endpoints.
playground/JavaSpringBoot/worker/pom.xml Defines plain-JAR worker.
playground/JavaSpringBoot/worker/src/main/java/com/example/worker/Worker.java Implements background worker.
tests/Aspire.Hosting.Java.Tests/Aspire.Hosting.Java.Tests.csproj Defines integration tests.
tests/Aspire.Hosting.Java.Tests/AddJavaAppTests.cs Tests run-mode behavior.
tests/Aspire.Hosting.Java.Tests/AddJavaAppPublishTests.cs Tests generated publishing artifacts.
tests/Aspire.Hosting.Java.Tests/JavaPublicApiTests.cs Tests resource validation.
tests/Aspire.Hosting.Java.Tests/TempJavaAppDirectory.cs Provides isolated test directories.
tests/Aspire.Hosting.Java.Tests/Snapshots/AddJavaAppPublishTests.VerifyPublish_CopiesABuildProducedOtelAgentIntoTheRuntimeImage.verified.txt Verifies agent copying.
tests/Aspire.Hosting.Java.Tests/Snapshots/AddJavaAppPublishTests.VerifyPublish_DetectsTheBuildToolFromDiskWhenOnlyAJarPathWasGiven.verified.txt Verifies build-tool detection.
tests/Aspire.Hosting.Java.Tests/Snapshots/AddJavaAppPublishTests.VerifyPublish_EmitsABuildContextIgnoreThatExcludesBuildOutputDirectories.verified.txt Verifies ignore rules.
tests/Aspire.Hosting.Java.Tests/Snapshots/AddJavaAppPublishTests.VerifyPublish_GeneratesAGradleBuild.verified.txt Verifies Gradle Dockerfile.
tests/Aspire.Hosting.Java.Tests/Snapshots/AddJavaAppPublishTests.VerifyPublish_GeneratesAMavenBuildAndJreRuntimePair.verified.txt Verifies Maven Dockerfile.
tests/Aspire.Hosting.Java.Tests/Snapshots/AddJavaAppPublishTests.VerifyPublish_HonoursWithDockerfileBaseImage.verified.txt Verifies image overrides.
tests/Aspire.Hosting.Java.Tests/Snapshots/AddJavaAppPublishTests.VerifyPublish_ProducesAContainerManifestEntry.verified.json Verifies container manifest.
tests/Aspire.Hosting.Java.Tests/Snapshots/AddJavaAppPublishTests.VerifyPublish_ReusesTheArgumentsConfiguredForTheHostBuildStep.verified.txt Verifies build arguments.
tests/Aspire.Hosting.Java.Tests/Snapshots/AddJavaAppPublishTests.VerifyPublish_UsesTheWrapperWhenTheProjectShipsOne.verified.txt Verifies wrapper usage.
tests/PolyglotAppHosts/Aspire.Hosting.Java/Go/apphost.go Exercises Go bindings.
tests/PolyglotAppHosts/Aspire.Hosting.Java/Go/aspire.config.json Configures Go validation.
tests/PolyglotAppHosts/Aspire.Hosting.Java/Go/go.mod Defines Go module.
tests/PolyglotAppHosts/Aspire.Hosting.Java/Java/AppHost.java Exercises Java bindings.
tests/PolyglotAppHosts/Aspire.Hosting.Java/Java/aspire.config.json Configures Java validation.
tests/PolyglotAppHosts/Aspire.Hosting.Java/Python/apphost.py Exercises Python bindings.
tests/PolyglotAppHosts/Aspire.Hosting.Java/Python/apphost_requirements.txt Defines Python fallback requirements.
tests/PolyglotAppHosts/Aspire.Hosting.Java/Python/aspire.config.json Configures Python validation.
tests/PolyglotAppHosts/Aspire.Hosting.Java/Python/pylock.apphost.toml Locks Python AppHost module.
tests/PolyglotAppHosts/Aspire.Hosting.Java/TypeScript/apphost.mts Exercises TypeScript bindings.
tests/PolyglotAppHosts/Aspire.Hosting.Java/TypeScript/aspire.config.json Configures TypeScript validation.
tests/PolyglotAppHosts/Aspire.Hosting.Java/TypeScript/package.json Defines TypeScript tooling.
tests/PolyglotAppHosts/Aspire.Hosting.Java/TypeScript/package-lock.json Locks TypeScript dependencies.
tests/PolyglotAppHosts/Aspire.Hosting.Java/TypeScript/tsconfig.json Configures TypeScript compilation.

Review details

Files not reviewed (1)
  • tests/PolyglotAppHosts/Aspire.Hosting.Java/TypeScript/package-lock.json: Generated file
  • Files reviewed: 72/74 changed files
  • Comments generated: 9
  • Review effort level: Balanced

Comment thread src/Aspire.Hosting.Java/JavaHostingExtensions.cs
Comment thread src/Aspire.Hosting.Java/JavaDockerfileGenerator.cs
Comment thread src/Aspire.Hosting.Java/JavaDockerfileGenerator.cs Outdated
Comment thread src/Aspire.Hosting.Java/JavaDockerfileGenerator.cs Outdated
Comment thread src/Aspire.Hosting.Java/JavaDockerfileGenerator.cs Outdated
Comment thread src/Aspire.Hosting.Java/JavaHostingExtensions.cs
Comment thread extension/src/debugger/languages/java.ts Outdated
Comment thread src/Aspire.Hosting.Java/JavaHostingExtensions.cs
Comment thread src/Aspire.Hosting.Java/README.md Outdated
Resource debugging: stop sending a JAR path as the debug adapter's main
class. The adapter documents mainClass as "the fully qualified class name
(e.g. [java module name/]com.xyz.MainApp) or the java file path of the
program entry", so it never opens an archive, and a JAR path left it
unable to resolve an entry point at all. The app host now reads Main-Class
from the JAR manifest itself, including values the manifest wrapped across
lines, and sends the archive on a new class_paths field that maps to the
adapter's classPaths. That is what java -jar does, and it keeps working
for Spring Boot fat JARs whose manifest names JarLauncher. Reading the
manifest is best effort: a missing or entry-point-less archive still gets
a classpath so the IDE can resolve the entry point from the project.

AppHost project model: add JavaAppHostToolchainResolver, mirroring the
TypeScript package manager resolver, because the choice depends on files
in the AppHost directory and GetRuntimeSpec() cannot see them. A pom.xml
selects Maven and a build.gradle selects Gradle; anything else keeps the
existing javac path byte for byte, so adopting a build tool is opt-in and
a bare JDK remains enough. A wrapper checked into the project wins over
the tool on PATH, and is referenced by absolute path because the process
starts without a shell.

Under a build tool the AppHost gains a real restore phase
(dependency:copy-dependencies, or a Gradle init script that adds the
equivalent task so no user build file edit is needed) and is compiled
before launch instead of inline on first run. It is then launched with a
plain java command rather than mvn exec:java or gradle run: those run the
app inside, or as a child of, the build tool's JVM, so console signals
never reach the AppHost. Launching directly also avoids the {args}
placeholder, so CLI arguments arrive as real argv entries instead of being
spliced into a shell string.

AppHost debugging: advertise ExtensionLaunchCapability so the CLI routes
the AppHost launch through the extension's Java debug adapter, the same
mechanism TypeScript and Rust already use.

Detection now also finds src/main/java/AppHost.java, which is where a
Maven or Gradle project puts it, and the javac command quotes the
substituted AppHost path so a directory containing spaces still builds.

Scaffolding emits .vscode/settings.json declaring both source roots.
Without a build file the Java language server treats the folder as an
invisible project rooted at the workspace, so every reference to the
generated SDK under .aspire/modules failed to resolve: no completion, no
navigation, and no breakpoint binding.

Fix the java-starter template, which could never run. It declared
"package aspire;", so it compiled to .java-build/aspire/AppHost.class
while the runner loads AppHost from the default package, failing with
ClassNotFoundException even though compilation succeeded. Verified
against the real generated SDK, and covered by a regression test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d
Copilot AI review requested due to automatic review settings August 15, 2026 03:22

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 not reviewed (1)
  • tests/PolyglotAppHosts/Aspire.Hosting.Java/TypeScript/package-lock.json: Generated file
Suppressed comments (4)

Previously missed (1) — in code that hasn't changed since the last review.

src/Aspire.Hosting.Java/JavaVersionDetector.cs:16

  • This XML documentation names WithDockerfileBaseImages, but the public API and README use the singular WithDockerfileBaseImage. The current IntelliSense reference sends users to a method that does not exist.

src/Aspire.Hosting.Java/JavaDockerfileGenerator.cs:254

  • Joining build arguments verbatim loses the argument boundaries preserved by WithMavenBuild/WithGradleBuild and lets shell metacharacters change the generated RUN command. For example, one argument such as -Dmessage=hello world becomes two arguments during publish even though it is one argument in run mode. Shell-quote every configured argument before composing the command.
    src/Aspire.Hosting.Java/JavaDockerfileGenerator.cs:149
  • Published containers drop all Java application arguments. PublishAsDockerFile explicitly clears executable arguments, launch-tool arguments are skipped for containers, and this Dockerfile sets only an ENTRYPOINT with no corresponding CMD. Thus arguments passed to the JAR overload or via WithArgs (for example the playground worker's --interval-seconds 10) work locally but disappear after publish. Preserve the evaluated application arguments as the container command/CMD.
    src/Aspire.Hosting.Java/JavaDockerfileGenerator.cs:86
  • A genuinely prebuilt JAR cannot be published: Write always resolves a Maven or Gradle build before generating the image, and ResolveToolAndArgs throws when the directory contains only the JAR. This contradicts the AddJavaApp(..., jarPath) prebuilt-JAR mode and the README statement that an app that runs should publish without extra configuration. Handle JavaJarPathAnnotation by copying the existing JAR from the build context instead of requiring a build tool.
  • Files reviewed: 78/80 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment thread src/Aspire.Hosting.CodeGeneration.Java/JavaLanguageSupport.cs
Comment thread src/Aspire.Cli/Projects/JavaAppHostToolchainResolver.cs Outdated
Comment thread src/Aspire.Cli/Projects/JavaAppHostToolchainResolver.cs Outdated
Comment thread src/Aspire.Hosting.Java/README.md 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.

…ws wrappers

Requires a checked-in Maven or Gradle wrapper everywhere a build tool is invoked,
in run mode, in publish mode, and for Java AppHosts. A globally installed tool is
now rejected with an actionable message naming the command that generates the
wrapper. The wrapper pins the tool version in the repository so the AppHost, CI,
and the container image all build with the same one. Because the wrapper carries
its own distribution, the container build stage is always a plain JDK image.

Resolves a Java AppHost's project root instead of assuming it is the AppHost's own
directory. The conventional src/main/java layout keeps its build file three levels
up, and commands run from the AppHost directory, so build tool paths are rewritten
relative to it and the tool is pointed back at the project.

Runs Windows wrappers through the command interpreter. They are batch files, which
produce no output when launched directly with redirected stdout. The wrapper is
passed as a relative path so cmd.exe never sees a quoted first token, which is the
case where its quote stripping mangles the rest of the command line.

Other review fixes: prebuilt JARs now publish as a single-stage JRE image, publish
honors WithWrapperPath, build arguments are shell quoted, container file sources
get pipeline dependencies, the language server readiness wait is bounded, repeated
WithOtelAgent calls collapse to one -javaagent entry, and the README uses the
hosting convention for link bullets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d
Copilot AI review requested due to automatic review settings August 15, 2026 03:57
Follows the precedent set by the JavaScript integration's pnpm image tests. The
JAR is produced inside a JDK container, so the test needs Docker and nothing
else, and only the runtime image's own JDK can emit class files that image is
guaranteed to load.

The test asserts on the published pair rather than the image alone. Application
arguments are deliberately not baked into the image, exactly like every other
container resource: they belong to the deployment spec, so the manifest carries
them and the runtime appends them to the entrypoint. Reading them back out of the
manifest and passing them to docker run is what proves the two halves agree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@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.

Three problems, all confirmed from the Windows CI logs rather than inferred.

`File.SetLastWriteTimeUtc` was called on a directory. That overload opens the
path without FILE_FLAG_BACKUP_SEMANTICS, which Windows refuses for a directory
handle, so the churn test died with

  System.UnauthorizedAccessException : Access to the path
  '...\classes\config' is denied.
     at System.IO.File.SetLastWriteTimeUtc(String path, DateTime lastWriteTimeUtc)

`Directory.SetLastWriteTimeUtc` is the directory overload, and is already what
BackdateWorkspace uses.

The staged-dependency test stamped at DateTime.UtcNow and then relied on the
real timestamp of an add/remove being newer. On Windows UtcNow advances in
~15ms steps, so the two landed in the same tick, the check saw no change, and
the assertion failed with the AppHost launched but never compiled. Both tests
now age the workspace first and stamp behind it, which is the pattern the
deletion tests already use.

Also restores three method braces that had been joined onto their signature
lines, against the .editorconfig rule.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

VS Code renders one merged CodeLens set per document, so a `java` file shows no
lenses at all until every registered provider has answered - including
redhat.java's. On a cold CI runner that server is still importing the
workspace, which is how this spec failed with

  Timed out waiting for a CodeLens containing 'bypass Aspire' in 'AppHost.java'
  CodeLenses: (none)

while the Aspire lens itself was ready. `(none)` rather than a list missing our
lens is what points at the merge rather than at the provider.

The other two Java specs already wait on `serverReady()` here, and carry the 30
minute suite budget that wait needs; this spec was the only one that did not.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

The probe suite opens editors on a dozen files in a temp directory, which
left two Windows-only failures behind.

Removing the temp directory raced the handle Windows releases after an
editor closes and threw EPERM, so the teardown failed the suite. Node
retries recursive removals for exactly that code, and a directory
stranded in the OS temp folder is not worth failing a suite whose
assertion is about editor tabs, so the removal now retries and then only
reports.

Showing those documents also queues background extension work, and on
Windows a process it starts can still be in flight when a later test
installs its own child_process stubs. That made
`execFileStub.firstCall` describe the leaked process instead of the
dotnet command under test, and the assertion reported the probe's temp
directory as the working directory. The dotnet debugger tests now select
the call by the project it was started for, which is what they meant to
assert and is immune to unrelated activity in the shared extension host.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

These shards had not run since the Windows unit test failure started skipping
them, so all three regressions had been accumulating unseen.

java-apphost: the endpoint wait silently clamped the caller's budget to 300s via
Math.min, so a spec asking for 900s got 300s and an error message that reported a
timeout the caller never chose. The resource is a Spring Boot app being compiled
and started under a debugger, which does not fit in 300s on a cold CI runner.
Honor the caller's budget, and pre-compile the Maven projects in the cache warm
step - dependency:go-offline does not fetch plugin artifacts, so the compiler and
Spring Boot plugins were still being downloaded during the timed run. The Gradle
project already ran compileJava; this removes the asymmetry.

java-apphost-debug-startup-timeout: pwsh does not stop on a native command's exit
code, and this step never checked $LASTEXITCODE after `aspire setup` even though
the rest of the workflow does. A failed setup let the shard run on without the
orchestration component and fail four minutes later with a debug startup timeout
instead of the actual provisioning error.

workspace-target-proof: the add-folder retry loop could not retry. A single failed
attempt spends up to 30s in InputBox.create and another 30s confirming the folder
never arrived, so the 90s deadline bought one attempt and part of a second before
reporting the confirmation poll as the cause. Shorten the confirmation poll, which
costs a successful add nothing, and size the deadline and suite timeout so the
retries the comment promises can actually happen.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

The DTO's setter is generated from the keyword-escaped field, so a property named
Default declares setDefault_. The exported-value initializer derived its call from
the raw property name and emitted setDefault(...) instead, which javac rejects with
"cannot find symbol".

The CLI compiles the whole generated SDK in a single javac invocation, so one
keyword-named property on any exported value would break `aspire run` for the entire
Java AppHost - and the user cannot fix generated code. This was latent only because
no current exported value uses a keyword-named property.

Route both the accessor declarations and the initializer call through a single
DtoAccessorSuffix helper so the two cannot drift apart again.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Windows releases the handle behind a closed editor or an exited child process
asynchronously, so a teardown running right after the test body can still see the
directory as in use and throw "EPERM, Permission denied". Mocha fails the hook,
which fails the whole run and skips every E2E shard that depends on it.

Fixing this per-file was whack-a-mole: e2eDiagnosticsProbe was fixed last week and
aspireEditorCommandProvider failed the very next run with the identical error. The
cleanup call sites had drifted into three different conventions - some passed
maxRetries/retryDelay, most passed only force: true, which suppresses ENOENT and
nothing else.

Route all 74 recursive cleanup removals through one shared removeDirectorySafely
helper that retries and then swallows. Leaving a temp directory behind is a
non-event; losing a whole run's signal is not.

Forcing the underlying removal to throw EPERM shows the exposure: without the guard
68 tests fail with the CI error shape, with it all 2244 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

package.nls.json is the only input to the XLF catalog (see gulpfile.js), so a string
that exists only in strings.ts never reaches translators and ships English-only. The
two configuredCliPath* strings added by this PR were missed, so the warning shown
when a configured aspire.aspireCliExecutablePath cannot be used would have been
English for every non-English user.

The existing guards only scan the rust* and java* prefixes, which is why this passed
CI. Replace that with a scan of every vscode.l10n.t export, allowlisting the 51
strings that predate the guard so the next omission fails here instead of in a
localization drop. Removing the two new entries again fails the guard by name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

The Java E2E shards ran for the first time on this PR because they are gated
behind the Windows extension unit tests, which were red until the temp-directory
cleanup fix. Three of them failed, and in each case the reported error described
a symptom rather than the cause.

The CodeLens spec waited on redhat.java's serverReady(), which resolves in
LightWeight mode. VS Code renders one merged CodeLens set per document, so while
that server was still importing the file showed `CodeLenses: (none)` - including
the Aspire lens, which had been ready the whole time. The wait now requires
Standard mode and names the mode it saw when it gives up.

The workspace target spec drove `Workspaces: Add Folder to Workspace...` and its
quick-open input. Adding the first folder converts the window into an untitled
multi-root workspace, which reloads the extension host, and after that reload the
second add never took - four retries failed identically, so the fragility was in
the UI flow rather than the budget. What the spec proves is that CLI commands
target the right workspace folder, so the folder now goes through the same API
VS Code's own command calls and the bridge resolves once the extension host has
observed it.

The debug startup shard failed because PrebuiltAppHostServer set ASPIRE_DCP_PATH
only when the layout supplied a DCP path and said nothing when it did not. The
AppHost then fell back to the DcpCliPath assembly metadata, which points into
~/.nuget/packages - a package a guest-language AppHost never restores - and
reported "The Aspire orchestration component is not installed at <nuget path>",
describing the fallback instead of the missing layout. That case is now logged,
and the workflow asserts the bundle really contains dcp right after `aspire
setup` so a missing component fails on that line instead of minutes later.

The endpoint wait reported "Last error: <none>" because a poll that returns
undefined never records an error, which cannot distinguish a resource that failed
to start from one still building from one that was never in the model. Waits can
now attach the state they were looking at, and the resource endpoint wait reports
every resource with its state, health, exit code and URLs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

# Conflicts:
#	extension/src/types/extensionApi.ts

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Retry the integration build with a restore after a package-cache eviction
--------------------------------------------------------------------------
The restore-skip fast path fingerprints this app's own inputs, but it cannot
see the shared global package cache. After `dotnet nuget locals all --clear`
(or ordinary cache eviction) the fingerprint is unchanged while the packages
it assumes are gone, so the `--no-restore` build fails with NETSDK1064 /
NU1101 / NU1102 rather than the NETSDK1004 the retry heuristic looked for.

That made the failure permanent, not transient: the restore stamp is written
only after a successful restore and is never cleared, so the next `aspire run`
re-evaluates the same unchanged fingerprint, skips restore again, and fails
identically until the user manually deletes obj/. Before the fast path existed
the build always restored, so it self-healed.

Broaden ShouldRetryWithRestore to the package-resolution error codes and record
in the remarks why the fingerprint cannot cover this. Ordinary compile errors
are still not retried, so the cost optimization is preserved -- the existing
ShouldRetryWithRestore_DoesNotRetryAnOrdinaryCompileFailure guard still passes.

Affects every bundle/guest-mode language (Python, JavaScript, Go, Rust, Java),
not just Java.

Verified red/green: the three new NETSDK1064/NU1101/NU1102 cases fail against
the old heuristic and pass against the new one; PrebuiltAppHostServerTests is
120/120.

Register JavaSwing.AppHost in Aspire.slnx
----------------------------------------
The JavaFX-to-Swing swap added playground/JavaSwing/JavaSwing.AppHost but never
registered it, unlike its JavaSpringBoot and JavaQuarkus siblings. It still
compiles (it matches the playground glob in eng/Build.props, so this was not a
CI gap) but it was invisible in the IDE solution and to solution-scoped tooling.
Verified with `dotnet sln Aspire.slnx list`, which now lists all three.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

This PR started forwarding AspireCliPath to the MCP server (upstream never set
env at all here), but it was the only AspireCliPath producer that forwarded the
raw resolved path without the forwardability guard. Every other site -- the
process environments, the terminal collection, the environment synchronizer, and
the dotnet debug env -- routes the value through getForwardableAspireCliPath or
getForwardableResolvedAspireCliPath first.

`aspire agent mcp` can build an AppHost, and that build inherits this
environment. With an unbundled framework-dependent CLI (an inner-loop
`dotnet build` output, where the apphost sits next to aspire.dll with no install
sidecar and no adjacent bundle layout), MSBuild's ResolveAspireCliBundle binds
bundle assets to a CLI that has no bundle layout and fails with ASPIRE009 --
while a plain terminal build in the same workspace succeeds, because the
terminal env collection omits the unbundled path.

Use getForwardableResolvedAspireCliPath, which is the right variant here: the
caller has already selected and launched this exact executable, so rejection of
a different configured value must not suppress it. Omitting the variable when it
is not forwardable lets the build fall back to PATH probing, matching the other
sites. Also drops the duplicate local 'AspireCliPath' literal in favour of the
exported ASPIRE_CLI_PATH_ENV_VAR.

Verified red/green: with the guard removed the new test fails, showing
AspireCliPath='/repo/artifacts/bin/Aspire.Cli/Debug/aspire' forwarded where the
env must be empty; with the guard it passes. Sibling tests for
getForwardableResolvedAspireCliPath and AspireTerminalProvider already assert
this same invariant -- the MCP provider was the only site missing it. Full
extension unit suite 2251 passing, exit 0; tsc and eslint clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@adamint

Adam Ratzman (adamint) commented Aug 17, 2026

Copy link
Copy Markdown
Member

Did a final pass over this after merging main in. Five reviewers over the whole diff (CLI/guest runtime, Java hosting + codegen, extension + workflows, deploy/publish + shared runtime, and tests/). Three findings, all fixed on the branch with red/green proofs. Details below, plus a couple of things I'd still like your take on.

Fixed

1. Permanent build failure after a NuGet cache eviction — and it hits every guest language, not just Java. PrebuiltAppHostServer.ShouldRetryWithRestore only matched NETSDK1004/project.assets.json. The restore-skip fingerprint covers the app's own inputs but can't see the shared global package cache, so after a dotnet nuget locals all --clear the fingerprint is unchanged while the packages are gone, and the --no-restore build fails with NETSDK1064/NU1101/NU1102 instead. None of those matched, so no retry.

The part that makes it more than a papercut: the restore stamp is only written on success and never cleared, so the next aspire run re-evaluates the same fingerprint, skips restore again, and fails identically — until the user manually deletes obj/. Before this fast path existed the build always restored, so it self-healed. Broadened the heuristic to the package-resolution codes; ordinary compile errors are still not retried so the optimization is intact.

2. MCP server was forwarding AspireCliPath without the forwardability guard. This is the only AspireCliPath producer that forwarded the raw resolved path — every other site routes through getForwardableAspireCliPath/getForwardableResolvedAspireCliPath. With an unbundled framework-dependent CLI (an inner-loop dotnet build output), aspire agent mcp builds inherit it and ResolveAspireCliBundle binds bundle assets to a CLI with no bundle layout → ASPIRE009, while a plain terminal build in the same workspace works fine because the terminal collection omits it. There were already tests asserting this invariant for getForwardableResolvedAspireCliPath and AspireTerminalProvider; MCP was just missed.

3. JavaSwing.AppHost wasn't in Aspire.slnx. Looks like it got dropped in the JavaFX→Swing swap. Not a CI gap (it still matches the playground glob in eng/Build.props), just invisible in the solution next to its two siblings.

Clean

Java hosting/codegen and the playgrounds came back clean — keyword escaping, invariant number formatting, no-shell argument arrays, path-traversal rejection all check out. tests/ came back clean across unfailable-test patterns, flaky patterns, conventions, and secrets/machine paths; it also confirmed the three deleted DotNetCliRunnerTests moved verbatim into AppHostStartupTimeoutTests with an added invalid-value theory, so no coverage was lost. And the deploy-environment idempotency guards are byte-for-byte inert on the single-run publish path every non-Java language takes.

The new forwardability test hardcoded POSIX separators while the production
guard builds the adjacent assembly with path.join(path.dirname(cliPath),
'aspire.dll'). On Windows that emits backslashes, so the stub never matched,
the path looked forwardable, and the assertion failed. On macOS the separators
coincide, which is why it passed locally and only broke on the Windows job.

Build the expectation with the same expression production uses so the two agree
on any platform.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

The DCP guard searched the directory the CLI archive was unpacked into, but
`aspire setup` extracts to SetupCommand.GetDefaultInstallPath, which is the
parent of the parent of the CLI binary. With the CLI at
artifacts/bundle/<rid>/aspire that install path is artifacts/bundle, so DCP
lands beside the search root rather than inside it and the guard threw on every
shard even though setup had succeeded.

Derive the same path the CLI derives instead of assuming the layout. The guard
had never executed before now: the shards were skipped in the two prior runs
because the Windows unit job gates them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5cff55fa-c3e2-4230-afa6-3831ae75b35d

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Java hosting integration

3 participants