Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/polyglot-validation/Dockerfile.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# Note: Expects self-extracting binary and NuGet artifacts to be pre-downloaded to /workspace/artifacts/
#
FROM mcr.microsoft.com/devcontainers/java:17
FROM mcr.microsoft.com/devcontainers/java:25-trixie

# Ensure Yarn APT repository signing key is available (base image includes Yarn repo)
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | sudo tee /etc/apt/keyrings/yarn-archive-keyring.gpg > /dev/null
Expand All @@ -30,7 +30,8 @@

COPY setup-local-cli.sh /scripts/setup-local-cli.sh
COPY test-java.sh /scripts/test-java.sh
RUN chmod +x /scripts/setup-local-cli.sh /scripts/test-java.sh
COPY test-java-playground.sh /scripts/test-java-playground.sh
RUN chmod +x /scripts/setup-local-cli.sh /scripts/test-java.sh /scripts/test-java-playground.sh

# Entrypoint: Set up Aspire CLI and run validation
# Bundle extraction happens lazily on first command that needs the layout
Expand All @@ -40,5 +41,6 @@
aspire --nologo config set features:experimentalPolyglot:java true --global && \
echo '' && \
echo '=== Running validation ===' && \
/scripts/test-java.sh \
/scripts/test-java.sh && \
/scripts/test-java-playground.sh \
"]
118 changes: 118 additions & 0 deletions .github/workflows/polyglot-validation/test-java-playground.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/bin/bash
# Polyglot SDK Validation - Java Playground Apps
# Iterates all Java playground apps under playground/polyglot/Java/,
# runs 'aspire restore' to regenerate the .modules/ SDK, and compiles the
# compact AppHost plus generated Java SDK sources to verify there are no
# regressions in the codegen API surface.
set -euo pipefail

echo "=== Java Playground Codegen Validation ==="

if ! command -v aspire &> /dev/null; then
echo "❌ Aspire CLI not found in PATH"
exit 1
fi

if ! command -v javac &> /dev/null; then
echo "❌ javac not found in PATH (JDK required)"
exit 1
fi

echo "Aspire CLI version:"
aspire --version

echo "javac version:"
javac -version

SCRIPT_SOURCE="${BASH_SOURCE[0]:-$0}"
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
if [ -d "/workspace/playground/polyglot/Java" ]; then
PLAYGROUND_ROOT="/workspace/playground/polyglot/Java"
elif [ -d "$PWD/playground/polyglot/Java" ]; then
PLAYGROUND_ROOT="$(cd "$PWD/playground/polyglot/Java" && pwd)"
elif [ -d "$SCRIPT_DIR/../../../playground/polyglot/Java" ]; then
PLAYGROUND_ROOT="$(cd "$SCRIPT_DIR/../../../playground/polyglot/Java" && pwd)"
else
echo "❌ Cannot find playground/polyglot/Java directory"
exit 1
fi

echo "Playground root: $PLAYGROUND_ROOT"

