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

uranusjr 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 390fc454bb2 Java gradle plugin and project split (#68380)
390fc454bb2 is described below

commit 390fc454bb28bcd51dfa583cf79f43bad9440dbe
Author: Tzu-ping Chung <[email protected]>
AuthorDate: Fri Jun 12 17:26:52 2026 +0800

    Java gradle plugin and project split (#68380)
---
 .github/workflows/ci-amd.yml                       |   2 +-
 .github/workflows/ci-arm.yml                       |   2 +-
 .github/workflows/codeql-analysis.yml              |   2 +-
 .../language-sdks/java.rst                         | 205 +++++++++++++++++--
 .../tests/airflow_e2e_tests/conftest.py            |  58 ++++--
 .../tests/airflow_e2e_tests/constants.py           |   5 +-
 .../airflow_breeze/commands/developer_commands.py  |   2 +-
 java-sdk/README.md                                 |  48 +++--
 java-sdk/{example => bom}/build.gradle.kts         |  37 ++--
 java-sdk/build.gradle.kts                          |  61 ------
 java-sdk/{example => buildSrc}/build.gradle.kts    |  29 +--
 .../main/kotlin/airflow-jvm-conventions.gradle.kts |  58 ++++++
 .../src/main/kotlin/airflow-publish.gradle.kts     | 112 +++++++++++
 .../example/{build.gradle.kts => build.gradle}     |  28 +--
 java-sdk/example/gradle.properties                 |   1 +
 .../example/{build.gradle.kts => settings.gradle}  |  31 +--
 .../src/resources}/dags/java_examples.py           |   0
 java-sdk/gradle.properties                         |   2 +-
 java-sdk/plugin/build.gradle.kts                   |  57 ++++++
 .../apache/airflow/sdk/plugin/AirflowSdkPlugin.kt  | 221 +++++++++++++++++++++
 java-sdk/{example => processor}/build.gradle.kts   |  38 ++--
 .../org/apache/airflow/sdk/BuilderProcessor.kt}    |  70 -------
 .../services/javax.annotation.processing.Processor |   0
 .../kotlin/org/apache/airflow/sdk/BuilderTest.kt   |   0
 java-sdk/sdk/build.gradle.kts                      |  84 +-------
 .../main/kotlin/org/apache/airflow/sdk/Builder.kt  | 192 +-----------------
 java-sdk/settings.gradle.kts                       |  20 +-
 scripts/docker/install_jdk.sh                      |   2 +-
 28 files changed, 824 insertions(+), 543 deletions(-)

diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd.yml
index f411d43d2b3..7329cbda22a 100644
--- a/.github/workflows/ci-amd.yml
+++ b/.github/workflows/ci-amd.yml
@@ -55,7 +55,7 @@ env:
   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
   GITHUB_USERNAME: ${{ github.actor }}
   # Keep these in sync:
-  # - jvmTarget, languageVersion, and sourceCompatibility in 
java-sdk/build.gradle.kts
+  # - jvmTarget, languageVersion, and sourceCompatibility in java-sdk/buildSrc
   # - TEMURIN_VERSION in scripts/docker/install_jdk.sh
   # - JAVA_VERSION in .github/workflows/ci-amd.yml and 
.github/workflows/ci-arm.yml
   # - java-version in .github/workflows/codeql-analysis.yml
diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml
index 732d1e261ca..73f06289094 100644
--- a/.github/workflows/ci-arm.yml
+++ b/.github/workflows/ci-arm.yml
@@ -45,7 +45,7 @@ env:
   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
   GITHUB_USERNAME: ${{ github.actor }}
   # Keep these in sync:
-  # - jvmTarget, languageVersion, and sourceCompatibility in 
java-sdk/build.gradle.kts
+  # - jvmTarget, languageVersion, and sourceCompatibility in java-sdk/buildSrc
   # - TEMURIN_VERSION in scripts/docker/install_jdk.sh
   # - JAVA_VERSION in .github/workflows/ci-amd.yml and 
.github/workflows/ci-arm.yml
   # - java-version in .github/workflows/codeql-analysis.yml
diff --git a/.github/workflows/codeql-analysis.yml 
b/.github/workflows/codeql-analysis.yml
index 6856afa12b1..9d660f18766 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -115,7 +115,7 @@ jobs:
           persist-credentials: false
 
       # Keep these in sync:
-      # - jvmTarget, languageVersion, and sourceCompatibility in 
java-sdk/build.gradle.kts
+      # - jvmTarget, languageVersion, and sourceCompatibility in 
java-sdk/buildSrc
       # - TEMURIN_VERSION in scripts/docker/install_jdk.sh
       # - JAVA_VERSION in .github/workflows/ci-amd.yml and 
.github/workflows/ci-arm.yml
       # - java-version in .github/workflows/codeql-analysis.yml
diff --git a/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst 
b/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
index a752bee8017..380ec763733 100644
--- a/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
+++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
@@ -272,25 +272,40 @@ represented as Java objects when read back via 
``getXCom``.
 Building and packaging
 -----------------------
 
-The Java SDK is distributed as a JAR. Use any build tool; Gradle is shown here.
+The Java SDK is distributed as a JAR. The sections below show how to build a 
bundle with Gradle or Maven.
 
-**Gradle setup**
+.. _java-sdk/build/gradle:
 
-Add the SDK dependency to your ``build.gradle.kts``:
+Gradle
+~~~~~~
 
-.. code-block:: kotlin
+Apply the Airflow SDK Gradle plugin in your ``build.gradle``:
+
+.. code-block:: groovy
+
+    plugins {
+        id("org.apache.airflow.sdk") version "${version}"
+    }
 
     dependencies {
-      implementation("org.apache.airflow:airflow-java-sdk:<version>")
-      annotationProcessor("org.apache.airflow:airflow-java-sdk:<version>")
+        
annotationProcessor("org.apache.airflow:airflow-sdk-processor:${version}")
+        implementation("org.apache.airflow:airflow-sdk:${version}")
     }
 
