This is an automated email from the ASF dual-hosted git repository.

jason810496 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 50f90d17dda Build Java SDK e2e example bundles natively in CI (#69794)
50f90d17dda is described below

commit 50f90d17ddab0f39d632917da722c9d4bdaa9307
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Wed Jul 15 16:51:54 2026 +0900

    Build Java SDK e2e example bundles natively in CI (#69794)
    
    The java_sdk e2e mode and the lang-SDK k8s test solved the same
    cold-build problem two different ways (containerized Gradle with a
    repo-local cache vs host toolchain with salted home-dir caches), which
    a review follow-up on #69239 asked to reconcile. Converge the e2e mode
    on the #69411 native-toolchain convention: CI provisions the JDK with
    actions/setup-java and caches ~/.gradle, dropping the per-run
    eclipse-temurin image pull and the --network=host Gradle lock-handover
    workaround; local runs keep the containerized build so a dev host
    still needs no JDK.
---
 .github/workflows/additional-prod-image-tests.yml  |  5 ++
 .github/workflows/airflow-e2e-tests.yml            | 54 +++++++++++++------
 .github/workflows/ci-amd.yml                       |  1 +
 .github/workflows/ci-arm.yml                       |  1 +
 .../tests/airflow_e2e_tests/conftest.py            | 63 ++++++++++++++--------
 .../tests/airflow_e2e_tests/constants.py           |  4 ++
 6 files changed, 89 insertions(+), 39 deletions(-)

diff --git a/.github/workflows/additional-prod-image-tests.yml 
b/.github/workflows/additional-prod-image-tests.yml
index e81bfb72566..99a3a4c8728 100644
--- a/.github/workflows/additional-prod-image-tests.yml
+++ b/.github/workflows/additional-prod-image-tests.yml
@@ -56,6 +56,10 @@ on:  # yamllint disable-line rule:truthy
         description: "Whether to run Java SDK e2e tests (true/false)"
         required: true
         type: string
+      java-sdk-version:
+        description: "JDK version used to build the Java SDK bundles natively 
in the e2e tests"
+        required: true
+        type: string
       run-go-sdk-e2e-tests:
         description: "Whether to run Go SDK e2e tests (true/false)"
         required: true
@@ -320,6 +324,7 @@ jobs:
       default-python-version: "${{ inputs.default-python-version }}"
       use-uv: ${{ inputs.use-uv }}
       e2e_test_mode: "java_sdk"
+      java-sdk-version: "${{ inputs.java-sdk-version }}"
     if: inputs.canary-run == 'true' || inputs.run-java-sdk-e2e-tests == 'true'
   test-e2e-integration-tests-go-sdk:
     name: "Go SDK e2e tests with PROD image"
diff --git a/.github/workflows/airflow-e2e-tests.yml 
b/.github/workflows/airflow-e2e-tests.yml
index 4fb1fcf8d84..a13a5cccf2d 100644
--- a/.github/workflows/airflow-e2e-tests.yml
+++ b/.github/workflows/airflow-e2e-tests.yml
@@ -52,6 +52,10 @@ on:  # yamllint disable-line rule:truthy
         description: "Test mode - basic, remote_log, remote_log_elasticsearch, 
remote_log_opensearch, xcom_object_storage, event_driven, java_sdk, or go_sdk"  
# yamllint disable-line rule:line-length
         type: string
         default: "basic"
+      java-sdk-version:
+        description: "JDK version used to build the Java SDK bundles natively 
(tracks the JAVA_SDK_VERSION breeze constant)"  # yamllint disable-line 
rule:line-length
+        type: string
+        default: "17"
 
   workflow_call:
     inputs:
@@ -83,6 +87,10 @@ on:  # yamllint disable-line rule:truthy
         description: "Test mode - basic, remote_log, remote_log_elasticsearch, 
remote_log_opensearch, xcom_object_storage, event_driven, java_sdk, or go_sdk"  
# yamllint disable-line rule:line-length
         type: string
         default: "basic"
+      java-sdk-version:
+        description: "JDK version used to build the Java SDK bundles natively 
(tracks the JAVA_SDK_VERSION breeze constant)"  # yamllint disable-line 
rule:line-length
+        type: string
+        default: "17"
 
 jobs:
   test-e2e-integration-tests:
@@ -113,30 +121,42 @@ jobs:
           use-uv: ${{ inputs.use-uv }}
           make-mnt-writeable-and-cleanup: true
         id: breeze
+      # Only the java_sdk mode runs the Gradle builds; every other e2e mode 
skips the
+      # steps below. LANG_SDK_NATIVE_TOOLCHAIN=true (the same switch the 
lang-SDK k8s
+      # job uses) makes the e2e conftest build with the host toolchain 
provisioned
+      # here instead of ephemeral eclipse-temurin containers, skipping the
+      # toolchain-image pull; local runs keep the containerized build. Without 
the
+      # cache each run re-downloads the example bundles' full dependency tree 
(Spark
+      # alone is ~1GB) into a fresh ~/.gradle. The key carries a ``-vN-`` salt 
(bump
+      # to force-invalidate a poisoned cache) and runner.arch keys the cache 
per
+      # architecture: ~/.gradle/jdks holds arch-specific auto-provisioned JDKs 
(the
+      # example pins a Java 11 toolchain) and this job runs on amd64 and arm64.
+      - name: "Setup Java for the Java SDK build"
+        if: inputs.e2e_test_mode == 'java_sdk'
+        uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520  # 
v5.4.0
+        with:
+          distribution: 'temurin'
+          java-version: ${{ inputs.java-sdk-version }}
       - name: "Restore Java SDK Gradle dependency cache"
-        # Only the java_sdk mode runs the Gradle builds; every other e2e mode 
skips
-        # this. Without it each java_sdk run re-downloads the example bundles' 
full
-        # dependency tree (Spark alone is ~1GB) into a fresh GRADLE_USER_HOME.
-        # runner.arch keys the cache per architecture: .gradle/jdks holds
-        # arch-specific auto-provisioned JDKs and this job runs on amd64 and 
arm64.
         if: inputs.e2e_test_mode == 'java_sdk'
         id: gradle-cache
         uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9  
# v6.1.0
         with:
           path: |
-            java-sdk/.gradle/caches
-            java-sdk/.gradle/wrapper
-            java-sdk/.gradle/jdks
-            !java-sdk/.gradle/caches/*.lock
-            !java-sdk/.gradle/caches/journal-1
-          key: java-sdk-gradle-${{ runner.os }}-${{ runner.arch }}-${{ 
hashFiles('java-sdk/**/*.gradle*', 'java-sdk/**/gradle.properties', 
'java-sdk/gradle/wrapper/gradle-wrapper.properties', 
'java-sdk/gradle/libs.versions.toml') }}  # yamllint disable-line 
rule:line-length
+            ~/.gradle/caches
+            ~/.gradle/wrapper
+            ~/.gradle/jdks
+            !~/.gradle/caches/*.lock
+            !~/.gradle/caches/journal-1
+          key: e2e-java-sdk-gradle-v1-${{ runner.os }}-${{ runner.arch }}-${{ 
hashFiles('java-sdk/**/*.gradle*', 'java-sdk/**/gradle.properties', 
'java-sdk/gradle/wrapper/gradle-wrapper.properties', 
'java-sdk/gradle/libs.versions.toml') }}  # yamllint disable-line 
rule:line-length
           restore-keys: |