APP_DIRS=()
for integration_dir in "$PLAYGROUND_ROOT"/*/; do
if [ -f "$integration_dir/ValidationAppHost/AppHost.java" ]; then
APP_DIRS+=("$integration_dir/ValidationAppHost")
fi
done

if [ ${#APP_DIRS[@]} -eq 0 ]; then
echo "❌ No Java playground apps found"
exit 1
fi

echo "Found ${#APP_DIRS[@]} Java playground apps:"
for dir in "${APP_DIRS[@]}"; do
echo " - $(basename "$(dirname "$dir")")/$(basename "$dir")"
done
echo ""

FAILED=()
PASSED=()

for app_dir in "${APP_DIRS[@]}"; do
app_name="$(basename "$(dirname "$app_dir")")/$(basename "$app_dir")"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Testing: $app_name"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

cd "$app_dir"

echo " → aspire restore..."
if ! aspire restore --non-interactive 2>&1; then
echo " ❌ aspire restore failed for $app_name"
FAILED+=("$app_name (aspire restore)")
continue
fi

echo " → javac..."
build_dir="$app_dir/.java-build"
rm -rf "$build_dir"
mkdir -p "$build_dir"

if [ ! -f ".modules/sources.txt" ]; then
echo " ❌ No generated Java source list found for $app_name"
FAILED+=("$app_name (generated sources missing)")
rm -rf "$build_dir"
continue
fi

if ! javac --enable-preview --source 25 -d "$build_dir" @.modules/sources.txt AppHost.java 2>&1; then
echo " ❌ javac compilation failed for $app_name"
FAILED+=("$app_name (javac)")
rm -rf "$build_dir"
continue
fi

rm -rf "$build_dir"
echo " ✅ $app_name passed"
PASSED+=("$app_name")
echo ""
done

echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Results: ${#PASSED[@]} passed, ${#FAILED[@]} failed out of ${#APP_DIRS[@]} apps"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

if [ ${#FAILED[@]} -gt 0 ]; then
echo ""
echo "❌ Failed apps:"
for f in "${FAILED[@]}"; do
echo " - $f"
done
exit 1
fi

echo "✅ All Java playground apps validated successfully!"
exit 0
40 changes: 20 additions & 20 deletions .github/workflows/polyglot-validation/test-java.sh
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#!/bin/bash
# Polyglot SDK Validation - Java
# This script validates the Java AppHost SDK with Redis integration
set -e
# Creates a Java AppHost via the CLI, adds Redis, and validates that
# `aspire run` can launch a Redis-backed app non-interactively.
set -euo pipefail

echo "=== Java AppHost SDK Validation ==="

# Verify aspire CLI is available
if ! command -v aspire &> /dev/null; then
echo "❌ Aspire CLI not found in PATH"
exit 1
Expand All @@ -14,16 +14,24 @@ fi
echo "Aspire CLI version:"
aspire --version

# Create project directory
WORK_DIR=$(mktemp -d)
WORK_DIR="$(mktemp -d)"
ASPIRE_PID=""

cleanup() {
if [ -n "${ASPIRE_PID:-}" ]; then
kill "$ASPIRE_PID" 2>/dev/null || true
fi
rm -rf "$WORK_DIR"
}

trap cleanup EXIT

echo "Working directory: $WORK_DIR"
cd "$WORK_DIR"

# Initialize Java AppHost
echo "Creating Java apphost project..."
aspire init --language java --non-interactive -d

# Add Redis integration
echo "Adding Redis integration..."
aspire add Aspire.Hosting.Redis --non-interactive -d 2>&1 || {
echo "aspire add failed, manually updating settings.json..."
Expand All @@ -37,23 +45,20 @@ aspire add Aspire.Hosting.Redis --non-interactive -d 2>&1 || {
fi
}

# Insert Redis code into AppHost.java
echo "Configuring AppHost.java with Redis..."
if grep -q "builder.build()" AppHost.java; then
sed -i '/builder.build()/i\ // Add Redis cache resource\n builder.addRedis("cache", null, null).withImageRegistry("netaspireci.azurecr.io");' AppHost.java
if grep -q "builder.build().run();" AppHost.java; then
sed -i '/builder.build().run();/i\ builder.addRedis("cache")\n .withImageRegistry("netaspireci.azurecr.io");' AppHost.java
echo "✅ Redis configuration added to AppHost.java"
fi

echo "=== AppHost.java ==="
cat AppHost.java

# Run the apphost in background
echo "Starting apphost in background..."
aspire run -d > aspire.log 2>&1 &
aspire run -d --non-interactive > aspire.log 2>&1 &
ASPIRE_PID=$!
echo "Aspire PID: $ASPIRE_PID"

# Poll for Redis container with retries
echo "Polling for Redis container..."
RESULT=1
for i in {1..12}; do
Expand All @@ -68,17 +73,12 @@ for i in {1..12}; do
sleep 10
done

if [ $RESULT -ne 0 ]; then
if [ "$RESULT" -ne 0 ]; then
echo "❌ FAILURE: Redis container not found after 2 minutes"
echo "=== Docker containers ==="
docker ps
echo "=== Aspire log ==="
cat aspire.log || true
fi

# Cleanup
echo "Stopping apphost..."
kill -9 $ASPIRE_PID 2>/dev/null || true
rm -rf "$WORK_DIR"

exit $RESULT
exit "$RESULT"
19 changes: 15 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,14 @@ jobs:
name: TypeScript SDK Unit Tests
uses: ./.github/workflows/typescript-sdk-tests.yml

java_sdk_tests:
name: Java SDK Unit Tests
uses: ./.github/workflows/run-tests.yml
with:
testShortName: Java SDK
testProjectPath: tests/Aspire.Hosting.CodeGeneration.Java.Tests/Aspire.Hosting.CodeGeneration.Java.Tests.csproj
extraTestArgs: --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"

results:
if: ${{ always() && github.repository_owner == 'microsoft' }}
runs-on: ubuntu-latest
Expand All @@ -230,6 +238,7 @@ jobs:
build_cli_archive_macos,
extension_tests_win,
typescript_sdk_tests,
java_sdk_tests,
tests_no_nugets,
tests_no_nugets_overflow,
tests_requires_nugets_linux,
Expand Down Expand Up @@ -294,8 +303,9 @@ jobs:
contains(needs.*.result, 'cancelled') ||
(github.event_name == 'pull_request' &&
(needs.extension_tests_win.result == 'skipped' ||
needs.typescript_sdk_tests.result == 'skipped' ||
needs.tests_no_nugets.result == 'skipped' ||
needs.typescript_sdk_tests.result == 'skipped' ||
needs.java_sdk_tests.result == 'skipped' ||
needs.tests_no_nugets.result == 'skipped' ||
needs.tests_requires_nugets_linux.result == 'skipped' ||
needs.tests_requires_nugets_windows.result == 'skipped' ||
(fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_nugets_macos).include[0] != null &&
Expand All @@ -304,8 +314,9 @@ jobs:
needs.polyglot_validation.result == 'skipped')) ||
(github.event_name != 'pull_request' &&
(needs.extension_tests_win.result == 'skipped' ||
needs.typescript_sdk_tests.result == 'skipped' ||
needs.tests_no_nugets.result == 'skipped' ||
needs.typescript_sdk_tests.result == 'skipped' ||
needs.java_sdk_tests.result == 'skipped' ||
needs.tests_no_nugets.result == 'skipped' ||
needs.tests_requires_nugets_linux.result == 'skipped' ||
needs.tests_requires_nugets_windows.result == 'skipped' ||
(fromJson(needs.setup_for_tests.outputs.tests_matrix_requires_nugets_macos).include[0] != null &&
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ node_modules/
target/
dependency-reduced-pom.xml

# Java AppHost build artifacts
playground/**/.java-build/
playground/**/*.class

