Skip to content

Add Java hosting integration - #18033

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

Add Java hosting integration#18033
Marshal Hayes (marshalhayes) wants to merge 137 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
Three CliPathEnvironmentSynchronizer tests drive the real CliPathResolver with a
${workspaceFolder} token and then assert on hardcoded POSIX paths. Production
expands those tokens through expandConfiguredCliPath, which finishes with
pathLib.normalize using path.win32 on Windows, so the expansion is
\repo\a\aspire and never matches '/repo/a/aspire'. Two tests saw the wrong
value and the third hung for its full 20s timeout, because its tryExecute stub
gates a promise on being called with the expanded path and never was.

Derive the expectations with path.normalize so they track the platform the same
way production does, and drop the file-private createWorkspaceFolder in favour
of the shared testHelpers one, which pins fsPath so the folder literals survive
Uri.file on Windows.

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 lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

The three ${workspaceFolder} tests still failed on Windows after normalizing
their expectations, because the fixture roots themselves are the problem.
'/repo/a' becomes '\repo\a' there, and isAbsoluteCliPath deliberately treats a
driveless root as drive-relative rather than absolute, so the resolver rejected
the expanded candidate and fell through to PATH instead of probing it.

Build the roots with path.resolve so they are drive qualified on Windows and
unchanged on POSIX, and derive the expected CLI paths from them with path.join.

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 lines (20,000). Try reducing the number of changed lines 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.

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

Skipping the rewrite of identical generated files broke the Python SDK
validation: the run added Redis, and the AppHost then failed with
'DistributedApplicationBuilder' object has no attribute 'add_redis'.

Python does not run .aspire/modules in place. uv builds it into the virtual
environment and decides whether to reuse its cached build from the source
timestamps, so the generated pyproject.toml staying untouched let uv keep the
install it had built at 'aspire init', before Redis was ever added. Rewriting
unchanged files was accidentally load-bearing there.

Make it an opt-in on LanguageInfo instead, set only for Java, whose javac,
Maven and Gradle commands read the generated sources directly and are the
reason the optimization exists. The regression is covered by a test that a
language which does not opt in still rewrites identical content, and by one
pinning Java as the only language that opts in.

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 lines (20,000). Try reducing the number of changed lines 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.

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

Five separate problems, all found by letting a full CI run finish:

The Java CodeLens shard reported no lenses even though the provider produced
one. ExTester's TextEditor.getCodeLenses() enumerates only lenses rendered as
anchors, and Aspire's entry-point warning is contributed with an empty command
precisely so it is not a dead link, so it was structurally invisible to that
API. Read the codelens widget text directly instead, which is also closer to
what a user sees. The provider now logs the lenses it computed and catches its
own failures, because a provider that silently returns nothing leaves no trace
in any log and cost a long time to diagnose.

deploy and publish were excluded from the shared CLI availability gate, so
invoking them with no CLI installed silently did nothing instead of offering to
install it. That exclusion was not needed and is reverted.

The package surface test hardcodes the extension's activation events, so the
three Java workspaceContains entries had to be mirrored there.

Adding the first folder in the workspace target proof converts the window into
an untitled multi-root workspace and reloads it, which can swallow the second
add. Retry until the extension reports the folder. Its update --self assertion
also required the literal 'aspire update --self', which only holds on a machine
with no CLI installed; assert the property under test instead, that a
window-scoped command resolves its CLI outside every workspace folder.

Finally, the Java shards downloaded the entire Spring dependency tree inside
each test's readiness budget. Cache the user-level Maven and Gradle stores and
resolve dependencies before VS Code starts.

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 lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@adamint

Copy link
Copy Markdown
Member

Correction to an earlier note in this PR: the "skip rewriting generated files that haven't changed" optimization is no longer unconditional. It is now opt-in per language, and only Java opts in.

Leaving it on for every language broke Python. The Python SDK is rebuilt by uv from file timestamps, so preserving an unchanged file's mtime made uv believe the SDK was already current and skip work it actually needed to do. aspire add Aspire.Hosting.Redis in a Python AppHost stopped picking up the new integration. Java has the opposite requirement: javac given an explicit source list recompiles everything every launch, so an unchanged AppHost paid a full compile of the generated SDK each time, which is exactly what the preservation avoids.

LanguageInfo.PreserveUnchangedGeneratedFiles now carries that distinction, defaulting to false, with the reasoning recorded on the property so the next person does not have to rediscover it. Verified both directions: the Java playground reports Generated 239 Java files ... (0 changed) and skips javac, and touching AppHost.java correctly re-runs it; the Python repro reports 2 changed of 2, matching the pre-regression behavior.

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

The synthesized IntegrationRestore project is rebuilt on every polyglot
AppHost launch, and restore dominates it: 5.6s of a 6.7s warm build,
measured on the real generated project.

Restore only needs to run again when something restore reads has changed,
and the generated project file is the single input that expresses all of
it - package identities and versions, project references, channel sources
and the synthesized NuGet.config path are all written into it. So an
assets file no older than the project file means the last restore already
saw this exact input. The generated files are now written only when their
content differs, since an unconditional rewrite would touch the very
timestamp the check reads.

Two properties keep this safe: skipping restore never skips the build, so
edits to referenced projects are still compiled; and a stale or partially
cleaned obj/ - the one input the check cannot see - retries with restore
instead of failing.

Measured end to end on a bundle-mode Java AppHost: 8.2s with restore,
6.3s with it skipped.

Also explain the failure that occurs when the AppHost server is older than
the integration packages it loads. It surfaces as a bare type-load error
naming an assembly version the user never chose, with nothing to indicate
that the CLI and the packages are out of step.

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 lines (20,000). Try reducing the number of changed lines 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.

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

`aspire ls` discovers AppHosts from `git ls-files` when it runs inside a
work tree. The Java E2E workspace has to live inside the repository so a
dev-build CLI can resolve its AppHost server, so ignoring it made
`git ls-files` return nothing there: `aspire ls` reported zero candidates,
and every Java spec failed in its `before all` hook waiting for an AppHost
that discovery could no longer see. The C# AppHost still appeared through
the extension's own filesystem scan, which made it look like Java
discovery was broken rather than the workspace being invisible.

The teardown path already deletes this workspace precisely so it can stay
un-ignored, so the ignore rule was solving a problem that was already
solved. Add a guard that names the cause, because a stray ignore rule
surfaces only as an empty candidate list several minutes into a run.

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 lines (20,000). Try reducing the number of changed lines 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.

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

The extension silently fell back to the installed CLI when
aspire.aspireCliExecutablePath could not be resolved, so a pin at a build
output directory looked like it worked while a stale CLI actually ran. That
surfaced as an NU1605 package downgrade rather than anything pointing at the
setting.

- Resolve a configured path that names a build output directory by probing
  <path>/aspire, so pointing at the build output works.
- Show a warning notification with an "Open Setting" action when a configured
  path is rejected, instead of only writing to the output channel.
- Skip the integration restore only when every referenced project pins exact
  versions; a floating version can resolve differently without any input
  changing.
- Fan out AggregateException children when looking for reflection load
  failures so assembly skew is still described when the failure is wrapped.

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.

@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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Java hosting integration

3 participants