-            java-sdk-gradle-${{ runner.os }}-${{ runner.arch }}-
+            e2e-java-sdk-gradle-v1-${{ runner.os }}-${{ runner.arch }}-
       - name: "Test e2e integration tests"
         run: breeze testing airflow-e2e-tests
         env:
           DOCKER_IMAGE: "${{ inputs.docker-image-tag }}"
           E2E_TEST_MODE: "${{ inputs.e2e_test_mode }}"
+          LANG_SDK_NATIVE_TOOLCHAIN: "${{ inputs.e2e_test_mode == 'java_sdk' 
&& 'true' || 'false' }}"
       - name: "Save Java SDK Gradle dependency cache"
         # Saved even when the e2e tests fail: the Gradle warm-up is 
independent of
         # test outcome, and actions/cache's post step would drop it on every 
red run.
@@ -144,11 +164,11 @@ jobs:
         uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9  # 
v6.1.0
         with:
           path: |
-            java-sdk/.gradle/caches
-            java-sdk/.gradle/wrapper
-            java-sdk/.gradle/jdks
-            !java-sdk/.gradle/caches/*.lock
-            !java-sdk/.gradle/caches/journal-1
+            ~/.gradle/caches
+            ~/.gradle/wrapper
+            ~/.gradle/jdks
+            !~/.gradle/caches/*.lock
+            !~/.gradle/caches/journal-1
           key: ${{ steps.gradle-cache.outputs.cache-primary-key }}
       - name: Zip logs
         run: |
diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd.yml
index ad0b70da871..fd2892027ff 100644
--- a/.github/workflows/ci-amd.yml
+++ b/.github/workflows/ci-amd.yml
@@ -920,6 +920,7 @@ jobs:
       run-remote-logging-s3-e2e-tests: ${{ 
needs.build-info.outputs.run-remote-logging-s3-e2e-tests }}
       run-event-driven-e2e-tests: ${{ 
needs.build-info.outputs.run-event-driven-e2e-tests }}
       run-java-sdk-e2e-tests: ${{ 
needs.build-info.outputs.run-java-sdk-e2e-tests }}
+      java-sdk-version: ${{ needs.build-info.outputs.java-sdk-version }}
       run-go-sdk-e2e-tests: ${{ needs.build-info.outputs.run-go-sdk-e2e-tests 
}}
       run-openlineage-e2e-tests: ${{ 
needs.build-info.outputs.run-openlineage-e2e-tests }}
       run-openlineage-e2e-compat-tests: ${{ 
needs.build-info.outputs.run-openlineage-e2e-compat-tests }}
diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml
index ee7ffdd916d..ed2da6cf434 100644
--- a/.github/workflows/ci-arm.yml
+++ b/.github/workflows/ci-arm.yml
@@ -909,6 +909,7 @@ jobs:
       run-remote-logging-s3-e2e-tests: ${{ 
needs.build-info.outputs.run-remote-logging-s3-e2e-tests }}
       run-event-driven-e2e-tests: ${{ 
needs.build-info.outputs.run-event-driven-e2e-tests }}
       run-java-sdk-e2e-tests: ${{ 
needs.build-info.outputs.run-java-sdk-e2e-tests }}
+      java-sdk-version: ${{ needs.build-info.outputs.java-sdk-version }}
       run-go-sdk-e2e-tests: ${{ needs.build-info.outputs.run-go-sdk-e2e-tests 
}}
       run-openlineage-e2e-tests: ${{ 
needs.build-info.outputs.run-openlineage-e2e-tests }}
       run-openlineage-e2e-compat-tests: ${{ 
needs.build-info.outputs.run-openlineage-e2e-compat-tests }}
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py 
b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
index 5b5883980c5..56b51992bb4 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
@@ -49,7 +49,9 @@ from airflow_e2e_tests.constants import (
     JAVA_SDK_EXAMPLE_DAGS_PATH,
     JAVA_SDK_EXAMPLE_LIBS_PATH,
     JAVA_SDK_MAVEN_CACHE_PATH,
+    JAVA_SDK_ROOT_PATH,
     KAFKA_DIR_PATH,
+    LANG_SDK_NATIVE_TOOLCHAIN,
     LOCALSTACK_PATH,
     LOGS_FOLDER,
     OPENLINEAGE_COMPOSE_PATH,
@@ -281,8 +283,16 @@ _SPARK_JAVA_MODULE_OPTIONS = [
 ]
 
 
-def _run_java_sdk_gradle_container(workdir, *gradle_argv, 
capture_output=False):
-    """Run the Java SDK Gradle wrapper inside the pinned JDK container.
+def _run_java_sdk_gradle(workdir, *gradle_argv, capture_output=False, 
native=False):
+    """Run the Java SDK Gradle wrapper natively or inside the pinned JDK 
container.
+
+    In ``native`` mode (used in CI, where the host already has a cached JDK +
+    Gradle cache via ``actions/setup-java``) it invokes the host ``./gradlew``
+    directly, skipping the toolchain-image pull and the container workarounds
+    below while reusing the runner's ~/.gradle cache.
+
+    The containerized path stays the default for local runs so a dev host needs
+    no JDK installed:
 
     * --user keeps build outputs owned by the current user (not root).
     * --network=host shares one loopback across concurrent builds so Gradle's
@@ -300,8 +310,18 @@ def _run_java_sdk_gradle_container(workdir, *gradle_argv, 
capture_output=False):
     * files/m2 is mounted directly as ~/.m2 so publishToMavenLocal writes
       there without nesting, and its contents are visible on the host.
     """
-    return subprocess.run(
-        [
+    if native:
+        cwd = workdir
+        argv = [
+            str(JAVA_SDK_ROOT_PATH / "gradlew"),
+            "--no-daemon",
+            "--console=plain",
+            *gradle_argv,
+        ]
+    else:
+        JAVA_SDK_MAVEN_CACHE_PATH.mkdir(parents=True, exist_ok=True)
+        cwd = None
+        argv = [
             "docker",
             "run",
             "--rm",
@@ -317,22 +337,19 @@ def _run_java_sdk_gradle_container(workdir, *gradle_argv, 
capture_output=False):
             "-v",
             f"{AIRFLOW_ROOT_PATH}:/repo",
             "-w",
-            workdir,
+            f"/repo/{workdir.relative_to(AIRFLOW_ROOT_PATH)}",
             "eclipse-temurin:17-jdk",
             "/repo/java-sdk/gradlew",
             "--no-daemon",
             *gradle_argv,
-        ],
-        check=True,
-        capture_output=capture_output,
-        text=True,
-    )
+        ]
+    return subprocess.run(argv, cwd=cwd, check=True, 
capture_output=capture_output, text=True)
 
 
-def _build_example_bundle(workdir):
+def _build_example_bundle(workdir, *, native=False):
     """Build one example bundle, capturing output so concurrent builds don't 
interleave."""
     try:
-        completed = _run_java_sdk_gradle_container(workdir, "bundle", 
capture_output=True)
+        completed = _run_java_sdk_gradle(workdir, "bundle", 
capture_output=True, native=native)
     except subprocess.CalledProcessError as e:
         console.print(f"[red]Bundle build failed in {workdir}:")
         console.print(e.stdout, e.stderr, sep="\n", markup=False, 
soft_wrap=True)
@@ -348,14 +365,15 @@ def _setup_java_sdk_integration(dot_env_file, tmp_dir):
     then builds a Java-capable Airflow worker image, copies the JARs into the
     temp directory, and writes the coordinator configuration.
     """
+    native = LANG_SDK_NATIVE_TOOLCHAIN
     console.print("[yellow]Publishing Java SDK artifacts to local Maven 
repository...")
-    JAVA_SDK_MAVEN_CACHE_PATH.mkdir(parents=True, exist_ok=True)
-    _run_java_sdk_gradle_container("/repo/java-sdk", "publishToMavenLocal", 
"-PskipSigning=true")
+    _run_java_sdk_gradle(JAVA_SDK_ROOT_PATH, "publishToMavenLocal", 
"-PskipSigning=true", native=native)
 
     # The example and scala_spark_example are independent Gradle builds that 
both
     # consume the SDK artifact published above, so build them concurrently. 
Sharing
-    # a writable Gradle user home between concurrent builds is safe only 
because
-    # --network=host lets each build ping the other's lock-owner port (see the
+    # a writable Gradle user home between concurrent builds is safe because 
each
+    # build can ping the other's lock-owner port over one shared loopback - the
+    # host's own in native mode, --network=host in the container path (see the
     # helper's docstring); publishToMavenLocal has already unpacked the shared
     # wrapper distribution, so neither build races to fetch it.
     #
@@ -365,15 +383,16 @@ def _setup_java_sdk_integration(dot_env_file, tmp_dir):
     # start each bundle from an empty directory.
     rmtree(JAVA_SDK_EXAMPLE_LIBS_PATH, ignore_errors=True)
     rmtree(SCALA_SPARK_EXAMPLE_LIBS_PATH, ignore_errors=True)
-    console.print(
-        "[yellow]Building Java SDK and Scala Spark example bundles 
concurrently (eclipse-temurin:17-jdk)..."
-    )
+    toolchain = "host toolchain" if native else "eclipse-temurin:17-jdk"
+    console.print(f"[yellow]Building Java SDK and Scala Spark example bundles 
concurrently ({toolchain})...")
     example_bundle_workdirs = [
-        "/repo/java-sdk/example",
-        "/repo/java-sdk/scala_spark_example",
+        JAVA_SDK_ROOT_PATH / "example",
+        JAVA_SDK_ROOT_PATH / "scala_spark_example",
     ]
     with ThreadPoolExecutor(max_workers=len(example_bundle_workdirs)) as pool:
-        bundle_builds = [pool.submit(_build_example_bundle, workdir) for 
workdir in example_bundle_workdirs]
+        bundle_builds = [
+            pool.submit(_build_example_bundle, workdir, native=native) for 
workdir in example_bundle_workdirs
+        ]
         for build in bundle_builds:
             build.result()
 
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py 
b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
index b0392d96a8a..ec1732ac75a 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
@@ -54,6 +54,10 @@ KAFKA_DIR_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / 
"docker" / "kafka"
 # openlineage_tests/prepare_dags.py; the overlay carries the 
OpenLineage-specific env + dag_doc mount.
 OPENLINEAGE_COMPOSE_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "docker" 
/ "openlineage.yml"
 
+# CI sets this (the same switch the lang-SDK k8s job uses) to build the 
lang-SDK
+# artifacts with the host toolchain instead of ephemeral toolchain containers.
+LANG_SDK_NATIVE_TOOLCHAIN = os.environ.get("LANG_SDK_NATIVE_TOOLCHAIN", 
"").lower() == "true"
+
 # Java SDK E2E test paths
 JAVA_SDK_ROOT_PATH = AIRFLOW_ROOT_PATH / "java-sdk"
 JAVA_SDK_EXAMPLE_DAGS_PATH = JAVA_SDK_ROOT_PATH / "example" / "src" / 
"resources" / "dags"

Reply via email to