# Generated Java AppHost SDK sources
/playground/JavaAppHost/.modules/
/playground/polyglot/Java/**/ValidationAppHost/.modules/

# Ignore cache created with the Angular CLI.
.angular/

Expand Down
17 changes: 17 additions & 0 deletions playground/JavaAppHost/AppHost.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import aspire.*;

void main(String[] args) throws Exception {
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(args);

NodeAppResource app = builder.addNodeApp("app", "./api", "src/index.ts");
app.withHttpEndpoint(new WithHttpEndpointOptions().env("PORT"));
app.withExternalHttpEndpoints();

ViteAppResource frontend = builder.addViteApp("frontend", "./frontend");
frontend.withReference(app);
frontend.waitFor(app);

app.publishWithContainerFiles(frontend, "./static");

builder.build().run();
}
22 changes: 22 additions & 0 deletions playground/JavaAppHost/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "java-apphost-api",
"private": true,
"type": "module",
"scripts": {
"start": "tsx src/index.ts"
},
"dependencies": {
"@opentelemetry/auto-instrumentations-node": "^0.71.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.213.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.213.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0",
"@opentelemetry/sdk-logs": "^0.213.0",
"@opentelemetry/sdk-metrics": "^2.6.0",
"@opentelemetry/sdk-node": "^0.213.0",
"express": "^5.1.0"
},
"devDependencies": {
"@types/express": "^5.0.6",
"tsx": "^4.21.0"
}
}
42 changes: 42 additions & 0 deletions playground/JavaAppHost/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Import the OpenTelemetry instrumentation setup first, before any other modules.
* This ensures all subsequent imports are automatically instrumented for
* distributed tracing, metrics, and logging in the Aspire dashboard.
*/
import "./instrumentation.ts";
import express from "express";
import { existsSync } from "fs";
import { join } from "path";

const app = express();
const port = process.env.PORT || 5000;

/** Returns a random 5-day weather forecast as JSON. */
app.get("/api/weatherforecast", (_req, res) => {
const summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];
const forecasts = Array.from({ length: 5 }, (_, i) => {
const temperatureC = Math.floor(Math.random() * 75) - 20;
return {
date: new Date(Date.now() + (i + 1) * 86400000).toISOString(),
temperatureC,
temperatureF: 32 + Math.trunc(temperatureC / 0.5556),
summary: summaries[Math.floor(Math.random() * summaries.length)],
};
});
res.json(forecasts);
});

app.get("/health", (_req, res) => {
res.send("Healthy");
});

// Serve static files from the "static" directory if it exists (used in publish/deploy mode
// when the frontend's build output is bundled into this container via publishWithContainerFiles)
const staticDir = join(import.meta.dirname, "..", "static");
if (existsSync(staticDir)) {
app.use(express.static(staticDir));
}

app.listen(port, () => {
console.log(`API server listening on port ${port}`);
});
Loading
Loading