-    tasks.withType<Jar> {
-      manifest {
-        attributes("Main-Class" to "com.example.Main")
-      }
+    airflowBundle {
+        mainClass = "com.example.Main"  // Point to your main class instead.
     }
 
+Then run:
+
+.. code-block:: bash
+
+    ./gradlew bundle
+
+The ``build/bundle/`` directory contains all required JAR(s). Copy or mount it 
into the directory pointed to
+by ``jars_root`` in the coordinator configuration. 
:class:`~airflow.sdk.coordinators.java.JavaCoordinator`
+scans ``jars_root`` recursively and builds the classpath automatically.
+
 .. note::
 
   You only need the ``annotationProcessor`` entry if you use the 
annotation-based API. It is not needed for
@@ -298,20 +313,172 @@ Add the SDK dependency to your ``build.gradle.kts``:
 
 .. note::
 
-  The ``Main-Class`` manifest value is needed for the coordinator to know how 
to run the JAR. You can choose
-  to set this *on the coordinator itself* too by adding the ``main_class`` 
kwarg in coordinator configuration.
+  The plugin generates a fat JAR with the `Shadow 
<https://gradleup.com/shadow/>`__ plugin by default. This is
+  generally a good idea since you only deploy one JAR file to avoid dependency 
issues between projects. If this
+  does not suit you, set ``fatJar = false`` in ``airflowBundle`` to produce 
thin JARs instead. The rest of the
+  process stays the same, but you will need to put all dependency JARs 
somewhere Airflow can find with
+  ``jars_root``.
+
+.. _java-sdk/build/maven:
+
+Maven
+~~~~~
+
+Import the ``airflow-sdk-bom`` Bill of Materials so that artifact versions and 
the
+``${airflow.supervisor.schema.version}`` property are managed in one place:
+
+.. code-block:: xml
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.airflow</groupId>
+                <artifactId>airflow-sdk-bom</artifactId>
+                <version>${version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+Add the SDK as a dependency (version is managed by the BOM):
+
+.. code-block:: xml
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.airflow</groupId>
+            <artifactId>airflow-sdk</artifactId>
+        </dependency>
+    </dependencies>
+
+Wire the annotation processor through ``maven-compiler-plugin`` so it stays 
off the runtime classpath:
+
+.. code-block:: xml
+
+    <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+            <annotationProcessorPaths>
+                <path>
+                    <groupId>org.apache.airflow</groupId>
+                    <artifactId>airflow-sdk-processor</artifactId>
+                    <version>${version}</version>
+                </path>
+            </annotationProcessorPaths>
+        </configuration>
+    </plugin>
+
+**Option 1 (recommended): fat JAR**
+
+Use ``maven-shade-plugin`` to bundle your code and all dependencies into a 
single JAR. This is the
+simplest deployment: one file, no dependency management at runtime.
+
+.. code-block:: xml
+
+    <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>3.6.0</version>
+        <executions>
+            <execution>
+                <phase>package</phase>
+                <goals><goal>shade</goal></goals>
+                <configuration>
+                    <transformers>
+                        <transformer 
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                            <!-- Replace with your BundleBuilder 
implementation. -->
+                            <mainClass>com.example.Main</mainClass>
+                            <manifestEntries>
+                                <!-- Resolved from the BOM; do not hard-code 
this value. -->
+                                
<Airflow-Supervisor-Schema-Version>${airflow.supervisor.schema.version}</Airflow-Supervisor-Schema-Version>
+                            </manifestEntries>
+                        </transformer>
+                    </transformers>
+                </configuration>
+            </execution>
+        </executions>
+    </plugin>
+
+Then run:
 
-Building a distribution
-~~~~~~~~~~~~~~~~~~~~~~~
+.. code-block:: bash
+
+    mvn package
+
+The fat JAR is written to ``target/<artifactId>-<version>.jar``. Copy it to 
the directory configured as
+``jars_root`` in your coordinator.
+
+**Option 2: thin JAR with separate dependencies**
+
+If a fat JAR does not suit your project, use ``maven-jar-plugin`` to set 
``Main-Class`` on the regular
+JAR and ``maven-dependency-plugin`` to collect all runtime dependencies 
alongside it. Note that
+``Airflow-Supervisor-Schema-Version`` does not need to be set here since 
Airflow reads it directly from the
+``airflow-sdk`` JAR on the classpath.
+
+.. code-block:: xml
+
+    <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+            <archive>
+                <manifestEntries>
+                    <Main-Class>com.example.Main</Main-Class>
+                </manifestEntries>
+            </archive>
+        </configuration>
+    </plugin>
+
+    <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <executions>
+            <execution>
+                <id>copy-dependencies</id>
+                <phase>package</phase>
+                <goals><goal>copy-dependencies</goal></goals>
+                <configuration>
+                    
<outputDirectory>${project.build.directory}/bundle</outputDirectory>
+                    <includeScope>runtime</includeScope>
+                </configuration>
+            </execution>
+            <execution>
+                <id>copy-artifact</id>
+                <phase>package</phase>
+                <goals><goal>copy</goal></goals>
+                <configuration>
+                    <artifactItems>
+                        <artifactItem>
+                            <groupId>${project.groupId}</groupId>
+                            <artifactId>${project.artifactId}</artifactId>
+                            <version>${project.version}</version>
+                            
<outputDirectory>${project.build.directory}/bundle</outputDirectory>
+                        </artifactItem>
+                    </artifactItems>
+                </configuration>
+            </execution>
+        </executions>
+    </plugin>
+
+Then run:
 
 .. code-block:: bash
 
-    ./gradlew :myproject:installDist
+    mvn package
+
+``target/bundle/`` will contain the thin JAR and all runtime dependency JARs. 
Point ``jars_root`` at
+this directory.
+
+.. note::
+
+  You only need the ``annotationProcessorPaths`` entry if you use the 
annotation-based API.
+
+.. note::
 
-The ``lib/`` directory of the resulting distribution contains all required 
JARs. Copy or mount it into the
-directory pointed to by ``jars_root`` in the coordinator configuration.
-:class:`~airflow.sdk.coordinators.java.JavaCoordinator` scans ``jars_root``
-recursively and builds the classpath automatically.
+  Unlike the Gradle plugin, Maven has no equivalent of the 
``verifyBundleMainClass`` validation step.
+  A wrong ``<mainClass>`` value will not be caught until runtime.
 
 .. _java-sdk/coordinator-config:
 
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py 
b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
index 9d6f12d0f73..ce65c6fc9a4 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/conftest.py
@@ -45,8 +45,9 @@ from airflow_e2e_tests.constants import (
     GO_SDK_EXAMPLE_BUNDLE_PKG,
     JAVA_COMPOSE_PATH,
     JAVA_DOCKERFILE_PATH,
-    JAVA_SDK_DAGS_PATH,
+    JAVA_SDK_EXAMPLE_DAGS_PATH,
     JAVA_SDK_EXAMPLE_LIBS_PATH,
+    JAVA_SDK_MAVEN_CACHE_PATH,
     KAFKA_DIR_PATH,
     LOCALSTACK_PATH,
     LOGS_FOLDER,
@@ -253,15 +254,17 @@ def _setup_java_sdk_integration(dot_env_file, tmp_dir):
     Java-capable Airflow worker image, copies the JARs into the temp directory,
     and writes the coordinator configuration.
     """
-    # Build the example bundle inside an ephemeral JDK container so the host
-    # does not need Java installed.
-    #
-    # --user keeps build outputs owned by the current user (not root).
-    # GRADLE_USER_HOME persists the Gradle distribution and dependency cache in
-    # java-sdk/.gradle/ (already gitignored) so the first run downloads once
-    # and subsequent runs skip straight to compilation.
-    # --no-daemon avoids a background JVM that would outlive the container.
-    console.print("[yellow]Building Java SDK example bundle 
(eclipse-temurin:17-jdk)...")
+    # * --user keeps build outputs owned by the current user (not root).
+    # * --no-daemon avoids a background JVM that would outlive the container.
+    # * GRADLE_USER_HOME persists the Gradle distribution and dependency cache
+    #   in java-sdk/.gradle/ so subsequent runs skip straight to compilation.
+    # * HOME is set explicitly because --user runs as the host UID which has no
+    #   entry in the container's /etc/passwd; Docker would otherwise inherit 
the
+    #   image's HOME (/root) which the non-root process cannot write to.
+    # * files/m2 is mounted directly as ~/.m2 so publishToMavenLocal writes
+    #   there without nesting, and its contents are visible on the host.
+    console.print("[yellow]Publishing Java SDK artifacts to local Maven 
repository...")
+    JAVA_SDK_MAVEN_CACHE_PATH.mkdir(parents=True, exist_ok=True)
     subprocess.run(
         [
             "docker",
@@ -271,14 +274,43 @@ def _setup_java_sdk_integration(dot_env_file, tmp_dir):
             f"{os.getuid()}:{os.getgid()}",
             "-e",
             "GRADLE_USER_HOME=/repo/java-sdk/.gradle",
-            # Mount java-sdk/ at /java-sdk (the Gradle root project).
+            "-e",
+            "HOME=/workspace-home",
+            "-v",
+            f"{JAVA_SDK_MAVEN_CACHE_PATH}:/workspace-home/.m2",
             "-v",
             f"{AIRFLOW_ROOT_PATH}:/repo",
             "-w",
             "/repo/java-sdk",
             "eclipse-temurin:17-jdk",
             "./gradlew",
-            ":example:installDist",
+            "publishToMavenLocal",
+            "-PskipSigning=true",
+            "--no-daemon",
+        ],
+        check=True,
+    )
+    console.print("[yellow]Building Java SDK example bundle 
(eclipse-temurin:17-jdk)...")
+    subprocess.run(
+        [
+            "docker",
+            "run",
+            "--rm",
+            "--user",
+            f"{os.getuid()}:{os.getgid()}",
+            "-e",
+            "GRADLE_USER_HOME=/repo/java-sdk/.gradle",
+            "-e",
+            "HOME=/workspace-home",
+            "-v",
+            f"{JAVA_SDK_MAVEN_CACHE_PATH}:/workspace-home/.m2",
+            "-v",
+            f"{AIRFLOW_ROOT_PATH}:/repo",
+            "-w",
+            "/repo/java-sdk/example",
+            "eclipse-temurin:17-jdk",
+            "../gradlew",
+            "bundle",
             "--no-daemon",
         ],
         check=True,
@@ -293,7 +325,7 @@ def _setup_java_sdk_integration(dot_env_file, tmp_dir):
     copytree(JAVA_SDK_EXAMPLE_LIBS_PATH, tmp_dir / "jars")
 
     # Copy the Java SDK example Dag file so Airflow can discover it.
-    copyfile(JAVA_SDK_DAGS_PATH / "java_examples.py", tmp_dir / "dags" / 
"java_examples.py")
+    copyfile(JAVA_SDK_EXAMPLE_DAGS_PATH / "java_examples.py", tmp_dir / "dags" 
/ "java_examples.py")
 
     # Build a local Docker image that extends DOCKER_IMAGE with a JRE.
     # We do this explicitly so testcontainers' DockerCompose.start() does not
diff --git a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py 
b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
index 259f8256fd8..ae3b64a94fa 100644
--- a/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
+++ b/airflow-e2e-tests/tests/airflow_e2e_tests/constants.py
@@ -52,8 +52,9 @@ KAFKA_DIR_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / 
"docker" / "kafka"
 
 # Java SDK E2E test paths
 JAVA_SDK_ROOT_PATH = AIRFLOW_ROOT_PATH / "java-sdk"
-JAVA_SDK_DAGS_PATH = JAVA_SDK_ROOT_PATH / "dags"
-JAVA_SDK_EXAMPLE_LIBS_PATH = JAVA_SDK_ROOT_PATH / "example" / "build" / 
"install" / "example" / "lib"
+JAVA_SDK_EXAMPLE_DAGS_PATH = JAVA_SDK_ROOT_PATH / "example" / "src" / 
"resources" / "dags"
+JAVA_SDK_EXAMPLE_LIBS_PATH = JAVA_SDK_ROOT_PATH / "example" / "build" / 
"bundle"
+JAVA_SDK_MAVEN_CACHE_PATH = AIRFLOW_ROOT_PATH / "files" / "m2"
 JAVA_COMPOSE_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "docker" / 
"java.yml"
 JAVA_DOCKERFILE_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests" / "docker" / 
"Dockerfile.java"
 
diff --git a/dev/breeze/src/airflow_breeze/commands/developer_commands.py 
b/dev/breeze/src/airflow_breeze/commands/developer_commands.py
index 3768a3e2461..5e76822a35b 100644
--- a/dev/breeze/src/airflow_breeze/commands/developer_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/developer_commands.py
@@ -763,7 +763,7 @@ def _get_java_sdk_version() -> str:
     """Read the Java SDK version from 'java-sdk/gradle.properties'."""
     props_path = AIRFLOW_ROOT_PATH / "java-sdk" / "gradle.properties"
     for line in props_path.read_text().splitlines():
-        if match := re.match(r"^sdkVersion\s*=\s*(\S+)$", line.strip()):
+        if match := re.match(r"^projectVersion\s*=\s*(\S+)$", line.strip()):
             return match.group(1)
     raise RuntimeError(f"Java SDK version not found in {props_path}")
 
diff --git a/java-sdk/README.md b/java-sdk/README.md
index 5123a594022..9d45f59d32e 100644
--- a/java-sdk/README.md
+++ b/java-sdk/README.md
@@ -43,16 +43,26 @@ This generates both an HTML representation and Javadoc.
 
 ## Running the example
 
-* Put the [DAG with stub tasks](./dags) to somewhere Airflow can find.
+The SDK projects must first built and published:
+
+```bash
+./gradlew publishToMavenLocal -PskipSigning=true
+```
+
+After the build is successful, you should be able to see directories in 
`~/.m2/repository/org/apache/airflow/`.
+
+Now `cd example` into the example project, and
+
+* Put the [DAG with stub tasks](./example/src/resources/dags) to somewhere 
Airflow can find.
 
 * Ensure the `java` command is available in the same environment the Airflow
   task worker is in.
 
-* Package the example and its dependencies into JARs in
-  `./example/build/install/example/lib`
+* Package the example to `./example/build/bundle`
 
   ```bash
-  ./gradlew :example:installDist
+  # We're now in the 'example' directory, so gradlew is in parent.
+  ../gradlew bundle
   ```
 
 * Configure Airflow to route tasks in the *java* queue to be run with Java:
@@ -61,7 +71,7 @@ This generates both an HTML representation and Javadoc.
   export AIRFLOW__SDK__COORDINATORS='{
     "java": {
       "classpath": "airflow.sdk.coordinators.java.JavaCoordinator",
-      "kwargs": {"jars_root": 
["/opt/airflow/java-sdk/example/build/install/example/lib"]}
+      "kwargs": {"jars_root": ["/opt/airflow/java-sdk/example/build/bundle"]}
     }
   }'
   export AIRFLOW__SDK__QUEUE_TO_COORDINATOR='{"java": "java"}'
@@ -101,7 +111,7 @@ The full release process follows the
 Edit `gradle.properties` and set the version for this release:
 
 ```properties
-sdkVersion=1.0.0
+projectVersion=1.0.0
 ```
 
 Commit the change and push it to the release branch.
@@ -112,8 +122,21 @@ Before touching any remote repository, publish to your 
local Maven cache and
 inspect the generated POM:
 
 ```bash
-./gradlew :sdk:publishToMavenLocal
-cat ~/.m2/repository/org/apache/airflow/airflow-sdk/*/airflow-sdk-*.pom
+rm -rf ~/.m2/repository/org/apache/airflow/  # Start clean.
+
+./gradlew publishToMavenLocal -PskipSigning=true
+
+# The airflow-sdk runtime.
+less ~/.m2/repository/org/apache/airflow/airflow-sdk/*/airflow-sdk-*.pom
+
+# The annotation processor for the builder pattern.
+less 
~/.m2/repository/org/apache/airflow/airflow-sdk-processor/*/airflow-sdk-*.pom
+
+# The Gradle plugin for bundling.
+less 
~/.m2/repository/org/apache/airflow/airflow-sdk-gradle-plugin/*/airflow-sdk-*.pom
+
+# The Gradle plugin's registration.
+less 
~/.m2/repository/org/apache/airflow/sdk/org.apache.airflow.sdk.gradle.plugin/*/*.pom
 ```
 
 Check that the coordinates, description, license, SCM, and organization fields
@@ -125,13 +148,14 @@ To test the full publish flow without touching ASF 
infrastructure, override the
 repository URL to a local directory
 
 ```bash
-./gradlew :sdk:publish -PmavenUrl=file:///tmp/local-maven-repo -PskipSigning
-ls /tmp/local-maven-repo/org/apache/airflow/airflow-sdk/
+./gradlew publish -PmavenUrl=file:///tmp/local-maven-repo -PskipSigning=true
+ls /tmp/local-maven-repo/org/apache/airflow/
+# This should contain the same components in ~/.m2 as inspected in the 
previous step.
 ```
 
 *NOTE:* Signing is not required since nothing goes to Maven Central. If you 
want
 to test signing, set the GPG private key and passphrase as described in the 
next
-section, and remove `-PskipSigning` from the above command.
+section, and remove `-PskipSigning=true` from the above command.
 
 ### Publish to ASF Nexus staging
 
@@ -147,7 +171,7 @@ signing.password=your-gpg-key-passphrase
 Then run the publish task.
 
 ```bash
-./gradlew :sdk:publish -P"signing.key=$(gpg --armor --export-secret-keys 
your-gpg-key-fingerprint)"
+./gradlew publish -P"signing.key=$(gpg --armor --export-secret-keys 
your-gpg-key-fingerprint)"
 ```
 
 *NOTE:* The signing key is supplied through the command line since it contains
diff --git a/java-sdk/example/build.gradle.kts b/java-sdk/bom/build.gradle.kts
similarity index 53%
copy from java-sdk/example/build.gradle.kts
copy to java-sdk/bom/build.gradle.kts
index 42fa5b4ca91..479cbdde27f 100644
--- a/java-sdk/example/build.gradle.kts
+++ b/java-sdk/bom/build.gradle.kts
@@ -18,29 +18,30 @@
  */
 
 plugins {
-    application
+    `java-platform`
+    id("airflow-publish")
 }
 
-dependencies {
-    annotationProcessor(project(":sdk"))
-    implementation(project(":sdk"))
-    implementation("org.slf4j:slf4j-simple:2.0.17")
-}
+val projectVersion: String by project
+val airflowSupervisorSchemaVersion: String by project
 
-sourceSets {
-    main {
-        java.srcDir("src/java")
+dependencies {
+    constraints {
+        api("org.apache.airflow:airflow-sdk:$projectVersion")
+        api("org.apache.airflow:airflow-sdk-processor:$projectVersion")
     }
 }
 
-application {
-    mainClass = "org.apache.airflow.example.ExampleBundleBuilder"
-}
-
-tasks.withType<Jar> {
-    manifest {
-        attributes(
-            "Main-Class" to application.mainClass.get(),
-        )
+publishing {
+    publications {
+        create<MavenPublication>("mavenBom") {
+            artifactId = "airflow-sdk-bom"
+            from(components["javaPlatform"])
+            pom {
+                name = "Apache Airflow Java SDK BOM"
+                description = "Bill of Materials for the Apache Airflow Java 
SDK."
+                properties.put("airflow.supervisor.schema.version", 
airflowSupervisorSchemaVersion)
+            }
+        }
     }
 }
diff --git a/java-sdk/build.gradle.kts b/java-sdk/build.gradle.kts
deleted file mode 100644
index 7a812bab0c4..00000000000
--- a/java-sdk/build.gradle.kts
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import com.diffplug.gradle.spotless.SpotlessExtension
-import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-
-plugins {
-    kotlin("jvm") version "2.3.0"
-    id("com.diffplug.spotless") version "7.2.1" // Last version supporting JDK 
11.
-    id("org.jlleitschuh.gradle.ktlint") version "14.0.1"
-}
-
-allprojects {
-    apply(plugin = "com.diffplug.spotless")
-    apply(plugin = "org.jetbrains.kotlin.jvm")
-    apply(plugin = "org.jlleitschuh.gradle.ktlint")
-
-    repositories { mavenCentral() }
-
-    // Keep these in sync:
-    // - jvmTarget, languageVersion, and sourceCompatibility in 
java-sdk/build.gradle.kts
-    // - TEMURIN_VERSION in scripts/docker/install_jdk.sh
-    // - JAVA_VERSION in .github/workflows/ci-amd.yml and 
.github/workflows/ci-arm.yml
-    // - java-version in .github/workflows/codeql-analysis.yml
-    java {
-        toolchain {
-            languageVersion.set(JavaLanguageVersion.of(11))
-        }
-        sourceCompatibility = JavaVersion.VERSION_11
-    }
-    kotlin {
-        compilerOptions {
-            jvmTarget = JvmTarget.JVM_11
-        }
-    }
-
-    configure<SpotlessExtension> {
-        java {
-            target("**/*.java")
-            googleJavaFormat().formatJavadoc(false)
-            trimTrailingWhitespace()
-            endWithNewline()
-        }
-    }
-}
diff --git a/java-sdk/example/build.gradle.kts 
b/java-sdk/buildSrc/build.gradle.kts
similarity index 66%
copy from java-sdk/example/build.gradle.kts
copy to java-sdk/buildSrc/build.gradle.kts
index 42fa5b4ca91..c620dd1e598 100644
--- a/java-sdk/example/build.gradle.kts
+++ b/java-sdk/buildSrc/build.gradle.kts
@@ -18,29 +18,16 @@
  */
 
 plugins {
-    application
+    `kotlin-dsl`
 }
 
-dependencies {
-    annotationProcessor(project(":sdk"))
-    implementation(project(":sdk"))
-    implementation("org.slf4j:slf4j-simple:2.0.17")
-}
-
-sourceSets {
-    main {
-        java.srcDir("src/java")
-    }
+repositories {
+    mavenCentral()
+    gradlePluginPortal()
 }
 
-application {
-    mainClass = "org.apache.airflow.example.ExampleBundleBuilder"
-}
-
-tasks.withType<Jar> {
-    manifest {
-        attributes(
-            "Main-Class" to application.mainClass.get(),
-        )
-    }
+dependencies {
+    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.0")
+    implementation("com.diffplug.spotless:spotless-plugin-gradle:7.2.1")
+    implementation("org.jlleitschuh.gradle:ktlint-gradle:14.0.1")
 }
diff --git 
a/java-sdk/buildSrc/src/main/kotlin/airflow-jvm-conventions.gradle.kts 
b/java-sdk/buildSrc/src/main/kotlin/airflow-jvm-conventions.gradle.kts
new file mode 100644
index 00000000000..2d541933fc0
--- /dev/null
+++ b/java-sdk/buildSrc/src/main/kotlin/airflow-jvm-conventions.gradle.kts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import com.diffplug.gradle.spotless.SpotlessExtension
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+    id("com.diffplug.spotless")
+    id("org.jetbrains.kotlin.jvm")
+    id("org.jlleitschuh.gradle.ktlint")
+}
+
+repositories {
+    mavenCentral()
+}
+
+// Keep these in sync:
+// - jvmTarget, languageVersion, and sourceCompatibility here
+// - TEMURIN_VERSION in scripts/docker/install_jdk.sh
+// - JAVA_VERSION in .github/workflows/ci-amd.yml and 
.github/workflows/ci-arm.yml
+// - java-version in .github/workflows/codeql-analysis.yml
+java {
+    toolchain {
+        languageVersion.set(JavaLanguageVersion.of(11))
+    }
+    sourceCompatibility = JavaVersion.VERSION_11
+}
+
+kotlin {
+    compilerOptions {
+        jvmTarget = JvmTarget.JVM_11
+    }
+}
+
+configure<SpotlessExtension> {
+    java {
+        target("**/*.java")
+        googleJavaFormat().formatJavadoc(false)
+        trimTrailingWhitespace()
+        endWithNewline()
+    }
+}
diff --git a/java-sdk/buildSrc/src/main/kotlin/airflow-publish.gradle.kts 
b/java-sdk/buildSrc/src/main/kotlin/airflow-publish.gradle.kts
new file mode 100644
index 00000000000..ec727c16856
--- /dev/null
+++ b/java-sdk/buildSrc/src/main/kotlin/airflow-publish.gradle.kts
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Convention plugin shared by all published Apache Airflow Java SDK 
subprojects.
+ *
+ * Applying `id("airflow-publish")` in a subproject's `plugins {}` would do the
+ * following:
+ *
+ * - Sets `group = "org.apache.airflow"` and `version = projectVersion`.
+ * - Applies `maven-publish` and `signing`.
+ * - Wires the shared Maven repository (reads `mavenUrl`, `mavenUsername`,
+ *   `mavenPassword`, or `ASF_NEXUS_*` env vars; auto-selects snapshot vs.
+ *   release URL from `projectVersion`).
+ * - Injects the common POM fields (url, organization, license, SCM) into
+ *   every `MavenPublication` in the subproject
+ * - Configures signing from `signing.key` / `SIGNING_KEY` and
+ *   `signing.password` / `SIGNING_PASSWORD` (skipped when `skipSigning=true`)
+ *
+ * Each subproject only needs to provide its artifact-specific POM fields
+ * (`name` and `description`) and the `artifactId`.
+ */
+
+plugins {
+    `maven-publish`
+    signing
+}
+
+val projectVersion: String by project
+
+group = "org.apache.airflow"
+version = projectVersion
+
+private fun getProperty(name: String): String? = 
providers.gradleProperty(name).orNull
+
+private fun getProperty(
+    name: String,
+    env: String,
+): String? = getProperty(name) ?: System.getenv(env)
+
+publishing {
+    publications.withType<MavenPublication>().configureEach {
+        pom {
+            url = "https://airflow.apache.org";
+            organization {
+                name = "The Apache Software Foundation"
+                url = "https://www.apache.org/";
+            }
+            licenses {
+                license {
+                    name = "The Apache License, Version 2.0"
+                    url = "https://www.apache.org/licenses/LICENSE-2.0.txt";
+                    distribution = "repo"
+                }
+            }
+            scm {
+                connection = 
"scm:git:https://gitbox.apache.org/repos/asf/airflow.git";
+                developerConnection = 
"scm:git:https://gitbox.apache.org/repos/asf/airflow.git";
+                url = "https://github.com/apache/airflow";
+            }
+        }
+    }
+
+    repositories {
+        maven {
+            name = "maven"
+            val repoPath =
+                getProperty("mavenUrl")
+                    ?: if (projectVersion.endsWith("-SNAPSHOT")) {
+                        
"https://repository.apache.org/content/repositories/snapshots/";
+                    } else {
+                        
"https://repository.apache.org/service/local/staging/deploy/maven2/";
+                    }
+            url = uri(repoPath)
+            if (!repoPath.startsWith("file:")) {
+                val user = getProperty("mavenUsername", "ASF_NEXUS_USERNAME")
+                val pass = getProperty("mavenPassword", "ASF_NEXUS_PASSWORD")
+                if (user != null && pass != null) {
+                    credentials {
+                        username = user
+                        password = pass
+                    }
+                }
+            }
+        }
+    }
+}
+
+signing {
+    if (!providers.gradleProperty("skipSigning").map { it.toBoolean() 
}.getOrElse(false)) {
+        val signingKey = getProperty("signing.key", "SIGNING_KEY")
+        val signingPassword = getProperty("signing.password", 
"SIGNING_PASSWORD")
+        useInMemoryPgpKeys(signingKey, signingPassword)
+        publishing.publications.configureEach { sign(this) }
+    }
+}
diff --git a/java-sdk/example/build.gradle.kts b/java-sdk/example/build.gradle
similarity index 69%
copy from java-sdk/example/build.gradle.kts
copy to java-sdk/example/build.gradle
index 42fa5b4ca91..497ea8f5103 100644
--- a/java-sdk/example/build.gradle.kts
+++ b/java-sdk/example/build.gradle
@@ -18,29 +18,33 @@
  */
 
 plugins {
-    application
+    id("org.apache.airflow.sdk") version "${projectVersion}"
+}
+
+repositories {
+    mavenLocal()  // Not needed for your own project.
+    mavenCentral()
 }
 
 dependencies {
-    annotationProcessor(project(":sdk"))
-    implementation(project(":sdk"))
+    
annotationProcessor("org.apache.airflow:airflow-sdk-processor:${projectVersion}")
+    implementation("org.apache.airflow:airflow-sdk:${projectVersion}")
     implementation("org.slf4j:slf4j-simple:2.0.17")
 }
 
+java {
+    toolchain {
+        languageVersion.set(JavaLanguageVersion.of(11))
+    }
+    sourceCompatibility = JavaVersion.VERSION_11
+}
+
 sourceSets {
     main {
         java.srcDir("src/java")
     }
 }
 
-application {
+airflowBundle {
     mainClass = "org.apache.airflow.example.ExampleBundleBuilder"
 }
-
-tasks.withType<Jar> {
-    manifest {
-        attributes(
-            "Main-Class" to application.mainClass.get(),
-        )
-    }
-}
diff --git a/java-sdk/example/gradle.properties 
b/java-sdk/example/gradle.properties
new file mode 120000
index 00000000000..7677fb73be8
--- /dev/null
+++ b/java-sdk/example/gradle.properties
@@ -0,0 +1 @@
+../gradle.properties
\ No newline at end of file
diff --git a/java-sdk/example/build.gradle.kts 
b/java-sdk/example/settings.gradle
similarity index 65%
copy from java-sdk/example/build.gradle.kts
copy to java-sdk/example/settings.gradle
index 42fa5b4ca91..83bae985ad8 100644
--- a/java-sdk/example/build.gradle.kts
+++ b/java-sdk/example/settings.gradle
@@ -17,30 +17,17 @@
  * under the License.
  */
 
-plugins {
-    application
-}
-
-dependencies {
-    annotationProcessor(project(":sdk"))
-    implementation(project(":sdk"))
-    implementation("org.slf4j:slf4j-simple:2.0.17")
-}
-
-sourceSets {
-    main {
-        java.srcDir("src/java")
+// This is only needed since we want to route the plugin to the local build.
+// You don't need it in your own project.
+pluginManagement {
+    repositories {
+        mavenLocal()
+        gradlePluginPortal()
     }
 }
 
-application {
-    mainClass = "org.apache.airflow.example.ExampleBundleBuilder"
+plugins {
+    id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0"
 }
 
-tasks.withType<Jar> {
-    manifest {
-        attributes(
-            "Main-Class" to application.mainClass.get(),
-        )
-    }
-}
+rootProject.name = "airflow-java-sdk-example"
diff --git a/java-sdk/dags/java_examples.py 
b/java-sdk/example/src/resources/dags/java_examples.py
similarity index 100%
rename from java-sdk/dags/java_examples.py
rename to java-sdk/example/src/resources/dags/java_examples.py
diff --git a/java-sdk/gradle.properties b/java-sdk/gradle.properties
index 30b8ff333aa..47099646bfb 100644
--- a/java-sdk/gradle.properties
+++ b/java-sdk/gradle.properties
@@ -19,4 +19,4 @@ org.gradle.configuration-cache=true
 
 airflowSupervisorSchemaVersion=2026-06-16
 
-sdkVersion=1.0.0-SNAPSHOT
+projectVersion=1.0.0-SNAPSHOT
diff --git a/java-sdk/plugin/build.gradle.kts b/java-sdk/plugin/build.gradle.kts
new file mode 100644
index 00000000000..1c242e71b5c
--- /dev/null
+++ b/java-sdk/plugin/build.gradle.kts
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+plugins {
+    `java-gradle-plugin`
+    id("airflow-jvm-conventions")
+    id("airflow-publish")
+}
+
+repositories {
+    gradlePluginPortal()
+}
+
+dependencies {
+    implementation("com.gradleup.shadow:shadow-gradle-plugin:9.1.0") // Last 
supporting Java 11.
+}
+
+gradlePlugin {
+    plugins {
+        create("airflowSdk") {
+            id = "org.apache.airflow.sdk"
+            implementationClass = 
"org.apache.airflow.sdk.plugin.AirflowSdkPlugin"
+            displayName = "Apache Airflow SDK Plugin"
+            description = "Pack a Dag bundle JAR for Airflow to consume."
+        }
+    }
+}
+
+publishing {
+    publications {
+        withType<MavenPublication>().configureEach {
+            if (name == "pluginMaven") {
+                artifactId = "airflow-sdk-gradle-plugin"
+                pom {
+                    name = "Apache Airflow SDK Gradle Plugin"
+                    description = "Gradle plugin for building Apache Airflow 
Dag bundle JARs."
+                }
+            }
+        }
+    }
+}
diff --git 
a/java-sdk/plugin/src/main/kotlin/org/apache/airflow/sdk/plugin/AirflowSdkPlugin.kt
 
b/java-sdk/plugin/src/main/kotlin/org/apache/airflow/sdk/plugin/AirflowSdkPlugin.kt
new file mode 100644
index 00000000000..95836c12fc5
--- /dev/null
+++ 
b/java-sdk/plugin/src/main/kotlin/org/apache/airflow/sdk/plugin/AirflowSdkPlugin.kt
@@ -0,0 +1,221 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.airflow.sdk.plugin
+
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.SourceSetContainer
+import org.gradle.api.tasks.bundling.Jar
+import java.lang.reflect.Modifier
+import java.net.URLClassLoader
+import java.util.jar.JarFile
+import kotlin.jvm.java
+
+/**
+ * DSL extension registered by [AirflowSdkPlugin] as the `airflowBundle` block.
+ *
+ * ```kotlin
+ * airflowBundle {
+ *     mainClass = "com.example.ExampleBundleBuilder"
+ *     // fatJar = false  // opt out of shadow JAR creation
+ * }
+ * ```
+ */
+abstract class AirflowBundleExtension {
+  /** Fully qualified name of the entry-point class.
+   *
+   * This is written to the JAR manifest under the standard `Main-Class` key. 
The
+   * coordinator uses the value to execute the bundle JAR correctly.
+   */
+  @get:Input
+  abstract val mainClass: Property<String>
+
+  /**
+   * Whether to build a fat JAR using the Shadow plugin (default: `true`).
+   *
+   * When `true`, the Shadow plugin is applied automatically and
+   * `Airflow-Supervisor-Schema-Version` is injected into the shadow JAR 
manifest.
+   * When `false`, no fat JAR is produced; only `Main-Class` is set on the
+   * regular `jar` task.
+   */
+  @get:Input
+  abstract val fatJar: Property<Boolean>
+}
+
+/**
+ * Gradle plugin for building Apache Airflow Java SDK bundles.
+ *
+ * Minimal usage:
+ *
+ * ```kotlin
+ * plugins {
+ *     id("org.apache.airflow.sdk") version <version>
+ * }
+ *
+ * airflowBundle {
+ *     mainClass = "com.example.ExampleBundleBuilder"
+ * }
+ * ```
+ *
+ * See [AirflowBundleExtension] to understand how to configure the plugin with 
the
+ * `airflowBundle` block.
+ *
+ * The plugin automatically sets the `Main-Class` metadata, and provides a new
+ * task `bundle` to create a Dag bundle in one command. This builds 
deploy-ready
+ * artifacts to `build/bundle/` that can be copied directly into an Airflow 
Java
+ * coordinator's `jars_root`.
+ *
+ * By default, plugin `com.github.johnrengelman.shadow` is applied 
automatically
+ * to enable fat JAR build. In this mode, one single JAR with user code and all
+ * dependencies is produced by the `bundle` task. An additional metadata entry
+ * `Airflow-Supervisor-Schema-Version` is also added to the JAR for Airflow to
+ * identify which version of the Supervisor Schema it should use to communicate
+ * with the built JAR.
+ *
+ * If `fatJar` is explicitly set to `false`, the `bundle` task builds a bare 
JAR
+ * containing only the Dag bundle, and collect all dependency JARs into the 
target
+ * directory instead. In this mode, `Airflow-Supervisor-Schema-Version` lives 
in
+ * the `airflow-sdk` JAR instead. The bundle JAR still contains `Main-Class`.
+ *
+ */
+class AirflowSdkPlugin : Plugin<Project> {
+  override fun apply(project: Project) {
+    project.plugins.apply("java")
+
+    val ext = project.extensions.create("airflowBundle", 
AirflowBundleExtension::class.java)
+    ext.fatJar.convention(true)
+
+    project.afterEvaluate {
+      project.tasks.withType(Jar::class.java).configureEach { task ->
+        task.doFirst {
+          ext.mainClass.orNull?.let { className ->
+            task.manifest.attributes(mapOf("Main-Class" to className))
+          }
+        }
+      }
+
+      val classFiles =
+        project.objects.fileCollection().from(
+          project.extensions
+            .getByType(SourceSetContainer::class.java)
+            .getByName("main")
+            .output
+            .classesDirs,
+          project.configurations.getByName("runtimeClasspath"),
+        )
+
+      val verifyTask =
+        project.tasks.register("verifyBundleMainClass") { task ->
+          task.group = "verification"
+          task.description =
+            "Verifies that mainClass exists in the compiled output and 
declares a public static main method."
+          task.dependsOn(project.tasks.named("classes"))
+          task.inputs.files(classFiles).withPropertyName("classFiles")
+          task.inputs.property("mainClass", ext.mainClass)
+          task.doLast {
+            val className =
+              ext.mainClass.orNull
+                ?: error("airflowBundle.mainClass is not set. Add it to your 
airflowBundle { } block.")
+            val urls = classFiles.map { it.toURI().toURL() }.toTypedArray()
+
+            URLClassLoader(urls, ClassLoader.getPlatformClassLoader()).use { 
loader ->
+              val klass =
+                try {
+                  loader.loadClass(className)
+                } catch (_: ClassNotFoundException) {
+                  error("Configured main class '$className' is not found.")
+                }
+              val main =
+                try {
+                  klass.getMethod("main", Array<String>::class.java)
+                } catch (_: NoSuchMethodException) {
+                  error("mainClass '$className' does not have a public static 
main method.")
+                }
+              check(Modifier.isStatic(main.modifiers)) {
+                "main method on '$className' is not static."
+              }
+              check(main.returnType == Void.TYPE) {
+                "main method on '$className' must return void."
+              }
+            }
+          }
+        }
+
+      if (ext.fatJar.get()) {
+        project.plugins.apply("com.gradleup.shadow")
+
+        val schemaVersionProvider =
+          project.providers.provider {
+            val runtimeClasspath =
+              project.configurations.findByName("runtimeClasspath")
+                ?: error(
+                  "No runtimeClasspath configuration found. " +
+                    "Make sure the java or application plugin is applied 
before " +
+                    "org.apache.airflow.sdk.",
+                )
+            runtimeClasspath.resolvedConfiguration.resolvedArtifacts
+              .firstOrNull { a ->
+                a.moduleVersion.id.group == "org.apache.airflow" && a.name == 
"airflow-sdk"
+              }?.let { artifact ->
+                JarFile(artifact.file).use { jar ->
+                  
jar.manifest?.mainAttributes?.getValue("Airflow-Supervisor-Schema-Version")
+                }
+              } ?: error(
+              "airflow-sdk not found in runtimeClasspath, or its JAR is 
missing the " +
+                "Airflow-Supervisor-Schema-Version manifest attribute. " +
+                "Make sure org.apache.airflow:airflow-sdk is declared as an " +
+                "implementation dependency.",
+            )
+          }
+
+        project.tasks.withType(ShadowJar::class.java).configureEach { task ->
+          task.doFirst {
+            task.manifest.attributes(
+              mapOf("Airflow-Supervisor-Schema-Version" to 
schemaVersionProvider.get()),
+            )
+          }
+        }
+
+        project.tasks.register("bundle", Copy::class.java) { task ->
+          task.group = "build"
+          task.description = "Assembles a fat JAR bundle for deployment to 
Airflow."
+          task.dependsOn(verifyTask)
+          task.from(project.tasks.named("shadowJar"))
+          task.into(project.layout.buildDirectory.dir("bundle"))
+        }
+      } else {
+        // bundle copies the thin JAR and all runtime dependency JARs into
+        // build/bundle/, mirroring what installDist puts in lib/.
+        project.tasks.register("bundle", Copy::class.java) { task ->
+          task.group = "build"
+          task.description = "Assembles a thin JAR bundle directory for 
deployment to Airflow."
+          task.dependsOn(verifyTask)
+          task.from(project.configurations.getByName("runtimeClasspath"))
+          task.from(project.tasks.named("jar"))
+          task.into(project.layout.buildDirectory.dir("bundle"))
+        }
+      }
+    }
+  }
+}
diff --git a/java-sdk/example/build.gradle.kts 
b/java-sdk/processor/build.gradle.kts
similarity index 50%
rename from java-sdk/example/build.gradle.kts
rename to java-sdk/processor/build.gradle.kts
index 42fa5b4ca91..bddafe1a051 100644
--- a/java-sdk/example/build.gradle.kts
+++ b/java-sdk/processor/build.gradle.kts
@@ -18,29 +18,39 @@
  */
 
 plugins {
-    application
+    `java-library`
+    id("airflow-jvm-conventions")
+    id("airflow-publish")
 }
 
 dependencies {
-    annotationProcessor(project(":sdk"))
+    compileOnly("javax.annotation:javax.annotation-api:1.3.2")
     implementation(project(":sdk"))
-    implementation("org.slf4j:slf4j-simple:2.0.17")
+    implementation("com.squareup:javapoet:1.13.0")
+
+    testImplementation(kotlin("test"))
+    testImplementation("com.google.testing.compile:compile-testing:0.23.0")
 }
 
-sourceSets {
-    main {
-        java.srcDir("src/java")
-    }
+java {
+    withSourcesJar()
 }
 
-application {
-    mainClass = "org.apache.airflow.example.ExampleBundleBuilder"
+tasks.withType<Test> {
+    useJUnitPlatform()
 }
 
-tasks.withType<Jar> {
-    manifest {
-        attributes(
-            "Main-Class" to application.mainClass.get(),
-        )
+publishing {
+    publications {
+        create<MavenPublication>("mavenJava") {
+            artifactId = "airflow-sdk-processor"
+            from(components["java"])
+            pom {
+                name = "Apache Airflow Java SDK — Annotation Processor"
+                description =
+                    "Annotation processor for the Apache Airflow Java SDK; " +
+                    "generates *Builder classes from @Builder.Dag-annotated 
sources."
+            }
+        }
     }
 }
diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt 
b/java-sdk/processor/src/main/kotlin/org/apache/airflow/sdk/BuilderProcessor.kt
similarity index 76%
copy from java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt
copy to 
java-sdk/processor/src/main/kotlin/org/apache/airflow/sdk/BuilderProcessor.kt
index 2231f4db10d..979615c12f2 100644
--- a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt
+++ 
b/java-sdk/processor/src/main/kotlin/org/apache/airflow/sdk/BuilderProcessor.kt
@@ -39,76 +39,6 @@ import javax.lang.model.type.TypeKind
 import javax.lang.model.type.TypeMirror
 import javax.tools.Diagnostic
 
-/**
- * Container for the annotation-based Dag-authoring API.
- *
- * This class is not instantiated directly. Its nested annotations drive the
- * [BuilderProcessor] annotation processor, which generates a `*Builder` class
- * for each class annotated with [Dag].
- *
- * Example:
- *
- * ```java
- * @Builder.Dag(id = "my_pipeline")
- * public class MyPipeline {
- *
- *     @Builder.Task(id = "extract")
- *     public long extract(Client client) { ... }
- *
- *     @Builder.Task(id = "transform")
- *     public long transform(Client client, @Builder.XCom(task = "extract") 
long extracted) { ... }
- * }
- * ```
- *
- * The processor generates `MyPipelineBuilder.build()`, which returns a
- * fully wired-up [Dag] ready to add to a [Bundle].
- */
-class Builder internal constructor() {
-  /**
-   * Annotation to automate a Dag-builder pattern.
-   *
-   * When applied on a class Foo, this generates a FooBuilder class with a
-   * static build method to create the Dag structure automatically.
-   *
-   * @param id Override the Dag ID. If empty or not provided, the annotated
-   *    class's name is used by default.
-   * @param to Name of the Dag-builder class. If empty or not provided, use the
-   *    annotated class name + "Builder".
-   */
-  @Target(AnnotationTarget.CLASS)
-  @MustBeDocumented
-  annotation class Dag(
-    val id: String = "",
-    val to: String = "",
-  )
-
-  /**
-   * Annotation to automate task definition in a Dag-builder pattern.
-   *
-   * @param id Override the task ID. If empty or not provided, the annotated
-   *    function's name is used by default.
-   */
-  @Target(AnnotationTarget.FUNCTION)
-  @MustBeDocumented
-  annotation class Task(
-    val id: String = "",
-  )
-
-  /**
-   * Annotation to mark a task definition's method parameter as an XCom input.
-   *
-   * @param task The task ID to pull. If empty or not given, the annotated
-   *    parameter's name is used by default.
-   * @param key The XCom key to pull. Defaults to the task's return value.
-   */
-  @Target(AnnotationTarget.VALUE_PARAMETER)
-  @MustBeDocumented
-  annotation class XCom(
-    val task: String = "",
-    val key: String = Client.XCOM_RETURN_KEY,
-  )
-}
-
 /**
  * @suppress
  *
diff --git 
a/java-sdk/sdk/src/main/resources/META-INF/services/javax.annotation.processing.Processor
 
b/java-sdk/processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor
similarity index 100%
rename from 
java-sdk/sdk/src/main/resources/META-INF/services/javax.annotation.processing.Processor
rename to 
java-sdk/processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor
diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BuilderTest.kt 
b/java-sdk/processor/src/test/kotlin/org/apache/airflow/sdk/BuilderTest.kt
similarity index 100%
rename from java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BuilderTest.kt
rename to 
java-sdk/processor/src/test/kotlin/org/apache/airflow/sdk/BuilderTest.kt
diff --git a/java-sdk/sdk/build.gradle.kts b/java-sdk/sdk/build.gradle.kts
index efeacace3a8..216ef765131 100644
--- a/java-sdk/sdk/build.gradle.kts
+++ b/java-sdk/sdk/build.gradle.kts
@@ -17,30 +17,16 @@
  * under the License.
  */
 
-buildscript {
-    repositories {
-        mavenCentral()
-    }
-}
-
 val airflowSupervisorSchemaVersion: String by project
 
-val sdkArtifact = "airflow-sdk"
-val sdkVersion: String by project
-
-// Full Maven coordinate: org.apache.airflow:airflow-sdk:<version>
-// artifactId is set explicitly on the MavenPublication below.
-group = "org.apache.airflow"
-version = sdkVersion
-
 plugins {
     `java-library`
-    `maven-publish`
-    signing
-    kotlin("plugin.serialization") version "2.3.0"
+    id("airflow-jvm-conventions")
+    id("airflow-publish")
     id("org.jetbrains.dokka") version "2.2.0"
     id("org.jetbrains.dokka-javadoc") version "2.2.0"
     id("org.jsonschema2pojo") version "1.2.2"
+    kotlin("plugin.serialization") version "2.3.0"
 }
 
 // TODO: Use a hosted file instead.
@@ -58,7 +44,6 @@ dependencies {
     implementation("com.fasterxml.jackson.core:jackson-databind:2.21.0")
     
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.0")
     
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.0")
-    implementation("com.squareup:javapoet:1.13.0")
     implementation("com.xenomachina:kotlin-argparser:2.0.7")
     implementation("io.ktor:ktor-network:3.3.3")
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
@@ -68,7 +53,6 @@ dependencies {
     implementation("org.msgpack:jackson-dataformat-msgpack:0.9.11")
 
     testImplementation(kotlin("test"))
-    testImplementation("com.google.testing.compile:compile-testing:0.23.0")
     testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
 }
 
@@ -214,7 +198,7 @@ sourceSets {
 }
 
 dokka {
-    moduleVersion.set(sdkVersion)
+    moduleVersion.set(project.version.toString())
     dokkaSourceSets.configureEach {
         // Suppress everything in 'execution' since it's implementation detail.
         perPackageOption {
@@ -262,74 +246,16 @@ tasks.withType<Test> {
     useJUnitPlatform()
 }
 
-private fun getProperty(name: String) = providers.gradleProperty(name).orNull
-
-private fun getProperty(
-    name: String,
-    env: String,
-): String? = getProperty(name) ?: System.getenv(env)
-
 publishing {
     publications {
         create<MavenPublication>("mavenJava") {
-            artifactId = sdkArtifact
+            artifactId = "airflow-sdk"
             from(components["java"])
             artifact(javadocJar)
             pom {
                 name = "Apache Airflow Java SDK"
                 description = "Java SDK for implementing Apache Airflow task 
logic on the JVM."
-                url = "https://airflow.apache.org";
-
-                organization {
-                    name = "The Apache Software Foundation"
-                    url = "https://www.apache.org/";
-                }
-                licenses {
-                    license {
-                        name = "The Apache License, Version 2.0"
-                        url = "https://www.apache.org/licenses/LICENSE-2.0.txt";
-                        distribution = "repo"
-                    }
-                }
-                scm {
-                    connection = 
"scm:git:https://gitbox.apache.org/repos/asf/airflow.git";
-                    developerConnection = 
"scm:git:https://gitbox.apache.org/repos/asf/airflow.git";
-                    url = "https://github.com/apache/airflow";
-                }
             }
         }
     }
-
-    repositories {
-        maven {
-            name = "mavenRepo"
-            val repoPath =
-                getProperty("mavenUrl")
-                    ?: if (sdkVersion.endsWith("-SNAPSHOT")) {
-                        
"https://repository.apache.org/content/repositories/snapshots/";
-                    } else {
-                        
"https://repository.apache.org/service/local/staging/deploy/maven2/";
-                    }
-            url = uri(repoPath)
-            if (!repoPath.startsWith("file:")) {
-                val user = getProperty("mavenUsername", "ASF_NEXUS_USERNAME")
-                val pass = getProperty("mavenPassword", "ASF_NEXUS_PASSWORD")
-                if (user != null && pass != null) {
-                    credentials {
-                        username = user
-                        password = pass
-                    }
-                }
-            }
-        }
-    }
-}
-
-signing {
-    if (!providers.gradleProperty("skipSigning").map { it.toBoolean() 
}.getOrElse(false)) {
-        val signingKey = getProperty("signing.key", "SIGNING_KEY")
-        val signingPassword = getProperty("signing.password", 
"SIGNING_PASSWORD")
-        useInMemoryPgpKeys(signingKey, signingPassword)
-        sign(publishing.publications["mavenJava"])
-    }
 }
diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt 
b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt
index 2231f4db10d..c12a53f83e5 100644
--- a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt
+++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt
@@ -17,34 +17,14 @@
  * under the License.
  */
 
-@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
-
 package org.apache.airflow.sdk
 
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.JavaFile
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.annotation.processing.AbstractProcessor
-import javax.annotation.processing.ProcessingEnvironment
-import javax.annotation.processing.RoundEnvironment
-import javax.annotation.processing.SupportedAnnotationTypes
-import javax.annotation.processing.SupportedSourceVersion
-import javax.lang.model.SourceVersion
-import javax.lang.model.element.ExecutableElement
-import javax.lang.model.element.Modifier
-import javax.lang.model.element.TypeElement
-import javax.lang.model.type.TypeKind
-import javax.lang.model.type.TypeMirror
-import javax.tools.Diagnostic
-
 /**
  * Container for the annotation-based Dag-authoring API.
  *
  * This class is not instantiated directly. Its nested annotations drive the
- * [BuilderProcessor] annotation processor, which generates a `*Builder` class
- * for each class annotated with [Dag].
+ * `BuilderProcessor` annotation processor in the :processor project,
+ * which generates a `*Builder` class for each class annotated with [Dag].
  *
  * Example:
  *
@@ -108,171 +88,3 @@ class Builder internal constructor() {
     val key: String = Client.XCOM_RETURN_KEY,
   )
 }
-
-/**
- * @suppress
- *
- * Annotation processor for [Builder.Dag].
- *
- * This is registered as a standard javac processor via
- * `META-INF/services/javax.annotation.processing.Processor`; not intended to 
be
- * instantiated or referenced directly.
- *
- * For each class annotated with [Builder.Dag], generates a `*Builder` class
- * containing:
- *
- * - One inner class per [Builder.Task]-annotated method, implementing [Task].
- * - A static `build()` method that constructs the [Dag] and registers those
- *   inner classes as tasks.
- *
- * [Builder.XCom]-annotated parameters are resolved via `client.getXCom` in the
- * generated `execute` body, with the result cast to the parameter's declared
- * type. Non-`void` return values are forwarded to `client.setXCom`.
- */
-@SupportedAnnotationTypes("org.apache.airflow.sdk.Builder.Dag")
-@SupportedSourceVersion(SourceVersion.RELEASE_11)
-class BuilderProcessor : AbstractProcessor() {
-  override fun process(
-    annotations: Set<TypeElement>,
-    roundEnv: RoundEnvironment,
-  ): Boolean {
-    if (annotations.isEmpty()) return false
-    
roundEnv.getElementsAnnotatedWith(Builder.Dag::class.java).filterIsInstance<TypeElement>().forEach
 { el ->
-      with(processingEnv) {
-        runCatching {
-          JavaFile
-            .builder(
-              elementUtils.getPackageOf(el).qualifiedName.toString(),
-              buildDag(el),
-            ).build()
-            .writeTo(filer)
-        }.onFailure { e ->
-          messager.printMessage(
-            Diagnostic.Kind.ERROR,
-            e.message ?: "Unknown error",
-            el,
-          )
-        }
-      }
-    }
-    return true
-  }
-
-  private fun buildDag(el: TypeElement): TypeSpec {
-    val ann = el.getAnnotation(Builder.Dag::class.java)!!
-
-    val builderClass =
-      TypeSpec
-        .classBuilder(ann.to.ifBlank { "${el.simpleName}Builder" })
-        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
-
-    val buildMethod =
-      MethodSpec
-        .methodBuilder("build")
-        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
-        .returns(ClassName.get(Dag::class.java))
-        .addStatement($$"var dag = new $T($S)", 
ClassName.get(Dag::class.java), ann.id.ifBlank { el.simpleName })
-
-    for (inner in el.enclosedElements) {
-      if (inner !is ExecutableElement) continue
-      if (inner.isVarArgs) throw IllegalArgumentException("Cannot create task 
from vararg function ${inner.simpleName}")
-
-      val ann = inner.getAnnotation(Builder.Task::class.java) ?: continue
-      val innerName = 
inner.simpleName.toString().replaceFirstChar(Char::uppercase)
-
-      val task = buildTask(innerName, inner, el)
-      builderClass.addType(task.spec)
-
-      buildMethod.addStatement(
-        $$"dag.addTask($S, $L.class)",
-        ann.id.ifBlank { inner.simpleName },
-        innerName,
-      )
-    }
-
-    buildMethod.addStatement("return dag")
-    builderClass.addMethod(buildMethod.build())
-    return builderClass.build()
-  }
-
-  private fun buildTask(
-    name: String,
-    inner: ExecutableElement,
-    parent: TypeElement,
-  ): BuildTaskResult {
-    val clientType = ClassName.get(Client::class.java)
-    val contextType = ClassName.get(Context::class.java)
-
-    val executeSpec =
-      MethodSpec
-        .methodBuilder("execute")
-        .addAnnotation(Override::class.java)
-        .addModifiers(Modifier.PUBLIC)
-        .returns(TypeName.VOID)
-        .addParameter(contextType, "context")
-        .addParameter(clientType, "client")
-        .addException(Exception::class.java)
-
-    val required = mutableListOf<RequiredXCom>()
-    val innerArgs =
-      with(processingEnv) {
-        inner.parameters.joinToString { param ->
-          val anno = param.getAnnotation(Builder.XCom::class.java)
-          val type = param.asType()
-          when {
-            anno != null ->
-              param.simpleName.toString().also {
-                required += RequiredXCom(type, it, anno.task.ifBlank { it })
-              }
-            isType(type, clientType) -> "client"
-            isType(type, contextType) -> "context"
-            else -> throw IllegalArgumentException("Unsupported task parameter 
'${param.simpleName}' with type: $type")
-          }
-        }
-      }
-    required.forEach {
-      executeSpec.addStatement(
-        $$"var $L = ($T) client.getXCom($S)",
-        it.paramName,
-        with(TypeName.get(it.paramType)) { if (isPrimitive) box() else this },
-        it.taskId,
-      )
-    }
-    if (inner.returnType.kind == TypeKind.VOID) {
-      $$"new $T().$L($L)"
-    } else {
-      $$"client.setXCom(new $T().$L($L))"
-    }.also {
-      executeSpec.addStatement(
-        it,
-        ClassName.get(parent),
-        inner.simpleName,
-        innerArgs,
-      )
-    }
-
-    val spec =
-      TypeSpec
-        .classBuilder(name)
-        .addSuperinterface(Task::class.java)
-        .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC)
-        .addMethod(executeSpec.build())
-        .build()
-    return BuildTaskResult(spec)
-  }
-}
-
-private fun ProcessingEnvironment.isType(
-  t: TypeMirror,
-  c: ClassName,
-): Boolean = typeUtils.isSameType(t, 
elementUtils.getTypeElement(c.canonicalName()).asType())
-
-private data class RequiredXCom(
-  val paramType: TypeMirror,
-  val paramName: String,
-  val taskId: String,
-)
-
-private data class BuildTaskResult(
-  val spec: TypeSpec,
-)
diff --git a/java-sdk/settings.gradle.kts b/java-sdk/settings.gradle.kts
index 0892437c13d..fd4f4ac001c 100644
--- a/java-sdk/settings.gradle.kts
+++ b/java-sdk/settings.gradle.kts
@@ -1,8 +1,20 @@
 /*
- * This file was generated by the Gradle 'init' task.
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
  *
- * The settings file is used to specify which projects to include in your 
build.
- * For more detailed information on multi-project builds, please refer to 
https://docs.gradle.org/9.2.1/userguide/multi_project_builds.html in the Gradle 
documentation.
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
  */
 
 plugins {
@@ -10,4 +22,4 @@ plugins {
 }
 
 rootProject.name = "airflow-java-sdk"
-include("example", "sdk")
+include("bom", "plugin", "processor", "sdk")
diff --git a/scripts/docker/install_jdk.sh b/scripts/docker/install_jdk.sh
index afe6eb3a69e..1c93483c80c 100755
--- a/scripts/docker/install_jdk.sh
+++ b/scripts/docker/install_jdk.sh
@@ -24,7 +24,7 @@ set -euo pipefail
 common::get_colors
 
 # Keep these in sync:
-# - jvmTarget, languageVersion, and sourceCompatibility in 
java-sdk/build.gradle.kts
+# - jvmTarget, languageVersion, and sourceCompatibility in java-sdk/buildSrc
 # - TEMURIN_VERSION in scripts/docker/install_jdk.sh
 # - JAVA_VERSION in .github/workflows/ci-amd.yml and 
.github/workflows/ci-arm.yml
 # - java-version in .github/workflows/codeql-analysis.yml

Reply via email to