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

lhotari pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-connectors.git


The following commit(s) were added to refs/heads/master by this push:
     new c762d7e  [improve][build] Improve the gradle build to use similar best 
practices as apache/pulsar build (#10)
c762d7e is described below

commit c762d7ef118f7eb165c22bf2681ce801893558f9
Author: Lari Hotari <[email protected]>
AuthorDate: Thu Apr 2 21:05:15 2026 +0300

    [improve][build] Improve the gradle build to use similar best practices as 
apache/pulsar build (#10)
---
 .github/actions/setup-gradle/action.yml            |  71 ++++
 .github/actions/tune-runner-vm/action.yml          |  80 +++++
 .github/workflows/ci.yaml                          |  41 ++-
 .gitignore                                         |   1 +
 .ratignore                                         |  48 +++
 README.md                                          |  37 +-
 aerospike/build.gradle.kts                         |   3 +-
 alluxio/build.gradle.kts                           |  27 +-
 .../apache/pulsar/io/alluxio/sink/AlluxioSink.java |  22 +-
 .../pulsar/io/alluxio/sink/AlluxioSinkTest.java    |  66 ++--
 aws/build.gradle.kts                               |   4 +
 azure-data-explorer/build.gradle.kts               |   3 +-
 .../conventions}/build.gradle.kts                  |  19 +-
 ...-connectors.code-quality-conventions.gradle.kts |  29 +-
 .../pulsar-connectors.java-conventions.gradle.kts  | 227 +++++++++++++
 .../pulsar-connectors.nar-conventions.gradle.kts   | 143 ++++++++
 ...pulsar-connectors.shadow-conventions.gradle.kts |  57 ++++
 .../settings.gradle.kts                            |  19 +-
 build.gradle.kts                                   | 301 +++--------------
 canal/build.gradle.kts                             |   3 +-
 cassandra/build.gradle.kts                         |   3 +-
 debezium/core/build.gradle.kts                     |   3 +
 debezium/mongodb/build.gradle.kts                  |   3 +-
 debezium/mssql/build.gradle.kts                    |   3 +-
 debezium/mysql/build.gradle.kts                    |   3 +-
 debezium/oracle/build.gradle.kts                   |   3 +-
 debezium/postgres/build.gradle.kts                 |   3 +-
 distribution/io/build.gradle.kts                   |   6 +-
 docker/pulsar-all/build.gradle.kts                 |   6 +-
 docs/build.gradle.kts                              |  34 +-
 gradle.properties => docs/pulsar-io-gen.sh         |  19 +-
 .../pulsar/io/docs/ConnectorDocGenerator.java      |  18 +-
 dynamodb/build.gradle.kts                          |   3 +-
 elastic-search/build.gradle.kts                    |   3 +-
 file/build.gradle.kts                              |   3 +-
 gradle.properties                                  |   4 +
 gradle/libs.versions.toml                          | 375 +++++++++------------
 hbase/build.gradle.kts                             |   3 +-
 hdfs3/build.gradle.kts                             |   3 +-
 http/build.gradle.kts                              |   3 +-
 influxdb/build.gradle.kts                          |   3 +-
 jdbc/clickhouse/build.gradle.kts                   |   3 +-
 jdbc/core/build.gradle.kts                         |   4 +
 jdbc/mariadb/build.gradle.kts                      |   3 +-
 jdbc/openmldb/build.gradle.kts                     |   3 +-
 jdbc/postgres/build.gradle.kts                     |   3 +-
 jdbc/sqlite/build.gradle.kts                       |   3 +-
 kafka-connect-adaptor-nar/build.gradle.kts         |   3 +-
 kafka-connect-adaptor/build.gradle.kts             |   3 +
 kafka/build.gradle.kts                             |  19 +-
 kinesis-kpl-shaded/build.gradle.kts                |  12 +-
 kinesis/build.gradle.kts                           |   7 +-
 mongo/build.gradle.kts                             |   3 +-
 netty/build.gradle.kts                             |   3 +-
 nsq/build.gradle.kts                               |   3 +-
 .../build.gradle.kts                               |  24 +-
 rabbitmq/build.gradle.kts                          |   3 +-
 redis/build.gradle.kts                             |   3 +-
 settings.gradle.kts                                |  11 +-
 solr/build.gradle.kts                              |   5 +-
 src/license-header.txt                             |  16 +
 61 files changed, 1199 insertions(+), 640 deletions(-)

diff --git a/.github/actions/setup-gradle/action.yml 
b/.github/actions/setup-gradle/action.yml
new file mode 100644
index 0000000..ceac384
--- /dev/null
+++ b/.github/actions/setup-gradle/action.yml
@@ -0,0 +1,71 @@
+#
+# 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.
+#
+
+name: Setup Gradle
+description: Sets up Gradle with Develocity or public build scan publishing by 
default
+inputs:
+  develocity-access-key:
+    description: 'Develocity access key for authenticated build scans'
+    required: false
+    default: ''
+  build-scan-publish:
+    description: 'Whether to publish build scans or use Develocity when the 
access key is set'
+    required: false
+    default: 'true'
+  cache-read-only:
+    description: 'Whether the Gradle cache is read-only'
+    required: false
+    default: ${{ github.event.repository != null && github.ref_name != 
github.event.repository.default_branch }}
+  add-job-summary:
+    description: 'When to add a job summary'
+    required: false
+    default: 'always'
+runs:
+  using: composite
+  steps:
+    - name: Set Develocity Project ID and configure custom settings
+      if: ${{ inputs.develocity-access-key != '' && inputs.build-scan-publish 
== 'true' }}
+      shell: bash
+      run: |
+        mkdir -p ~/.gradle
+        touch ~/.gradle/gradle.properties
+        grep -q 'systemProp.develocity.projectId=' ~/.gradle/gradle.properties 
|| echo systemProp.develocity.projectId=pulsar >> ~/.gradle/gradle.properties
+        grep -q 'systemProp.scan.uploadInBackground=' 
~/.gradle/gradle.properties || echo systemProp.scan.uploadInBackground=false >> 
~/.gradle/gradle.properties
+
+    - name: Setup Gradle with Develocity
+      if: ${{ inputs.develocity-access-key != '' && inputs.build-scan-publish 
== 'true' }}
+      uses: 
gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f
+      with:
+        develocity-injection-enabled: true
+        develocity-url: https://develocity.apache.org
+        # expected format is develocity.apache.org:<access-key>
+        develocity-access-key: ${{ inputs.develocity-access-key }}
+        build-scan-publish: ${{ inputs.build-scan-publish }}
+        cache-read-only: ${{ inputs.cache-read-only }}
+        add-job-summary: ${{ inputs.add-job-summary }}
+
+    - name: Setup Gradle
+      if: ${{ !(inputs.develocity-access-key != '' && 
inputs.build-scan-publish == 'true') }}
+      uses: 
gradle/actions/setup-gradle@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f
+      with:
+        build-scan-publish: ${{ inputs.build-scan-publish }}
+        build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
+        build-scan-terms-of-use-agree: 'yes'
+        cache-read-only: ${{ inputs.cache-read-only }}
+        add-job-summary: ${{ inputs.add-job-summary }}
\ No newline at end of file
diff --git a/.github/actions/tune-runner-vm/action.yml 
b/.github/actions/tune-runner-vm/action.yml
new file mode 100644
index 0000000..00eb084
--- /dev/null
+++ b/.github/actions/tune-runner-vm/action.yml
@@ -0,0 +1,80 @@
+#
+# 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.
+#
+
+name: Tune Runner VM performance
+description: tunes the GitHub Runner VM operation system
+runs:
+  using: composite
+  steps:
+    - shell: bash
+      run: |
+        if [[ "$OSTYPE" == "linux-gnu"* ]]; then
+            echo "::group::Configure and tune OS"
+            # Ensure that reverse lookups for current hostname are handled 
properly
+            # Add the current IP address, long hostname and short hostname 
record to /etc/hosts file
+            echo -e "$(ip addr show eth0 | grep "inet\b" | awk '{print $2}' | 
cut -d/ -f1)\t$(hostname -f) $(hostname -s)" | sudo tee -a /etc/hosts
+
+            # The default vm.swappiness setting is 60 which has a tendency to 
start swapping when memory
+            # consumption is high.
+            # Set vm.swappiness=1 to avoid swapping and allow high RAM usage
+            echo 1 | sudo tee /proc/sys/vm/swappiness
+
+            # use "madvise" Linux Transparent HugePages (THP) setting
+            # 
https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html
+            # "madvise" is generally a better option than the default "always" 
setting
+            # recommendation from 
https://netflixtechblog.com/bending-pause-times-to-your-will-with-generational-zgc-256629c9386b
+            echo madvise | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
+            echo advise | sudo tee 
/sys/kernel/mm/transparent_hugepage/shmem_enabled
+            echo defer | sudo tee /sys/kernel/mm/transparent_hugepage/defrag
+            echo 1 | sudo tee 
/sys/kernel/mm/transparent_hugepage/khugepaged/defrag
+    
+            # tune filesystem mount options, 
https://www.kernel.org/doc/Documentation/filesystems/ext4.txt
+            # commit=999999, effectively disables automatic syncing to disk 
(default is every 5 seconds)
+            # nobarrier/barrier=0, loosen data consistency on system crash (no 
negative impact to empheral CI nodes)
+            sudo mount -o remount,nodiscard,commit=999999,barrier=0 / || true
+            if mountpoint -q /mnt; then
+              sudo mount -o remount,nodiscard,commit=999999,barrier=0 /mnt || 
true
+            fi
+            # disable discard/trim at device level since remount with 
nodiscard doesn't seem to be effective
+            # https://www.spinics.net/lists/linux-ide/msg52562.html
+            for i in /sys/block/sd*/queue/discard_max_bytes; do
+              echo 0 | sudo tee $i
+            done
+            # disable unnecessary timers
+            sudo systemctl stop fstrim.timer fstrim.service \
+              podman-auto-update.timer sysstat-collect.timer 
sysstat-summary.timer \
+              phpsessionclean.timer man-db.timer motd-news.timer \
+              dpkg-db-backup.timer e2scrub_all.timer \
+              update-notifier-download.timer update-notifier-motd.timer || true
+
+            # stop unnecessary services
+            sudo systemctl stop php8.3-fpm.service ModemManager.service \
+              multipathd.socket multipathd.service udisks2.service 
walinuxagent.service || true
+
+            echo '::endgroup::'
+
+            # show memory
+            echo "::group::Available Memory"
+            free -m
+            echo '::endgroup::'
+            # show disk
+            echo "::group::Available diskspace"
+            df -BM
+            echo "::endgroup::"
+        fi
\ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 838b667..424363a 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -40,17 +40,26 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 30
     steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-java@v4
+      - uses: actions/checkout@v6
+
+      - name: Tune Runner VM
+        uses: ./.github/actions/tune-runner-vm
+
+      - uses: actions/setup-java@v5
         with:
           distribution: ${{ env.JDK_DISTRIBUTION }}
           java-version: ${{ env.JDK_VERSION }}
-      - uses: 
gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c
 
-      - name: Build all modules
-        run: ./gradlew build -x test
-      - name: License check (RAT)
-        run: ./gradlew rat
+      - name: Setup Gradle
+        uses: ./.github/actions/setup-gradle
+        with:
+          develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+          cache-read-only: false
+
+      - name: Build and check licenses
+        run: >-
+          ./gradlew -x test -x nar build rat spotlessCheck
+          --no-configuration-cache
 
   tests:
     name: Tests - ${{ matrix.name }}
@@ -64,18 +73,26 @@ jobs:
           - name: Connectors
             tasks: test
     steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-java@v4
+      - uses: actions/checkout@v6
+
+      - name: Tune Runner VM
+        uses: ./.github/actions/tune-runner-vm
+
+      - uses: actions/setup-java@v5
         with:
           distribution: ${{ env.JDK_DISTRIBUTION }}
           java-version: ${{ env.JDK_VERSION }}
-      - uses: 
gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c
+
+      - name: Setup Gradle
+        uses: ./.github/actions/setup-gradle
+        with:
+          develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
 
       - name: Run tests
-        run: ./gradlew ${{ matrix.tasks }}
+        run: ./gradlew ${{ matrix.tasks }} -PtestFailFast=true
       - name: Upload test reports
         if: failure()
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v7
         with:
           name: ${{ matrix.name }}-test-reports
           path: '**/build/reports/tests/'
diff --git a/.gitignore b/.gitignore
index 91bd581..33093cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 # Gradle
 .gradle/
+.kotlin/
 build/
 **/build/
 
diff --git a/.ratignore b/.ratignore
new file mode 100644
index 0000000..ab06f40
--- /dev/null
+++ b/.ratignore
@@ -0,0 +1,48 @@
+# Build artifacts
+**/build/**
+**/target/**
+# Gradle files
+.gradle/**
+gradle/wrapper/**
+**/.gradle/**
+**/.kotlin/**
+**/gradle/wrapper/**
+gradlew
+gradlew.bat
+gradle/libs.versions.toml
+# Generated Flatbuffer files (Kinesis)
+**/org/apache/pulsar/io/kinesis/fbs/*.java
+# Services files
+**/META-INF/services/*
+# Certificates and keys
+**/*.crt
+**/*.key
+**/*.csr
+**/*.pem
+**/*.srl
+**/certificate-authority/serial
+**/certificate-authority/index.txt
+**/*.json
+**/*.txt
+# Project/IDE files
+**/*.md
+.github/**
+**/*.nar
+**/.gitignore
+**/.gitattributes
+**/*.iml
+**/.classpath
+**/.project
+**/.settings
+**/.idea/**
+**/.vscode/**
+# Avro schemas
+**/*.avsc
+# Patch files
+**/*.patch
+# Hidden directories
+.*/**
+# Test output
+**/test-output/**
+# Log files
+**/*.log
diff --git a/README.md b/README.md
index 6ecb271..ea0694f 100644
--- a/README.md
+++ b/README.md
@@ -66,26 +66,48 @@ mounting them into the `apachepulsar/pulsar` Docker image.
 |-----------|-------------|
 | Kafka Connect Adaptor | Run Kafka Connect connectors on Pulsar |
 
+## Prerequisites
+
+- **JDK 17** or later — e.g. [Eclipse 
Temurin](https://adoptium.net/en-GB/temurin/releases?version=17&os=any&arch=any)
+  or [Amazon 
Corretto](https://docs.aws.amazon.com/corretto/latest/corretto-17-ug/what-is-corretto-17.html)
+
+> **Note**: This project includes a [Gradle 
Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html)
+> so no separate Gradle installation is needed. Use `./gradlew` on Linux/macOS 
and `gradlew.bat` on Windows.
+
 ## Building
 
+Compile and assemble all modules:
+
 ```bash
-./gradlew build -x test
+./gradlew assemble
 ```
 
-To build all connector NARs:
+NAR files are produced under each connector's `build/libs/` directory.
+
+Build a specific connector:
 
 ```bash
-./gradlew build -x test
+./gradlew :elastic-search:assemble
 ```
 
-NAR files are produced under each connector's `build/libs/` directory.
-
-To build the distribution tarball containing all connector NARs:
+Build the distribution package containing all connector NARs:
 
 ```bash
 ./gradlew :distribution:pulsar-io-distribution:assemble
 ```
 
+Check source code license headers:
+
+```bash
+./gradlew rat spotlessCheck
+```
+
+Auto-fix license headers:
+
+```bash
+./gradlew spotlessApply
+```
+
 ## Running Tests
 
 ```bash
@@ -94,6 +116,9 @@ To build the distribution tarball containing all connector 
NARs:
 
 # Specific connector
 ./gradlew :elastic-search:test
+
+# Specific test class
+./gradlew :elastic-search:test --tests "ElasticSearchSinkTests"
 ```
 
 ## Using Connectors
diff --git a/aerospike/build.gradle.kts b/aerospike/build.gradle.kts
index eae094f..9fb452f 100644
--- a/aerospike/build.gradle.kts
+++ b/aerospike/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/alluxio/build.gradle.kts b/alluxio/build.gradle.kts
index be406b5..9d2be5b 100644
--- a/alluxio/build.gradle.kts
+++ b/alluxio/build.gradle.kts
@@ -18,14 +18,33 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
+
+val alluxioVersion = "2.9.4"
+
+// Alluxio requires older versions of netty, grpc, and jetty than the shared 
platform provides.
+// Exclude these BOMs from the enforced platform so the alluxio-specific 
versions below can apply.
+pulsarConnectorsDependencies {
+    exclude(libs.jetty.bom)
+    exclude(libs.netty.bom)
+    exclude(libs.grpc.bom)
+}
+
 dependencies {
+    // Alluxio-compatible BOMs — these override the shared platform versions.
+    implementation(enforcedPlatform(libs.jetty9.bom))
+    implementation(enforcedPlatform("io.netty:netty-bom:4.1.100.Final"))
+    implementation(enforcedPlatform("io.grpc:grpc-bom:1.37.0"))
+
     implementation(libs.pulsar.io.core)
-    implementation("org.alluxio:alluxio-core-client-fs:2.9.3")
+    implementation("org.alluxio:alluxio-core-client-fs:$alluxioVersion")
     implementation(libs.jackson.dataformat.yaml)
     implementation(libs.guava)
 
     testImplementation(libs.pulsar.client)
-    testImplementation("org.alluxio:alluxio-minicluster:2.9.3")
-}
+    testImplementation("org.alluxio:alluxio-minicluster:$alluxioVersion") {
+        exclude(group = "org.glassfish", module = "javax.el")
+    }
+}
\ No newline at end of file
diff --git 
a/alluxio/src/main/java/org/apache/pulsar/io/alluxio/sink/AlluxioSink.java 
b/alluxio/src/main/java/org/apache/pulsar/io/alluxio/sink/AlluxioSink.java
index 3b72dc9..bde3cad 100644
--- a/alluxio/src/main/java/org/apache/pulsar/io/alluxio/sink/AlluxioSink.java
+++ b/alluxio/src/main/java/org/apache/pulsar/io/alluxio/sink/AlluxioSink.java
@@ -151,11 +151,7 @@ public class AlluxioSink implements Sink<GenericObject> {
                 } catch (AlluxioException | IOException e) {
                     log.error("Unable to flush records to alluxio.", e);
                     failRecords();
-                    try {
-                        deleteTmpFile();
-                    } catch (AlluxioException | IOException e1) {
-                        log.error("Failed to delete tmp cache file.", e);
-                    }
+                    deleteTmpFile();
                     break;
                 }
             case FILE_COMMITTED:
@@ -224,10 +220,11 @@ public class AlluxioSink implements Sink<GenericObject> {
     }
 
     private void closeAndCommitTmpFile() throws AlluxioException, IOException {
-        // close the tmpFile
-        if (fileOutStream != null) {
-            fileOutStream.close();
+        if (fileOutStream == null) {
+            return;
         }
+        // close the tmpFile
+        fileOutStream.close();
         // commit the tmpFile
         String filePrefix = alluxioSinkConfig.getFilePrefix();
         String fileExtension = alluxioSinkConfig.getFileExtension();
@@ -240,9 +237,14 @@ public class AlluxioSink implements Sink<GenericObject> {
         lastRotationTime = System.currentTimeMillis();
     }
 
-    private void deleteTmpFile() throws AlluxioException, IOException {
-        if (!tmpFilePath.equals("")) {
+    private void deleteTmpFile() {
+        if (tmpFilePath == null || tmpFilePath.isEmpty()) {
+            return;
+        }
+        try {
             fileSystem.delete(new AlluxioURI(tmpFilePath));
+        } catch (Exception e) {
+            log.warn("Failed to delete tmp file {}", tmpFilePath, e);
         }
     }
 
diff --git 
a/alluxio/src/test/java/org/apache/pulsar/io/alluxio/sink/AlluxioSinkTest.java 
b/alluxio/src/test/java/org/apache/pulsar/io/alluxio/sink/AlluxioSinkTest.java
index 2f777b6..b1b1d0f 100644
--- 
a/alluxio/src/test/java/org/apache/pulsar/io/alluxio/sink/AlluxioSinkTest.java
+++ 
b/alluxio/src/test/java/org/apache/pulsar/io/alluxio/sink/AlluxioSinkTest.java
@@ -27,6 +27,8 @@ import alluxio.client.file.URIStatus;
 import alluxio.conf.Configuration;
 import alluxio.conf.PropertyKey;
 import alluxio.master.LocalAlluxioCluster;
+import alluxio.security.authentication.AuthType;
+import java.lang.reflect.Method;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -47,6 +49,7 @@ import org.mockito.Mock;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 import org.testng.Assert;
+import org.testng.annotations.AfterClass;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
@@ -64,6 +67,7 @@ public class AlluxioSinkTest {
     protected Map<String, Object> map;
     protected AlluxioSink sink;
     protected LocalAlluxioCluster cluster;
+    protected String alluxioDir;
 
     @Mock
     protected Record<GenericObject> mockRecord;
@@ -74,7 +78,7 @@ public class AlluxioSinkTest {
     static GenericRecord fooBar;
 
     @BeforeClass
-    public static void init() {
+    public void init() throws Exception {
         valueSchema = Schema.JSON(Foobar.class);
         genericSchema = Schema.generic(valueSchema.getSchemaInfo());
         fooBar = genericSchema.newRecordBuilder()
@@ -83,25 +87,30 @@ public class AlluxioSinkTest {
                 .set("age", 20)
                 .build();
         kvSchema = Schema.KeyValue(Schema.STRING, genericSchema, 
KeyValueEncodingType.SEPARATED);
-    }
 
-    @BeforeMethod
-    public final void setUp() throws Exception {
         try {
             cluster = setupSingleMasterCluster();
         } catch (java.util.concurrent.TimeoutException e) {
             throw new org.testng.SkipException(
                     "Skipping test: Alluxio local cluster failed to start 
within timeout", e);
         }
+    }
+
+    @AfterClass
+    public void destroyCluster() throws Exception {
+        if (cluster != null) {
+            cluster.stop();
+        }
+    }
+
+    @BeforeMethod
+    public final void setUp(Method method) throws Exception {
+        alluxioDir = "/" + method.getName();
 
         map = new HashMap<>();
-        // alluxioMasterHost should be set via LocalAlluxioCluster#getHostname
-        // instead of using a fixed value "localhost", since it seems that
-        // LocalAlluxioCluster may bind other address than localhost
-        // when the node has multiple network interfaces.
         map.put("alluxioMasterHost", cluster.getHostname());
         map.put("alluxioMasterPort", cluster.getMasterRpcPort());
-        map.put("alluxioDir", "/pulsar");
+        map.put("alluxioDir", alluxioDir);
         map.put("filePrefix", "prefix");
         map.put("schemaEnable", "true");
 
@@ -131,8 +140,9 @@ public class AlluxioSinkTest {
 
     @AfterMethod
     public void tearDown() throws Exception {
-        if (cluster != null) {
-            cluster.stop();
+        if (sink != null) {
+            sink.close();
+            sink = null;
         }
     }
 
@@ -143,8 +153,6 @@ public class AlluxioSinkTest {
         map.put("lineSeparator", "\n");
         map.put("rotationRecords", "100");
 
-        String alluxioDir = "/pulsar";
-
         sink = new AlluxioSink();
         sink.open(map, mockSinkContext);
 
@@ -156,8 +164,6 @@ public class AlluxioSinkTest {
         String alluxioTmpDir = FilenameUtils.concat(alluxioDir, "tmp");
         AlluxioURI alluxioTmpURI = new AlluxioURI(alluxioTmpDir);
         Assert.assertTrue(client.exists(alluxioTmpURI));
-
-        sink.close();
     }
 
     @Test
@@ -167,9 +173,6 @@ public class AlluxioSinkTest {
         map.put("lineSeparator", "\n");
         map.put("rotationRecords", "1");
         map.put("writeType", "THROUGH");
-        map.put("alluxioDir", "/pulsar");
-
-        String alluxioDir = "/pulsar";
 
         sink = new AlluxioSink();
         sink.open(map, mockSinkContext);
@@ -201,11 +204,9 @@ public class AlluxioSinkTest {
 
         for (String path : pathList) {
             if (path.contains("tmp")) {
-                // Ensure that the temporary file is rotated and the directory 
is empty
-                Assert.assertEquals(path, "/pulsar/tmp");
+                Assert.assertEquals(path, alluxioDir + "/tmp");
             } else {
-                // Ensure that all rotated files conform the naming convention
-                Assert.assertTrue(path.startsWith("/pulsar/TopicA-"));
+                Assert.assertTrue(path.startsWith(alluxioDir + "/TopicA-"));
             }
         }
 
@@ -228,21 +229,34 @@ public class AlluxioSinkTest {
 
         for (String path : pathList) {
             if (path.contains("tmp")) {
-                Assert.assertEquals(path, "/pulsar/tmp");
+                Assert.assertEquals(path, alluxioDir + "/tmp");
             } else {
-                Assert.assertTrue(path.startsWith("/pulsar/TopicA-"));
+                Assert.assertTrue(path.startsWith(alluxioDir + "/TopicA-"));
             }
         }
-
-        sink.close();
     }
 
     private LocalAlluxioCluster setupSingleMasterCluster() throws Exception {
         // Setup and start the local alluxio cluster
+        log.info("Configuring local Alluxio cluster");
         LocalAlluxioCluster cluster = new LocalAlluxioCluster();
         cluster.initConfiguration(getTestName(getClass().getSimpleName(), 
"test"));
         Configuration.set(PropertyKey.USER_FILE_WRITE_TYPE_DEFAULT, 
WriteType.MUST_CACHE);
+        // Disable auth handshake overhead
+        Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, 
AuthType.NOSASL);
+        
Configuration.set(PropertyKey.SECURITY_AUTHORIZATION_PERMISSION_ENABLED, false);
+        // Disable unnecessary services
+        Configuration.set(PropertyKey.USER_METRICS_COLLECTION_ENABLED, false);
+        Configuration.set(PropertyKey.MASTER_AUDIT_LOGGING_ENABLED, false);
+        Configuration.set(PropertyKey.MASTER_DAILY_BACKUP_ENABLED, false);
+        
Configuration.set(PropertyKey.MASTER_STARTUP_BLOCK_INTEGRITY_CHECK_ENABLED, 
false);
+        // Minimal safe mode wait — in a local minicluster the worker 
registers almost instantly.
+        // A longer wait (e.g. 5s) causes "master is in safe mode" failures 
when tests write right
+        // after startup.
+        Configuration.set(PropertyKey.MASTER_WORKER_CONNECT_WAIT_TIME, 
"500ms");
+        log.info("Starting local Alluxio cluster");
         cluster.start();
+        log.info("Alluxio cluster started");
         return cluster;
     }
 
diff --git a/aws/build.gradle.kts b/aws/build.gradle.kts
index b845607..81fc5e0 100644
--- a/aws/build.gradle.kts
+++ b/aws/build.gradle.kts
@@ -17,6 +17,10 @@
  * under the License.
  */
 
+plugins {
+    id("pulsar-connectors.java-conventions")
+}
+
 dependencies {
     implementation(libs.pulsar.io.core)
     implementation(libs.gson)
diff --git a/azure-data-explorer/build.gradle.kts 
b/azure-data-explorer/build.gradle.kts
index 7fcedd5..0f0662b 100644
--- a/azure-data-explorer/build.gradle.kts
+++ b/azure-data-explorer/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 nar {
     narId.set("pulsar-io-azuredataexplorer")
diff --git a/alluxio/build.gradle.kts b/build-logic/conventions/build.gradle.kts
similarity index 68%
copy from alluxio/build.gradle.kts
copy to build-logic/conventions/build.gradle.kts
index be406b5..bc3d07a 100644
--- a/alluxio/build.gradle.kts
+++ b/build-logic/conventions/build.gradle.kts
@@ -18,14 +18,17 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    `kotlin-dsl`
 }
-dependencies {
-    implementation(libs.pulsar.io.core)
-    implementation("org.alluxio:alluxio-core-client-fs:2.9.3")
-    implementation(libs.jackson.dataformat.yaml)
-    implementation(libs.guava)
 
-    testImplementation(libs.pulsar.client)
-    testImplementation("org.alluxio:alluxio-minicluster:2.9.3")
+dependencies {
+    implementation(libs.plugins.shadow.get().let {
+        "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}"
+    })
+    implementation(libs.plugins.spotless.get().let {
+        "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}"
+    })
+    implementation(libs.plugins.nar.get().let {
+        "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}"
+    })
 }
diff --git a/hdfs3/build.gradle.kts 
b/build-logic/conventions/src/main/kotlin/pulsar-connectors.code-quality-conventions.gradle.kts
similarity index 50%
copy from hdfs3/build.gradle.kts
copy to 
build-logic/conventions/src/main/kotlin/pulsar-connectors.code-quality-conventions.gradle.kts
index 1f20430..ad2ffae 100644
--- a/hdfs3/build.gradle.kts
+++ 
b/build-logic/conventions/src/main/kotlin/pulsar-connectors.code-quality-conventions.gradle.kts
@@ -18,17 +18,24 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("com.diffplug.spotless")
 }
-dependencies {
-    implementation(libs.pulsar.io.core)
-    implementation(libs.jackson.databind)
-    implementation(libs.jackson.dataformat.yaml)
-    implementation(libs.commons.collections4)
-    implementation(libs.hadoop.client) {
-        exclude(group = "org.slf4j")
-        exclude(group = "log4j")
+
+// ── License header check (Spotless) ────────────────────────────────────────
+val asfLicenseHeader = rootProject.file("src/license-header.txt").readText()
+val asfLicenseHeaderJava = "/*\n" + asfLicenseHeader.lines()
+    .map { " * $it".trimEnd() }
+    .joinToString("\n") + "/\n"
+
+configure<com.diffplug.gradle.spotless.SpotlessExtension> {
+    java {
+        targetExclude(
+            "**/generated/**",
+            "**/generated-sources/**",
+            // Generated FlatBuffers files (Kinesis)
+            "**/org/apache/pulsar/io/kinesis/fbs/*.java",
+            "build/**",
+        )
+        licenseHeader(asfLicenseHeaderJava, 
"(\\n|package|import|public|class|module) ?")
     }
-    implementation(libs.bcprov.jdk18on)
-    implementation(libs.jakarta.activation.api)
 }
diff --git 
a/build-logic/conventions/src/main/kotlin/pulsar-connectors.java-conventions.gradle.kts
 
b/build-logic/conventions/src/main/kotlin/pulsar-connectors.java-conventions.gradle.kts
new file mode 100644
index 0000000..a118e33
--- /dev/null
+++ 
b/build-logic/conventions/src/main/kotlin/pulsar-connectors.java-conventions.gradle.kts
@@ -0,0 +1,227 @@
+/*
+ * 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-library`
+    id("pulsar-connectors.code-quality-conventions")
+}
+
+val catalog = the<VersionCatalogsExtension>().named("libs")
+
+fun lib(alias: String): Provider<MinimalExternalModuleDependency> =
+    catalog.findLibrary(alias).orElseThrow {
+        GradleException("Library alias '$alias' not found in version catalog 
'libs'")
+    }
+
+// Add shared test resources (log4j2-test.xml) to the test classpath for all 
modules.
+the<SourceSetContainer>()["test"].resources.srcDir(rootProject.file("gradle/test-resources"))
+
+tasks.withType<JavaCompile>().configureEach {
+    options.encoding = "UTF-8"
+    options.release.set(17)
+    options.compilerArgs.addAll(listOf("-parameters", "-Xlint:deprecation", 
"-Xlint:unchecked"))
+}
+
+configurations.all {
+    // Exclude the old SLF4J 1.x bridge pulled in by transitive dependencies.
+    // Pulsar uses SLF4J 2.x with log4j-slf4j2-impl; having both causes
+    // NoSuchMethodError in Log4jLoggerFactory at test startup.
+    exclude(group = "org.apache.logging.log4j", module = "log4j-slf4j-impl")
+}
+
+// Exclude bc-fips from modules that don't need it. bc-fips's 
CryptoServicesRegistrar
+// conflicts with bcprov-jdk18on's version — having both causes 
NoSuchMethodError.
+val modulesUsingBcFips = setOf("kafka-connect-adaptor")
+if (project.name !in modulesUsingBcFips) {
+    configurations.all {
+        exclude(group = "org.bouncycastle", module = "bc-fips")
+    }
+}
+
+/**
+ * Configures how the shared `pulsar-connectors-dependencies` platform is 
applied.
+ *
+ * By default, the platform is applied as `enforcedPlatform`, which pins 
(strictly enforces) all
+ * dependency versions from the version catalog. Subprojects can customize 
this behavior:
+ *
+ * ```kotlin
+ * pulsarConnectorsDependencies {
+ *     // Exclude using a version catalog reference:
+ *     exclude(libs.netty.bom)
+ *
+ *     // Exclude using "group:module" notation:
+ *     exclude("io.netty:netty-bom")
+ *
+ *     // Exclude using named parameters:
+ *     exclude(group = "io.netty", module = "netty-bom")
+ *
+ *     // Set enforced = false to use platform() instead of 
enforcedPlatform(). This makes all
+ *     // version constraints non-strict: the platform versions are used only 
as defaults (allowing
+ *     // version omission when declaring dependencies), but can be overridden 
by the module's own
+ *     // enforcedPlatform or strictly-versioned dependencies.
+ *     enforced = false
+ * }
+ * ```
+ */
+open class PulsarConnectorsDependenciesExtension {
+    var enforced: Boolean = true
+
+    internal val excludes: MutableList<DependencyExclusion> = mutableListOf()
+
+    fun exclude(group: String, module: String) {
+        excludes.add(DependencyExclusion(group, module))
+    }
+
+    fun exclude(dependency: Provider<MinimalExternalModuleDependency>) {
+        val dep = dependency.get()
+        excludes.add(DependencyExclusion(dep.module.group, dep.module.name))
+    }
+
+    fun exclude(notation: String) {
+        val parts = notation.split(":")
+        require(parts.size == 2) { "Expected 'group:module' format, got: 
$notation" }
+        excludes.add(DependencyExclusion(parts[0], parts[1]))
+    }
+}
+
+data class DependencyExclusion(val group: String, val module: String)
+
+val pulsarConnectorsDependencies = 
extensions.create<PulsarConnectorsDependenciesExtension>("pulsarConnectorsDependencies")
+
+// withDependencies runs lazily after subproject build scripts have configured 
the extension.
+// This is configuration-cache compatible (unlike afterEvaluate).
+configurations["implementation"].withDependencies {
+    val platformProject = project(":pulsar-connectors-dependencies")
+    val configureAction = Action<Dependency> {
+        (this as ModuleDependency).apply {
+            pulsarConnectorsDependencies.excludes.forEach { exc ->
+                exclude(group = exc.group, module = exc.module)
+            }
+        }
+    }
+    val dep = if (pulsarConnectorsDependencies.enforced) {
+        dependencies.enforcedPlatform(platformProject, configureAction)
+    } else {
+        dependencies.platform(platformProject, configureAction)
+    }
+    add(dep)
+}
+
+dependencies {
+
+    // Resolve lz4-java capability conflict: at.yawk.lz4:lz4-java (used by 
Pulsar) and
+    // org.lz4:lz4-java (used by kafka-clients) both provide the 
org.lz4:lz4-java capability.
+    // Prefer at.yawk.lz4 which is the version Pulsar standardizes on.
+    configurations.all {
+        
resolutionStrategy.capabilitiesResolution.withCapability("org.lz4:lz4-java") {
+            select("at.yawk.lz4:lz4-java:0")
+        }
+    }
+
+    // Annotation processing for Lombok
+    "compileOnly"(lib("lombok"))
+    "annotationProcessor"(lib("lombok"))
+    "testCompileOnly"(lib("lombok"))
+    "testAnnotationProcessor"(lib("lombok"))
+
+    // Common test dependencies
+    "testImplementation"(lib("testng"))
+    "testImplementation"(lib("mockito-core"))
+    "testImplementation"(lib("assertj-core"))
+    "testImplementation"(lib("awaitility"))
+    "testImplementation"(lib("system-lambda"))
+    "testImplementation"(lib("slf4j-api"))
+
+    // Logging runtime for tests — provides Log4j2 as the SLF4J backend.
+    // Some connectors (Alluxio minicluster, Solr embedded) require a logging
+    // implementation to be present at test runtime.
+    "testRuntimeOnly"(lib("log4j-api"))
+    "testRuntimeOnly"(lib("log4j-core"))
+    "testRuntimeOnly"(lib("log4j-slf4j2-impl"))
+    "testRuntimeOnly"(lib("jcl-over-slf4j"))
+}
+
+tasks.withType<Test>().configureEach {
+    useTestNG {
+        // TestNG group filtering: -PtestGroups=group1,group2 
-PexcludedTestGroups=flaky
+        providers.gradleProperty("testGroups").orNull?.let { groups ->
+            includeGroups(*groups.split(",").map { it.trim() }.toTypedArray())
+        }
+        val excludedTestGroups = 
providers.gradleProperty("excludedTestGroups").getOrElse("quarantine,flaky")
+        excludeGroups(*(excludedTestGroups.split(",").map { it.trim() 
}.toTypedArray()))
+    }
+    testLogging {
+        events("passed", "skipped", "failed")
+        exceptionFormat = 
org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
+        showStackTraces = true
+        showExceptions = true
+        showCauses = true
+        showStandardStreams = providers.gradleProperty("testShowOutput")
+            .map { it.isBlank() || it.toBoolean() }.getOrElse(false)
+    }
+    maxHeapSize = "1300m"
+    maxParallelForks = 4
+    val failFastValue = 
providers.gradleProperty("testFailFast").getOrElse("true").toBoolean()
+    failFast = failFastValue
+    systemProperty("testRetryCount", 
providers.gradleProperty("testRetryCount").getOrElse("1"))
+    systemProperty("testFailFast", failFastValue.toString())
+    systemProperty("java.net.preferIPv4Stack", "true")
+    jvmArgs(
+        "--add-opens", "java.base/jdk.internal.loader=ALL-UNNAMED",
+        "--add-opens", "java.base/java.lang=ALL-UNNAMED",
+        "--add-opens", "java.base/java.io=ALL-UNNAMED",
+        "--add-opens", "java.base/java.util=ALL-UNNAMED",
+        "--add-opens", "java.base/sun.net=ALL-UNNAMED",
+        "--add-opens", "java.management/sun.management=ALL-UNNAMED",
+        "--add-opens", 
"jdk.management/com.sun.management.internal=ALL-UNNAMED",
+        "--add-opens", "java.base/jdk.internal.platform=ALL-UNNAMED",
+        "--add-opens", "java.base/java.nio=ALL-UNNAMED",
+        "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED",
+        "-XX:+EnableDynamicAgentLoading",
+        "-Xshare:off",
+        "-Dio.netty.tryReflectionSetAccessible=true",
+        "-Dorg.apache.pulsar.shade.io.netty.tryReflectionSetAccessible=true",
+        "-Dpulsar.allocator.pooled=true",
+        "-Dpulsar.allocator.exit_on_oom=false",
+        "-Dpulsar.allocator.out_of_memory_policy=FallbackToHeap",
+        "-Dpulsar.test.preventExit=true",
+    )
+}
+
+// Set archive names to match Maven artifactId for nested modules.
+// Skip if the project name is already qualified (starts with parent name),
+// which happens for sub-modules that use qualified names in 
settings.gradle.kts
+// to avoid Gradle name clashes.
+val parentProject = project.parent
+if (parentProject != null && parentProject != rootProject && 
parentProject.parent != rootProject
+        && !project.name.startsWith(parentProject.name)) {
+    
the<BasePluginExtension>().archivesName.set("${parentProject.name}-${project.name}")
+}
+
+tasks.withType<Jar>().configureEach {
+    manifest {
+        attributes(
+            "Implementation-Title" to project.name,
+            "Implementation-Version" to project.version,
+        )
+    }
+}
+
+// Add a task for viewing all configurations for all projects in a simple way
+tasks.register<DependencyReportTask>("allDependencies"){}
diff --git 
a/build-logic/conventions/src/main/kotlin/pulsar-connectors.nar-conventions.gradle.kts
 
b/build-logic/conventions/src/main/kotlin/pulsar-connectors.nar-conventions.gradle.kts
new file mode 100644
index 0000000..29d79e3
--- /dev/null
+++ 
b/build-logic/conventions/src/main/kotlin/pulsar-connectors.nar-conventions.gradle.kts
@@ -0,0 +1,143 @@
+/*
+ * 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 for NAR (Nifi Archive) modules.
+// Configures platform module exclusions from runtimeClasspath, forces JAR 
artifacts
+// for bundled-dependencies, and handles archive name qualification.
+
+plugins {
+    id("io.github.merlimat.nar")
+}
+
+/**
+ * Extension for customizing NAR packaging behavior.
+ *
+ * By default, Pulsar platform modules (pulsar-common, pulsar-client, etc.) 
are excluded
+ * from the NAR's bundled dependencies since they are provided at runtime by 
Pulsar's
+ * classloader hierarchy. Subprojects can include specific modules when needed:
+ *
+ * ```kotlin
+ * pulsarConnectorsNar {
+ *     // Include pulsar-common in the NAR bundle (e.g., for SchemaInfoImpl)
+ *     includePulsarModule("pulsar-common")
+ * }
+ * ```
+ */
+open class PulsarConnectorsNarExtension {
+    internal val includedPulsarModules: MutableSet<String> = mutableSetOf()
+
+    fun includePulsarModule(module: String) {
+        includedPulsarModules.add(module)
+    }
+}
+
+val pulsarConnectorsNar = 
extensions.create<PulsarConnectorsNarExtension>("pulsarConnectorsNar")
+
+// NAR modules should not bundle Pulsar platform dependencies — they are 
provided
+// at runtime by Pulsar's classloader hierarchy.
+// Note: pulsar-io-common is NOT in java-instance.jar (runtime-all), so it 
must be
+// bundled in each NAR that uses it (e.g., IOConfigUtils).
+val defaultExcludedPulsarModules = setOf(
+    "pulsar-client-api",
+    "pulsar-client-admin-api",
+    "pulsar-client-original",
+    "pulsar-client",
+    "pulsar-common",
+    "pulsar-config-validation",
+    "bouncy-castle-bc",
+    "pulsar-functions-api",
+    "pulsar-functions-instance",
+    "pulsar-functions-proto",
+    "pulsar-functions-secrets",
+    "pulsar-functions-utils",
+    "pulsar-io-core",
+    "pulsar-metadata",
+    "pulsar-opentelemetry",
+    "managed-ledger",
+    "pulsar-package-core",
+)
+
+// Use withDependencies to defer exclusion logic until after subproject build 
scripts
+// have configured the extension.
+configurations.named("runtimeClasspath") {
+    exclude(group = "org.apache.bookkeeper")
+    // Protobuf is in java-instance.jar (runtime-all), so NARs must not bundle 
it.
+    // Bundling a different version causes GeneratedMessage.getUnknownFields() 
conflicts.
+    exclude(group = "com.google.protobuf")
+    withDependencies {
+        val excludedModules = defaultExcludedPulsarModules - 
pulsarConnectorsNar.includedPulsarModules
+        excludedModules.forEach { module ->
+            exclude(group = "org.apache.pulsar", module = module)
+        }
+    }
+}
+
+// The NAR plugin copies from runtimeClasspath which resolves project 
dependencies
+// as class directories, not JARs. The NarClassLoader expects JARs in
+// META-INF/bundled-dependencies/. Force the NAR task to use JAR artifacts.
+// Use lazy resolution to avoid eagerly resolving the configuration at 
configuration
+// time, which would cause configuration cache invalidation when JARs are 
created.
+tasks.named("nar", Jar::class.java) {
+    val runtimeClasspath = configurations.named("runtimeClasspath")
+    into("META-INF/bundled-dependencies") {
+        from(runtimeClasspath.map { conf ->
+            conf.incoming.artifactView {
+                attributes {
+                    attribute(
+                        LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+                        objects.named(LibraryElements::class.java, 
LibraryElements.JAR)
+                    )
+                }
+            }.files
+        })
+        duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+    }
+}
+
+// Set NAR-specific archive name qualification for nested modules.
+val parentProject = project.parent
+if (parentProject != null && parentProject != rootProject && 
parentProject.parent != rootProject
+        && !project.name.startsWith(parentProject.name)) {
+    val qualifiedName = "${parentProject.name}-${project.name}"
+    val narExt = extensions.getByName("nar")
+    @Suppress("UNCHECKED_CAST")
+    val narIdProp = narExt.javaClass.getMethod("getNarId").invoke(narExt) as 
org.gradle.api.provider.Property<String>
+    narIdProp.set(qualifiedName)
+}
+
+tasks.named<Jar>("jar") {
+    enabled = false
+}
+
+configurations {
+    named("runtimeElements") {
+        outgoing {
+            artifacts.clear()
+            artifact(tasks.named("nar"))
+            variants.clear()
+        }
+    }
+    named("apiElements") {
+        outgoing {
+            artifacts.clear()
+            artifact(tasks.named("nar"))
+            variants.clear()
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/build-logic/conventions/src/main/kotlin/pulsar-connectors.shadow-conventions.gradle.kts
 
b/build-logic/conventions/src/main/kotlin/pulsar-connectors.shadow-conventions.gradle.kts
new file mode 100644
index 0000000..7a760e5
--- /dev/null
+++ 
b/build-logic/conventions/src/main/kotlin/pulsar-connectors.shadow-conventions.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.
+ */
+
+// Convention plugin for modules using the Shadow plugin.
+// Applies the shadow plugin, disables the default jar task, and makes the
+// shadow jar the primary artifact for both runtimeElements and apiElements,
+// so plain project() dependencies resolve to the shadow jar.
+
+plugins {
+    id("com.gradleup.shadow")
+}
+
+shadow {
+    addShadowVariantIntoJavaComponent.set(false)
+}
+
+tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar")
 {
+    archiveClassifier.set("")
+    mergeServiceFiles()
+}
+
+tasks.named<Jar>("jar") {
+    enabled = false
+}
+
+configurations {
+    named("runtimeElements") {
+        outgoing {
+            artifacts.clear()
+            artifact(tasks.named("shadowJar"))
+            variants.clear()
+        }
+    }
+    named("apiElements") {
+        outgoing {
+            artifacts.clear()
+            artifact(tasks.named("shadowJar"))
+            variants.clear()
+        }
+    }
+}
diff --git a/debezium/mongodb/build.gradle.kts b/build-logic/settings.gradle.kts
similarity index 73%
copy from debezium/mongodb/build.gradle.kts
copy to build-logic/settings.gradle.kts
index 53c2670..ec81a01 100644
--- a/debezium/mongodb/build.gradle.kts
+++ b/build-logic/settings.gradle.kts
@@ -17,11 +17,16 @@
  * under the License.
  */
 
-plugins {
-    alias(libs.plugins.nar)
-}
-dependencies {
-    implementation(libs.pulsar.io.core)
-    implementation(project(":debezium:pulsar-io-debezium-core"))
-    implementation(libs.debezium.connector.mongodb)
+dependencyResolutionManagement {
+    repositories {
+        gradlePluginPortal()
+        mavenCentral()
+    }
+    versionCatalogs {
+        create("libs") {
+            from(files("../gradle/libs.versions.toml"))
+        }
+    }
 }
+rootProject.name = "build-logic"
+include("conventions")
diff --git a/build.gradle.kts b/build.gradle.kts
index 9aefca6..9f08278 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -17,269 +17,66 @@
  * under the License.
  */
 
+import com.github.vlsi.gradle.git.dsl.gitignore
+import org.jetbrains.gradle.ext.copyright
+import org.jetbrains.gradle.ext.settings
+
 plugins {
     alias(libs.plugins.rat)
+    alias(libs.plugins.version.catalog.update)
+    alias(libs.plugins.versions)
+    alias(libs.plugins.crlf) apply false
+    alias(libs.plugins.idea.ext)
+    alias(libs.plugins.spotless) apply false // workaround for 
https://github.com/diffplug/spotless/issues/2877
 }
 
-tasks.named("rat").configure {
-    val excludesProp = this.javaClass.getMethod("getExcludes").invoke(this)
-    @Suppress("UNCHECKED_CAST")
-    val excludes = excludesProp as MutableCollection<String>
-    excludes.addAll(listOf(
-        // Build artifacts
-        "**/build/**",
-        "**/target/**",
-        // Gradle files
-        ".gradle/**",
-        "gradle/wrapper/**",
-        "**/.gradle/**",
-        "**/gradle/wrapper/**",
-        // Generated Flatbuffer files (Kinesis)
-        "**/org/apache/pulsar/io/kinesis/fbs/*.java",
-        // Services files
-        "**/META-INF/services/*",
-        // Certificates and keys
-        "**/*.crt",
-        "**/*.key",
-        "**/*.csr",
-        "**/*.pem",
-        "**/*.srl",
-        "**/certificate-authority/serial",
-        "**/certificate-authority/index.txt",
-        "**/*.json",
-        "**/*.txt",
-        // Project/IDE files
-        "**/*.md",
-        ".github/**",
-        "**/*.nar",
-        "**/.gitignore",
-        "**/.gitattributes",
-        "**/*.iml",
-        "**/.classpath",
-        "**/.project",
-        "**/.settings",
-        "**/.idea/**",
-        "**/.vscode/**",
-        // Avro schemas
-        "**/*.avsc",
-        // Patch files
-        "**/*.patch",
-        // Hidden directories
-        ".*/**",
-        // Test output
-        "**/test-output/**",
-        // Log files
-        "**/*.log",
-    ))
-}
-
-val catalog = the<VersionCatalogsExtension>().named("libs")
-val pulsarConnectorsVersion = 
catalog.findVersion("pulsar-connectors").get().requiredVersion
-
-allprojects {
-    group = "org.apache.pulsar"
-    version = pulsarConnectorsVersion
-}
-
-subprojects {
-    // Platform modules use java-platform which is mutually exclusive with 
java-library.
-    if (project.name == "pulsar-dependencies") {
-        return@subprojects
-    }
-
-    // Parent modules and non-Java modules that have no source code of their 
own
-    val skipModules = setOf("jdbc", "debezium", "distribution", "docker")
-    if (project.name in skipModules && project.childProjects.isNotEmpty()) {
-        return@subprojects
-    }
-
-    apply(plugin = "java-library")
-
-    // Add shared test resources (log4j2-test.xml) to the test classpath for 
all modules.
-    
the<SourceSetContainer>()["test"].resources.srcDir(rootProject.file("gradle/test-resources"))
-
-    tasks.withType<JavaCompile> {
-        options.encoding = "UTF-8"
-        options.release.set(17)
-        options.compilerArgs.addAll(listOf("-parameters"))
-    }
-
-    configurations.all {
-        // Exclude the old SLF4J 1.x bridge
-        exclude(group = "org.apache.logging.log4j", module = 
"log4j-slf4j-impl")
-
-        // Force Jackson version to match the version catalog
-        resolutionStrategy.eachDependency {
-            if (requested.group.startsWith("com.fasterxml.jackson")) {
-                useVersion(rootProject.libs.versions.jackson.get())
-            }
-        }
-    }
-
-    // Exclude bc-fips from modules that don't need it.
-    val modulesUsingBcFips = setOf("kafka-connect-adaptor")
-    if (project.name !in modulesUsingBcFips) {
-        configurations.all {
-            exclude(group = "org.bouncycastle", module = "bc-fips")
-        }
-    }
-
-    dependencies {
-        // Enforced platform pins all dependency versions from the version 
catalog.
-        "implementation"(enforcedPlatform(project(":pulsar-dependencies")))
-
-        // Resolve lz4-java capability conflict
-        configurations.all {
-            
resolutionStrategy.capabilitiesResolution.withCapability("org.lz4:lz4-java") {
-                select("at.yawk.lz4:lz4-java:0")
-            }
-        }
-
-        // Annotation processing for Lombok
-        "compileOnly"(rootProject.libs.lombok)
-        "annotationProcessor"(rootProject.libs.lombok)
-        "testCompileOnly"(rootProject.libs.lombok)
-        "testAnnotationProcessor"(rootProject.libs.lombok)
-
-        // Common test dependencies
-        "testImplementation"(rootProject.libs.testng)
-        "testImplementation"(rootProject.libs.mockito.core)
-        "testImplementation"(rootProject.libs.assertj.core)
-        "testImplementation"(rootProject.libs.awaitility)
-        "testImplementation"(rootProject.libs.system.lambda)
-        "testImplementation"(rootProject.libs.slf4j.api)
-
-        // Logging runtime for tests — provides Log4j2 as the SLF4J backend.
-        // Some connectors (Alluxio minicluster, Solr embedded) require a 
logging
-        // implementation to be present at test runtime.
-        "testRuntimeOnly"(rootProject.libs.log4j.api)
-        "testRuntimeOnly"(rootProject.libs.log4j.core)
-        "testRuntimeOnly"(rootProject.libs.log4j.slf4j2.impl)
-        "testRuntimeOnly"(rootProject.libs.jcl.over.slf4j)
-    }
-
-    tasks.withType<Test> {
-        useTestNG {
-            // TestNG group filtering
-            providers.gradleProperty("testGroups").orNull?.let { groups ->
-                includeGroups(*groups.split(",").map { it.trim() 
}.toTypedArray())
-            }
-            val excludedTestGroups = 
providers.gradleProperty("excludedTestGroups").getOrElse("quarantine,flaky")
-            excludeGroups(*(excludedTestGroups.split(",").map { it.trim() 
}.toTypedArray()))
-        }
-        maxHeapSize = "1300m"
-        maxParallelForks = 4
-        systemProperty("testRetryCount", System.getProperty("testRetryCount", 
"1"))
-        systemProperty("testFailFast", System.getProperty("testFailFast", 
"true"))
-        jvmArgs(
-            "--add-opens", "java.base/jdk.internal.loader=ALL-UNNAMED",
-            "--add-opens", "java.base/java.lang=ALL-UNNAMED",
-            "--add-opens", "java.base/java.io=ALL-UNNAMED",
-            "--add-opens", "java.base/java.util=ALL-UNNAMED",
-            "--add-opens", "java.base/sun.net=ALL-UNNAMED",
-            "--add-opens", "java.management/sun.management=ALL-UNNAMED",
-            "--add-opens", 
"jdk.management/com.sun.management.internal=ALL-UNNAMED",
-            "--add-opens", "java.base/jdk.internal.platform=ALL-UNNAMED",
-            "--add-opens", "java.base/java.nio=ALL-UNNAMED",
-            "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED",
-            "-XX:+EnableDynamicAgentLoading",
-            "-Xshare:off",
-            "-Dio.netty.tryReflectionSetAccessible=true",
-            "-Dpulsar.allocator.pooled=true",
-            "-Dpulsar.allocator.exit_on_oom=false",
-            "-Dpulsar.allocator.out_of_memory_policy=FallbackToHeap",
-            "-Dpulsar.test.preventExit=true",
-        )
+versionCatalogUpdate {
+    sortByKey = false
+    keep {
+        keepUnusedVersions.set(true)
     }
+}
 
-    // Shadow JAR modules: expose the shadow JAR as a consumable configuration 
so other
-    // projects can depend on it via project(path = "...", configuration = 
"shadowElements")
-    pluginManager.withPlugin("com.gradleup.shadow") {
-        val shadowElements by configurations.creating {
-            isCanBeConsumed = true
-            isCanBeResolved = false
-            attributes {
-                attribute(Usage.USAGE_ATTRIBUTE, 
objects.named(Usage.JAVA_RUNTIME))
-                attribute(Bundling.BUNDLING_ATTRIBUTE, 
objects.named(Bundling.SHADOWED))
-            }
-        }
-        artifacts.add("shadowElements", tasks.named("shadowJar"))
+tasks.named<com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask>("dependencyUpdates")
 {
+    outputFormatter = "html"
+    rejectVersionIf {
+        val nonStable = candidate.version.contains("alpha") || 
candidate.version.contains("beta") || candidate.version.contains("rc")
+        // OpenTelemetry publishes stable releases with -alpha suffix for some 
modules
+        val isOpenTelemetry = candidate.group.startsWith("io.opentelemetry")
+        nonStable && !(isOpenTelemetry && candidate.version.contains("alpha"))
     }
+}
 
-    // NAR modules should not bundle Pulsar platform dependencies — they are 
provided
-    // at runtime by Pulsar's classloader hierarchy.
-    pluginManager.withPlugin("io.github.merlimat.nar") {
-        val pulsarPlatformModules = setOf(
-            "pulsar-client-api",
-            "pulsar-client-admin-api",
-            "pulsar-client-original",
-            "pulsar-client",
-            "pulsar-common",
-            "pulsar-config-validation",
-            "bouncy-castle-bc",
-            "pulsar-functions-api",
-            "pulsar-functions-instance",
-            "pulsar-functions-proto",
-            "pulsar-functions-secrets",
-            "pulsar-functions-utils",
-            "pulsar-io-core",
-            "pulsar-metadata",
-            "pulsar-opentelemetry",
-            "managed-ledger",
-            "pulsar-package-core",
-        )
-        configurations.named("runtimeClasspath") {
-            exclude(group = "org.apache.bookkeeper")
-            exclude(group = "com.google.protobuf")
-            pulsarPlatformModules.forEach { module ->
-                exclude(group = "org.apache.pulsar", module = module)
-            }
-        }
+// ── Apache RAT (Release Audit Tool) ─────────────────────────────────────────
+tasks.named<org.nosphere.apache.rat.RatTask>("rat").configure {
+    // Honour .gitignore exclusions so RAT skips untracked/generated files.
+    // Register .gitignore files as inputs so the task re-runs when they 
change.
+    inputs.files(fileTree(rootDir) {
+        include("**/.gitignore")
+        exclude("**/build/**")
+        exclude("**/.gradle/**")
+    })
+    // use crlf plugin's gitignore dsl
+    gitignore(rootDir)
+    // Apply additional RAT-specific exclusions from .ratignore.
+    val ratignoreFile = rootDir.resolve(".ratignore")
+    inputs.file(ratignoreFile)
+    exclude(ratignoreFile.readLines().map { it.trim() }.filter { 
it.isNotBlank() && !it.startsWith("#") })
+}
 
-        // The NAR plugin copies from runtimeClasspath which resolves project 
dependencies
-        // as class directories, not JARs. The NarClassLoader expects JARs in
-        // META-INF/bundled-dependencies/. Force the NAR task to use JAR 
artifacts.
-        val jarView = configurations.named("runtimeClasspath").get()
-            .incoming.artifactView {
-                attributes {
-                    attribute(
-                        LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
-                        objects.named(LibraryElements::class.java, 
LibraryElements.JAR)
-                    )
+idea {
+    project {
+        settings {
+            // add ASL2 copyright profile to IntelliJ
+            copyright {
+                useDefault = "ASL2"
+                profiles {
+                    create("ASL2") {
+                        notice = 
rootProject.file("src/license-header.txt").readText().trimEnd()
+                        keyword = "Copyright"
+                    }
                 }
-            }.files
-        tasks.named("nar", Jar::class.java) {
-            into("META-INF/bundled-dependencies") {
-                from(jarView)
-                duplicatesStrategy = DuplicatesStrategy.EXCLUDE
             }
         }
     }
-
-    // Set archive names to match Maven artifactId for nested modules.
-    val parentProject = project.parent
-    if (parentProject != null && parentProject != rootProject && 
parentProject.parent != rootProject
-            && !project.name.startsWith(parentProject.name)) {
-        val qualifiedName = "${parentProject.name}-${project.name}"
-        the<BasePluginExtension>().archivesName.set(qualifiedName)
-        pluginManager.withPlugin("io.github.merlimat.nar") {
-            @Suppress("UNCHECKED_CAST")
-            val narExt = extensions.getByName("nar")
-            val narIdProp = 
narExt.javaClass.getMethod("getNarId").invoke(narExt) as Property<String>
-            narIdProp.set(qualifiedName)
-        }
-    }
-
-    tasks.withType<Jar> {
-        manifest {
-            attributes(
-                "Implementation-Title" to project.name,
-                "Implementation-Version" to project.version,
-            )
-        }
-    }
 }
-
-// Access version catalog from subprojects
-val Project.libs: org.gradle.accessors.dm.LibrariesForLibs
-    get() = rootProject.extensions.getByType()
diff --git a/canal/build.gradle.kts b/canal/build.gradle.kts
index 2a20556..29397be 100644
--- a/canal/build.gradle.kts
+++ b/canal/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.common)
diff --git a/cassandra/build.gradle.kts b/cassandra/build.gradle.kts
index 8c2fab2..da76cc0 100644
--- a/cassandra/build.gradle.kts
+++ b/cassandra/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/debezium/core/build.gradle.kts b/debezium/core/build.gradle.kts
index df1cc24..746dc05 100644
--- a/debezium/core/build.gradle.kts
+++ b/debezium/core/build.gradle.kts
@@ -17,6 +17,9 @@
  * under the License.
  */
 
+plugins {
+    id("pulsar-connectors.java-conventions")
+}
 
 dependencies {
     compileOnly(libs.pulsar.io.core)
diff --git a/debezium/mongodb/build.gradle.kts 
b/debezium/mongodb/build.gradle.kts
index 53c2670..d4292a9 100644
--- a/debezium/mongodb/build.gradle.kts
+++ b/debezium/mongodb/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/debezium/mssql/build.gradle.kts b/debezium/mssql/build.gradle.kts
index 1cfcc2c..07aac7f 100644
--- a/debezium/mssql/build.gradle.kts
+++ b/debezium/mssql/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/debezium/mysql/build.gradle.kts b/debezium/mysql/build.gradle.kts
index bc79ff6..33e3048 100644
--- a/debezium/mysql/build.gradle.kts
+++ b/debezium/mysql/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/debezium/oracle/build.gradle.kts b/debezium/oracle/build.gradle.kts
index 35bf12b..dd1ef02 100644
--- a/debezium/oracle/build.gradle.kts
+++ b/debezium/oracle/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/debezium/postgres/build.gradle.kts 
b/debezium/postgres/build.gradle.kts
index f49f8a8..8dc37a5 100644
--- a/debezium/postgres/build.gradle.kts
+++ b/debezium/postgres/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/distribution/io/build.gradle.kts b/distribution/io/build.gradle.kts
index 91c7721..9aa7b81 100644
--- a/distribution/io/build.gradle.kts
+++ b/distribution/io/build.gradle.kts
@@ -18,9 +18,9 @@
  */
 
 // Distribution module — no Java compilation needed
-tasks.named("compileJava") { enabled = false }
-tasks.named("compileTestJava") { enabled = false }
-tasks.named("jar") { enabled = false }
+plugins {
+    base
+}
 
 val pulsarVersion = project.version.toString()
 
diff --git a/docker/pulsar-all/build.gradle.kts 
b/docker/pulsar-all/build.gradle.kts
index 5ea1aa5..448d3d1 100644
--- a/docker/pulsar-all/build.gradle.kts
+++ b/docker/pulsar-all/build.gradle.kts
@@ -18,9 +18,9 @@
  */
 
 // Docker image module — no Java compilation needed
-tasks.named("compileJava") { enabled = false }
-tasks.named("compileTestJava") { enabled = false }
-tasks.named("jar") { enabled = false }
+plugins {
+    base
+}
 
 val catalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
 val pulsarConnectorsVersion = project.version.toString()
diff --git a/docs/build.gradle.kts b/docs/build.gradle.kts
index 62cabf8..0f89397 100644
--- a/docs/build.gradle.kts
+++ b/docs/build.gradle.kts
@@ -17,6 +17,10 @@
  * under the License.
  */
 
+plugins {
+    id("pulsar-connectors.java-conventions")
+}
+
 dependencies {
     implementation(libs.pulsar.io.core)
     implementation(libs.guava)
@@ -51,6 +55,34 @@ dependencies {
     implementation(project(":rabbitmq"))
     implementation(project(":redis"))
     implementation(project(":solr"))
-    implementation(project(":alluxio"))
+    implementation(project(":alluxio")) {
+        exclude("org.eclipse.jetty", "jetty-bom")
+        exclude("io.netty", "netty-bom")
+        exclude("io.grpc", "grpc-bom")
+    }
     implementation(project(":azure-data-explorer"))
 }
+
+val exportClasspath by tasks.registering {
+    dependsOn(tasks.classes)
+    val classpath = sourceSets.main.get().output + 
configurations.runtimeClasspath.get()
+    inputs.files(classpath)
+    val outputFile = layout.buildDirectory.file("classpath.txt")
+    outputs.file(outputFile)
+    doLast {
+        outputFile.get().asFile.apply {
+            parentFile.mkdirs()
+            writeText(classpath.asPath)
+        }
+    }
+}
+
+tasks.register<JavaExec>("generateConnectorDocs") {
+    dependsOn(tasks.classes)
+    mainClass.set("org.apache.pulsar.io.docs.ConnectorDocGenerator")
+    classpath = sourceSets.main.get().output + 
configurations.runtimeClasspath.get()
+    inputs.files(classpath)
+    val outputDir = layout.buildDirectory.dir("connector-docs")
+    outputs.dir(outputDir)
+    args("-o", outputDir.get().asFile.absolutePath)
+}
\ No newline at end of file
diff --git a/gradle.properties b/docs/pulsar-io-gen.sh
old mode 100644
new mode 100755
similarity index 59%
copy from gradle.properties
copy to docs/pulsar-io-gen.sh
index 20a29b3..0f2416a
--- a/gradle.properties
+++ b/docs/pulsar-io-gen.sh
@@ -1,3 +1,4 @@
+#!/usr/bin/env bash
 #
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
@@ -17,7 +18,17 @@
 # under the License.
 #
 
-org.gradle.configuration-cache=true
-org.gradle.parallel=true
-org.gradle.caching=true
-org.gradle.jvmargs=-Xmx4g -Xss2m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
+# Check for the java to use
+if [[ -z $JAVA_HOME ]]; then
+    JAVA=$(which java)
+    if [ $? != 0 ]; then
+        echo "Error: JAVA_HOME not set, and no java executable found in 
$PATH." 1>&2
+        exit 1
+    fi
+else
+    JAVA=$JAVA_HOME/bin/java
+fi
+CLASSPATH_FILE="$SCRIPT_DIR/build/classpath.txt"
+$SCRIPT_DIR/../gradlew exportClasspath --console=plain > /dev/null
+$JAVA -cp "$(cat $CLASSPATH_FILE)" 
org.apache.pulsar.io.docs.ConnectorDocGenerator "$@"
\ No newline at end of file
diff --git 
a/docs/src/main/java/org/apache/pulsar/io/docs/ConnectorDocGenerator.java 
b/docs/src/main/java/org/apache/pulsar/io/docs/ConnectorDocGenerator.java
index 2e9d6a9..e159c88 100644
--- a/docs/src/main/java/org/apache/pulsar/io/docs/ConnectorDocGenerator.java
+++ b/docs/src/main/java/org/apache/pulsar/io/docs/ConnectorDocGenerator.java
@@ -28,6 +28,7 @@ import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
@@ -51,8 +52,11 @@ public class ConnectorDocGenerator implements 
Callable<Integer> {
     private static Reflections newReflections() throws Exception {
         final String[] classpathList = 
System.getProperty("java.class.path").split(":");
         final List<URL> urlList = new ArrayList<>();
-        for (String file : classpathList) {
-            urlList.add(new File(file).toURI().toURL());
+        for (String cp : classpathList) {
+            File file = new File(cp);
+            if (file.isFile() && cp.endsWith(".nar")) {
+                urlList.add(new URL("jar:" + file.toURI() + "!/"));
+            }
         }
         return new Reflections(new ConfigurationBuilder().setUrls(urlList));
     }
@@ -70,7 +74,7 @@ public class ConnectorDocGenerator implements 
Callable<Integer> {
 
         Field[] fields = configClass.getDeclaredFields();
         for (Field field : fields) {
-            if (Modifier.isStatic(field.getModifiers())) {
+            if (Modifier.isStatic(field.getModifiers()) || 
Modifier.isTransient(field.getModifiers())) {
                 continue;
             }
             FieldDoc fieldDoc = field.getDeclaredAnnotation(FieldDoc.class);
@@ -107,12 +111,16 @@ public class ConnectorDocGenerator implements 
Callable<Integer> {
         Set<Class<?>> connectorClasses = 
reflections.getTypesAnnotatedWith(Connector.class);
         log.info("Retrieve all `Connector` annotated classes : {}", 
connectorClasses);
 
+        Path outputDirPath = Path.of(outputDir);
+        if (!Files.exists(outputDirPath)) {
+            Files.createDirectories(outputDirPath);
+        }
         for (Class<?> connectorClass : connectorClasses) {
             final Connector connectorDef = 
connectorClass.getDeclaredAnnotation(Connector.class);
             final String name = connectorDef.name().toLowerCase();
             final String type = connectorDef.type().name().toLowerCase();
             final String filename = "pulsar-io-%s-%s.yml".formatted(name, 
type);
-            final Path outputPath = Path.of(outputDir, filename);
+            final Path outputPath = outputDirPath.resolve(filename);
             try (FileOutputStream fos = new 
FileOutputStream(outputPath.toFile())) {
                 PrintWriter pw = new PrintWriter(new OutputStreamWriter(fos, 
StandardCharsets.UTF_8));
                 generateConnectorYamlFile(connectorClass, connectorDef, pw);
@@ -125,7 +133,7 @@ public class ConnectorDocGenerator implements 
Callable<Integer> {
             names = {"-o", "--output-dir"},
             description = "The output dir to dump connector docs",
             required = true)
-    String outputDir = null;
+    String outputDir;
 
     @Option(names = {"-h", "--help"}, usageHelp = true, description = "Show 
this help message")
     boolean help = false;
diff --git a/dynamodb/build.gradle.kts b/dynamodb/build.gradle.kts
index 7697d16..980e18c 100644
--- a/dynamodb/build.gradle.kts
+++ b/dynamodb/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.common)
diff --git a/elastic-search/build.gradle.kts b/elastic-search/build.gradle.kts
index 620cb77..1b2022c 100644
--- a/elastic-search/build.gradle.kts
+++ b/elastic-search/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     compileOnly(libs.pulsar.io.core)
diff --git a/file/build.gradle.kts b/file/build.gradle.kts
index a22c7ac..3725b4f 100644
--- a/file/build.gradle.kts
+++ b/file/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/gradle.properties b/gradle.properties
index 20a29b3..fc83eee 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,7 +17,11 @@
 # under the License.
 #
 
+group=org.apache.pulsar
+version=4.3.0-SNAPSHOT
+
 org.gradle.configuration-cache=true
+org.gradle.configureondemand=true
 org.gradle.parallel=true
 org.gradle.caching=true
 org.gradle.jvmargs=-Xmx4g -Xss2m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0e87504..743d543 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -16,26 +16,21 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-
 [versions]
 # Core
-pulsar = "4.1.3"
-pulsar-connectors = "4.3.0-SNAPSHOT"
+pulsar = "4.2.0"
 java = "17"
-
-# Docker
-docker-jdk = "21"
-pulsar-client-python = "3.10.0"
-
 # Code quality
-checkstyle = "10.14.2"
-
+checkstyle = "13.3.0"
+spotless = "8.4.0"
+idea-ext = "1.4.1"
 # Major frameworks
 bookkeeper = "4.17.3"
 zookeeper = "3.9.5"
 netty = "4.1.132.Final"
 netty-iouring = "0.0.26.Final"
 jetty = "12.1.5"
+jetty9 = "9.4.58.v20250814"
 jersey = "2.42"
 jackson = "2.18.6"
 protobuf = "3.25.5"
@@ -43,16 +38,14 @@ grpc = "1.75.0"
 slf4j = "2.0.17"
 log4j2 = "2.25.3"
 lombok = "1.18.42"
-
 # OpenTelemetry
 opentelemetry = "1.56.0"
 opentelemetry-alpha = "1.56.0-alpha"
 opentelemetry-instrumentation = "2.21.0"
 opentelemetry-instrumentation-alpha = "2.21.0-alpha"
 opentelemetry-semconv = "1.37.0"
-
 # Apache Commons
-commons-lang3 = "3.19.0"
+commons-lang3 = "3.20.0"
 commons-io = "2.21.0"
 commons-codec = "1.20.0"
 commons-compress = "1.28.0"
@@ -63,7 +56,6 @@ commons-math3 = "3.6.1"
 commons-logging = "1.3.5"
 commons-beanutils = "1.11.0"
 commons-configuration2 = "2.12.0"
-
 # BouncyCastle
 bouncycastle-bcprov = "1.78.1"
 bouncycastle-bcpkix = "1.81"
@@ -71,49 +63,40 @@ bouncycastle-bcutil = "1.81"
 bouncycastle-bcprov-ext = "1.78.1"
 bouncycastle-bcpkix-fips = "2.0.10"
 bouncycastle-bc-fips = "2.0.1"
-
 # Serialization
 avro = "1.12.0"
 gson = "2.13.2"
 snakeyaml = "2.0"
-
 # Vert.x
 vertx = "4.5.24"
-
 # Networking / HTTP
 asynchttpclient = "2.12.4"
 conscrypt = "2.5.2"
 okhttp3 = "5.3.1"
 okio = "3.16.3"
-netty-tcnative = "2.0.75.Final"
 httpcomponents-httpclient = "4.5.13"
 httpcomponents-httpcore = "4.4.15"
-
 # Google libraries (transitive deps, versions managed to match Maven)
 google-auth = "1.24.1"
 google-http-client = "1.41.0"
 j2objc-annotations = "1.3"
 opencensus = "0.28.0"
 opentelemetry-gcp-resources = "1.48.0-alpha"
-
 # Data structures / Utils
 guava = "33.4.8-jre"
 caffeine = "3.2.3"
-fastutil = "8.5.16"
 jctools = "4.0.5"
 roaringbitmap = "1.6.9"
 hppc = "0.9.1"
 aircompressor = "2.0.3"
 completable-futures = "0.3.6"
 re2j = "1.8"
-
 # Metrics / Observability
 prometheus = "0.16.0"
 prometheus-jmx = "0.16.1"
 dropwizardmetrics = "4.1.12.1"
 hdrHistogram = "2.1.9"
 perfmark = "0.26.0"
-
 # Auth
 jsonwebtoken = "0.13.0"
 athenz = "1.10.62"
@@ -121,18 +104,15 @@ jose4j = "0.9.6"
 nimbus-jose-jwt = "9.37.4"
 auth0-java-jwt = "4.3.0"
 auth0-jwks-rsa = "0.22.0"
-
 # CLI
 picocli = "4.7.5"
 jline3 = "3.21.0"
 jline2 = "2.14.6"
-
 javassist = "3.25.0-GA"
 rocksdb = "7.9.2"
 kotlin-stdlib = "1.8.20"
 audience-annotations = "0.12.0"
 jetbrains-annotations = "13.0"
-
 # Misc
 curator = "5.7.1"
 reflections = "0.10.2"
@@ -151,7 +131,6 @@ guice = "5.1.0"
 snappy = "1.1.10.8"
 ipaddress = "5.5.0"
 zt-zip = "1.17"
-
 # Jakarta
 jakarta-ws-rs = "2.1.6"
 jakarta-annotation = "1.3.5"
@@ -159,17 +138,14 @@ jakarta-activation = "1.2.2"
 jakarta-xml-bind = "2.3.3"
 jakarta-validation = "2.0.2"
 javax-servlet = "3.1.0"
-
 # Oxia / etcd
 oxia = "0.7.4"
-
 # Build plugins
-lightproto = "0.6.2"
+lightproto = "0.6.6"
 errorprone = "2.45.0"
 spotbugs = "4.9.6"
 checkerframework = "3.33.0"
 jsr305 = "3.0.2"
-
 # Test
 testng = "7.7.1"
 mockito = "5.19.0"
@@ -189,16 +165,13 @@ skyscreamer = "1.5.0"
 zstd-jni = "1.5.7-3"
 lz4java = "1.10.3"
 spring = "6.2.12"
-
 # Connectors / IO
 kafka-client = "4.1.1"
 confluent = "8.1.1"
 opensearch = "2.19.4"
 elasticsearch-java = "8.15.3"
 debezium = "3.4.2.Final"
-debezium-mysql-connector = "9.4.0"
 kubernetesclient = "23.0.0"
-
 # IO connector specific
 aerospike-client = "4.5.0"
 aws-sdk = "1.12.788"
@@ -214,67 +187,47 @@ solr = "9.8.0"
 hbase = "2.6.4-hadoop3"
 hadoop3 = "3.4.3"
 jclouds = "2.6.0"
-
+openmldb = "0.4.4-hotfix1"
+docker-java = "3.7.1"
 # Shading
-shadow = "9.3.2"
+shadow = "9.4.1"
 
 [libraries]
 # SLF4J
-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
-jcl-over-slf4j = { module = "org.slf4j:jcl-over-slf4j", version.ref = "slf4j" }
-
+slf4j-bom = { module = "org.slf4j:slf4j-bom", version.ref = "slf4j" }
+slf4j-api = { module = "org.slf4j:slf4j-api" }
+jcl-over-slf4j = { module = "org.slf4j:jcl-over-slf4j" }
 # Log4j2
-log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = 
"log4j2" }
-log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = 
"log4j2" }
-log4j-web = { module = "org.apache.logging.log4j:log4j-web", version.ref = 
"log4j2" }
-log4j-layout-template-json = { module = 
"org.apache.logging.log4j:log4j-layout-template-json", version.ref = "log4j2" }
-log4j-slf4j2-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", 
version.ref = "log4j2" }
-
+log4j-bom = { module = "org.apache.logging.log4j:log4j-bom", version.ref = 
"log4j2" }
+log4j-api = { module = "org.apache.logging.log4j:log4j-api" }
+log4j-core = { module = "org.apache.logging.log4j:log4j-core" }
+log4j-slf4j2-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl" }
 # Lombok
 lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
-
 # Jackson
 jackson-bom = { module = "com.fasterxml.jackson:jackson-bom", version.ref = 
"jackson" }
-jackson-annotations = { module = 
"com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" }
-jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", 
version.ref = "jackson" }
-jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", 
version.ref = "jackson" }
-jackson-module-parameter-names = { module = 
"com.fasterxml.jackson.module:jackson-module-parameter-names", version.ref = 
"jackson" }
-jackson-datatype-jsr310 = { module = 
"com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = 
"jackson" }
-jackson-datatype-jdk8 = { module = 
"com.fasterxml.jackson.datatype:jackson-datatype-jdk8", version.ref = "jackson" 
}
-jackson-dataformat-yaml = { module = 
"com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = 
"jackson" }
-jackson-module-jsonSchema = { module = 
"com.fasterxml.jackson.module:jackson-module-jsonSchema", version.ref = 
"jackson" }
-jackson-jaxrs-json-provider = { module = 
"com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider", version.ref = 
"jackson" }
-
+jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind" }
+jackson-datatype-jsr310 = { module = 
"com.fasterxml.jackson.datatype:jackson-datatype-jsr310" }
+jackson-dataformat-yaml = { module = 
"com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" }
+jackson-dataformat-cbor = { module = 
"com.fasterxml.jackson.dataformat:jackson-dataformat-cbor" }
 # Netty
 netty-bom = { module = "io.netty:netty-bom", version.ref = "netty" }
-netty-common = { module = "io.netty:netty-common", version.ref = "netty" }
-netty-buffer = { module = "io.netty:netty-buffer", version.ref = "netty" }
-netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" }
-netty-transport = { module = "io.netty:netty-transport", version.ref = "netty" 
}
-netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = 
"netty" }
-netty-codec-http2 = { module = "io.netty:netty-codec-http2", version.ref = 
"netty" }
-netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = 
"netty" }
-netty-handler-proxy = { module = "io.netty:netty-handler-proxy", version.ref = 
"netty" }
-netty-codec-socks = { module = "io.netty:netty-codec-socks", version.ref = 
"netty" }
-netty-resolver-dns = { module = "io.netty:netty-resolver-dns", version.ref = 
"netty" }
-netty-resolver-dns-native-macos = { module = 
"io.netty:netty-resolver-dns-native-macos", version.ref = "netty" }
-netty-transport-native-epoll = { module = 
"io.netty:netty-transport-native-epoll", version.ref = "netty" }
-netty-transport-native-unix-common = { module = 
"io.netty:netty-transport-native-unix-common", version.ref = "netty" }
-netty-tcnative-boringssl-static = { module = 
"io.netty:netty-tcnative-boringssl-static", version.ref = "netty-tcnative" }
+netty-buffer = { module = "io.netty:netty-buffer" }
+netty-handler = { module = "io.netty:netty-handler" }
+netty-codec-http = { module = "io.netty:netty-codec-http" }
 netty-incubator-transport-classes-io_uring = { module = 
"io.netty.incubator:netty-incubator-transport-classes-io_uring", version.ref = 
"netty-iouring" }
 netty-incubator-transport-native-io-uring = { module = 
"io.netty.incubator:netty-incubator-transport-native-io_uring", version.ref = 
"netty-iouring" }
 netty-reactive-streams = { module = 
"com.typesafe.netty:netty-reactive-streams", version.ref = 
"netty-reactive-streams" }
-
 # Protobuf / gRPC
-protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = 
"protobuf" }
-protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", 
version.ref = "protobuf" }
-grpc-all = { module = "io.grpc:grpc-all", version.ref = "grpc" }
-grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded", version.ref = 
"grpc" }
-grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" }
-
+protobuf-bom = { module = "com.google.protobuf:protobuf-bom", version.ref = 
"protobuf" }
+protobuf-java = { module = "com.google.protobuf:protobuf-java" }
+protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util" }
+grpc-bom = { module = "io.grpc:grpc-bom", version.ref = "grpc" }
+grpc-all = { module = "io.grpc:grpc-all" }
+grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded" }
+grpc-stub = { module = "io.grpc:grpc-stub" }
 # Guava
 guava = { module = "com.google.guava:guava", version.ref = "guava" }
-
 # Apache Commons
 commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = 
"commons-lang3" }
 commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
@@ -283,7 +236,6 @@ commons-compress = { module = 
"org.apache.commons:commons-compress", version.ref
 commons-collections4 = { module = "org.apache.commons:commons-collections4", 
version.ref = "commons-collections4" }
 commons-cli = { module = "commons-cli:commons-cli", version.ref = 
"commons-cli" }
 commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = 
"commons-math3" }
-
 # BookKeeper
 bookkeeper-server = { module = "org.apache.bookkeeper:bookkeeper-server", 
version.ref = "bookkeeper" }
 bookkeeper-common-allocator = { module = 
"org.apache.bookkeeper:bookkeeper-common-allocator", version.ref = "bookkeeper" 
}
@@ -297,86 +249,74 @@ bookkeeper-stream-storage-service-impl = { module = 
"org.apache.bookkeeper:strea
 bookkeeper-tools-framework = { module = 
"org.apache.bookkeeper:bookkeeper-tools-framework", version.ref = "bookkeeper" }
 bookkeeper-http-vertx-server = { module = 
"org.apache.bookkeeper.http:vertx-http-server", version.ref = "bookkeeper" }
 distributedlog-core = { module = 
"org.apache.distributedlog:distributedlog-core", version.ref = "bookkeeper" }
-
 # ZooKeeper
 zookeeper = { module = "org.apache.zookeeper:zookeeper", version.ref = 
"zookeeper" }
 zookeeper-tests = { module = "org.apache.zookeeper:zookeeper", version.ref = 
"zookeeper" }
-
 # Curator
 curator-recipes = { module = "org.apache.curator:curator-recipes", version.ref 
= "curator" }
-
 # Conscrypt
 conscrypt-openjdk-uber = { module = "org.conscrypt:conscrypt-openjdk-uber", 
version.ref = "conscrypt" }
-
 # Jetty
-jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = 
"jetty" }
-jetty-util = { module = "org.eclipse.jetty:jetty-util", version.ref = "jetty" }
-jetty-alpn-conscrypt-server = { module = 
"org.eclipse.jetty:jetty-alpn-conscrypt-server", version.ref = "jetty" }
-jetty-compression-server = { module = 
"org.eclipse.jetty.compression:jetty-compression-server", version.ref = "jetty" 
}
-jetty-compression-gzip = { module = 
"org.eclipse.jetty.compression:jetty-compression-gzip", version.ref = "jetty" }
-jetty-client = { module = "org.eclipse.jetty:jetty-client", version.ref = 
"jetty" }
-jetty-ee8-servlet = { module = "org.eclipse.jetty.ee8:jetty-ee8-servlet", 
version.ref = "jetty" }
-jetty-ee8-servlets = { module = "org.eclipse.jetty.ee8:jetty-ee8-servlets", 
version.ref = "jetty" }
-jetty-ee8-proxy = { module = "org.eclipse.jetty.ee8:jetty-ee8-proxy", 
version.ref = "jetty" }
-jetty-websocket-jetty-api = { module = 
"org.eclipse.jetty.websocket:jetty-websocket-jetty-api", version.ref = "jetty" }
-jetty-websocket-jetty-client = { module = 
"org.eclipse.jetty.websocket:jetty-websocket-jetty-client", version.ref = 
"jetty" }
-jetty-ee8-websocket-jetty-server = { module = 
"org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-server", version.ref 
= "jetty" }
-
+jetty-bom = { module = "org.eclipse.jetty:jetty-bom", version.ref = "jetty" }
+jetty9-bom = { module = "org.eclipse.jetty:jetty-bom", version.ref = "jetty9" }
+jetty-server = { module = "org.eclipse.jetty:jetty-server" }
+jetty-util = { module = "org.eclipse.jetty:jetty-util" }
+jetty-alpn-conscrypt-server = { module = 
"org.eclipse.jetty:jetty-alpn-conscrypt-server" }
+jetty-compression-server = { module = 
"org.eclipse.jetty.compression:jetty-compression-server" }
+jetty-compression-gzip = { module = 
"org.eclipse.jetty.compression:jetty-compression-gzip" }
+jetty-client = { module = "org.eclipse.jetty:jetty-client" }
+jetty-ee8-servlet = { module = "org.eclipse.jetty.ee8:jetty-ee8-servlet" }
+jetty-ee8-servlets = { module = "org.eclipse.jetty.ee8:jetty-ee8-servlets" }
+jetty-ee8-proxy = { module = "org.eclipse.jetty.ee8:jetty-ee8-proxy" }
+jetty-websocket-jetty-api = { module = 
"org.eclipse.jetty.websocket:jetty-websocket-jetty-api" }
+jetty-websocket-jetty-client = { module = 
"org.eclipse.jetty.websocket:jetty-websocket-jetty-client" }
+jetty-ee8-websocket-jetty-server = { module = 
"org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-server" }
 # Jersey
-jersey-server = { module = "org.glassfish.jersey.core:jersey-server", 
version.ref = "jersey" }
-jersey-container-servlet-core = { module = 
"org.glassfish.jersey.containers:jersey-container-servlet-core", version.ref = 
"jersey" }
-jersey-container-servlet = { module = 
"org.glassfish.jersey.containers:jersey-container-servlet", version.ref = 
"jersey" }
-jersey-media-json-jackson = { module = 
"org.glassfish.jersey.media:jersey-media-json-jackson", version.ref = "jersey" }
-jersey-client = { module = "org.glassfish.jersey.core:jersey-client", 
version.ref = "jersey" }
-jersey-hk2 = { module = "org.glassfish.jersey.inject:jersey-hk2", version.ref 
= "jersey" }
-jersey-media-multipart = { module = 
"org.glassfish.jersey.media:jersey-media-multipart", version.ref = "jersey" }
-jersey-test-framework-core = { module = 
"org.glassfish.jersey.test-framework:jersey-test-framework-core", version.ref = 
"jersey" }
-jersey-test-framework-grizzly2 = { module = 
"org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2",
 version.ref = "jersey" }
-
+jersey-bom = { module = "org.glassfish.jersey:jersey-bom", version.ref = 
"jersey" }
+jersey-server = { module = "org.glassfish.jersey.core:jersey-server" }
+jersey-container-servlet-core = { module = 
"org.glassfish.jersey.containers:jersey-container-servlet-core" }
+jersey-container-servlet = { module = 
"org.glassfish.jersey.containers:jersey-container-servlet" }
+jersey-media-json-jackson = { module = 
"org.glassfish.jersey.media:jersey-media-json-jackson" }
+jersey-client = { module = "org.glassfish.jersey.core:jersey-client" }
+jersey-hk2 = { module = "org.glassfish.jersey.inject:jersey-hk2" }
+jersey-media-multipart = { module = 
"org.glassfish.jersey.media:jersey-media-multipart" }
+jersey-test-framework-core = { module = 
"org.glassfish.jersey.test-framework:jersey-test-framework-core" }
+jersey-test-framework-grizzly2 = { module = 
"org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2"
 }
 # Prometheus
-simpleclient = { module = "io.prometheus:simpleclient", version.ref = 
"prometheus" }
-simpleclient-hotspot = { module = "io.prometheus:simpleclient_hotspot", 
version.ref = "prometheus" }
-simpleclient-caffeine = { module = "io.prometheus:simpleclient_caffeine", 
version.ref = "prometheus" }
-simpleclient-httpserver = { module = "io.prometheus:simpleclient_httpserver", 
version.ref = "prometheus" }
-simpleclient-servlet = { module = "io.prometheus:simpleclient_servlet", 
version.ref = "prometheus" }
-simpleclient-common = { module = "io.prometheus:simpleclient_common", 
version.ref = "prometheus" }
-simpleclient-log4j2 = { module = "io.prometheus:simpleclient_log4j2", 
version.ref = "prometheus" }
+simpleclient-bom = { module = "io.prometheus:simpleclient_bom", version.ref = 
"prometheus" }
 prometheus-jmx-collector = { module = "io.prometheus.jmx:collector", 
version.ref = "prometheus-jmx" }
-
 # OpenTelemetry
-opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api", 
version.ref = "opentelemetry" }
-opentelemetry-api-incubator = { module = 
"io.opentelemetry:opentelemetry-api-incubator", version.ref = 
"opentelemetry-alpha" }
-opentelemetry-sdk = { module = "io.opentelemetry:opentelemetry-sdk", 
version.ref = "opentelemetry" }
-opentelemetry-sdk-extension-autoconfigure = { module = 
"io.opentelemetry:opentelemetry-sdk-extension-autoconfigure", version.ref = 
"opentelemetry" }
-opentelemetry-sdk-testing = { module = 
"io.opentelemetry:opentelemetry-sdk-testing", version.ref = "opentelemetry" }
-opentelemetry-exporter-otlp = { module = 
"io.opentelemetry:opentelemetry-exporter-otlp", version.ref = "opentelemetry" }
-opentelemetry-exporter-prometheus = { module = 
"io.opentelemetry:opentelemetry-exporter-prometheus", version.ref = 
"opentelemetry-alpha" }
-opentelemetry-instrumentation-resources = { module = 
"io.opentelemetry.instrumentation:opentelemetry-resources", version.ref = 
"opentelemetry-instrumentation-alpha" }
-opentelemetry-instrumentation-runtime-telemetry-java17 = { module = 
"io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17", 
version.ref = "opentelemetry-instrumentation-alpha" }
+opentelemetry-bom = { module = "io.opentelemetry:opentelemetry-bom", 
version.ref = "opentelemetry" }
+opentelemetry-bom-alpha = { module = 
"io.opentelemetry:opentelemetry-bom-alpha", version.ref = "opentelemetry-alpha" 
}
+opentelemetry-instrumentation-bom = { module = 
"io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom", 
version.ref = "opentelemetry-instrumentation" }
+opentelemetry-instrumentation-bom-alpha = { module = 
"io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha", 
version.ref = "opentelemetry-instrumentation-alpha" }
+opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api" }
+opentelemetry-api-incubator = { module = 
"io.opentelemetry:opentelemetry-api-incubator" }
+opentelemetry-sdk = { module = "io.opentelemetry:opentelemetry-sdk" }
+opentelemetry-sdk-extension-autoconfigure = { module = 
"io.opentelemetry:opentelemetry-sdk-extension-autoconfigure" }
+opentelemetry-sdk-testing = { module = 
"io.opentelemetry:opentelemetry-sdk-testing" }
+opentelemetry-exporter-otlp = { module = 
"io.opentelemetry:opentelemetry-exporter-otlp" }
+opentelemetry-exporter-prometheus = { module = 
"io.opentelemetry:opentelemetry-exporter-prometheus" }
+opentelemetry-instrumentation-resources = { module = 
"io.opentelemetry.instrumentation:opentelemetry-resources" }
+opentelemetry-instrumentation-runtime-telemetry-java17 = { module = 
"io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17" }
 opentelemetry-semconv = { module = 
"io.opentelemetry.semconv:opentelemetry-semconv", version.ref = 
"opentelemetry-semconv" }
 opentelemetry-gcp-resources = { module = 
"io.opentelemetry.contrib:opentelemetry-gcp-resources", version.ref = 
"opentelemetry-gcp-resources" }
-
 # BouncyCastle
 bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = 
"bouncycastle-bcpkix" }
 bcprov-ext-jdk18on = { module = "org.bouncycastle:bcprov-ext-jdk18on", 
version.ref = "bouncycastle-bcprov-ext" }
 bcpkix-fips = { module = "org.bouncycastle:bcpkix-fips", version.ref = 
"bouncycastle-bcpkix-fips" }
 bc-fips = { module = "org.bouncycastle:bc-fips", version.ref = 
"bouncycastle-bc-fips" }
-bcutil-fips = { module = "org.bouncycastle:bcutil-fips", version = "2.0.5" }
-
+bcutil-fips = "org.bouncycastle:bcutil-fips:2.0.5"
 # RocksDB
 rocksdbjni = { module = "org.rocksdb:rocksdbjni", version.ref = "rocksdb" }
-
 # Error Prone
 error-prone-annotations = { module = 
"com.google.errorprone:error_prone_annotations", version.ref = "errorprone" }
-
 # Data structures
 caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = 
"caffeine" }
-fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" }
 jctools-core = { module = "org.jctools:jctools-core", version.ref = "jctools" }
 roaringbitmap = { module = "org.roaringbitmap:RoaringBitmap", version.ref = 
"roaringbitmap" }
 hppc = { module = "com.carrotsearch:hppc", version.ref = "hppc" }
 aircompressor = { module = "io.airlift:aircompressor", version.ref = 
"aircompressor" }
-
 # Misc libs
 gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
 re2j = { module = "com.google.re2j:re2j", version.ref = "re2j" }
@@ -391,10 +331,12 @@ javassist = { module = "org.javassist:javassist", 
version.ref = "javassist" }
 commons-text = { module = "org.apache.commons:commons-text", version.ref = 
"commons-text" }
 typetools = { module = "net.jodah:typetools", version.ref = "typetools" }
 perfmark-api = { module = "io.perfmark:perfmark-api", version.ref = "perfmark" 
}
-okhttp3 = { module = "com.squareup.okhttp3:okhttp-jvm", version.ref = 
"okhttp3" }
-okhttp3-logging-interceptor = { module = 
"com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp3" }
-okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
-# Transitive dep version pins (enforced by pulsar-dependencies platform)
+okhttp3-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = 
"okhttp3" }
+okhttp3 = { module = "com.squareup.okhttp3:okhttp-jvm" }
+okhttp3-logging-interceptor = { module = 
"com.squareup.okhttp3:logging-interceptor" }
+okio-bom = { module = "com.squareup.okio:okio-bom", version.ref = "okio" }
+okio = { module = "com.squareup.okio:okio" }
+# Transitive dep version pins (enforced by pulsar-connectors-dependencies 
platform)
 google-auth-library-credentials = { module = 
"com.google.auth:google-auth-library-credentials", version.ref = "google-auth" }
 google-auth-library-oauth2-http = { module = 
"com.google.auth:google-auth-library-oauth2-http", version.ref = "google-auth" }
 google-http-client = { module = "com.google.http-client:google-http-client", 
version.ref = "google-http-client" }
@@ -406,7 +348,6 @@ httpcomponents-httpclient = { module = 
"org.apache.httpcomponents:httpclient", v
 httpcomponents-httpcore = { module = "org.apache.httpcomponents:httpcore", 
version.ref = "httpcomponents-httpcore" }
 jakarta-annotation-api = { module = 
"jakarta.annotation:jakarta.annotation-api", version.ref = "jakarta-annotation" 
}
 kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = 
"kotlin-stdlib" }
-
 snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" }
 ant = { module = "org.apache.ant:ant", version.ref = "ant" }
 guice = { module = "com.google.inject:guice", version.ref = "guice" }
@@ -416,7 +357,7 @@ vertx-core = { module = "io.vertx:vertx-core", version.ref 
= "vertx" }
 vertx-web = { module = "io.vertx:vertx-web", version.ref = "vertx" }
 avro = { module = "org.apache.avro:avro", version.ref = "avro" }
 avro-protobuf = { module = "org.apache.avro:avro-protobuf", version.ref = 
"avro" }
-joda-time = { module = "joda-time:joda-time", version = "2.10.10" }
+joda-time = "joda-time:joda-time:2.10.10"
 sketches-core = { module = "com.yahoo.datasketches:sketches-core", version.ref 
= "sketches" }
 java-semver = { module = "com.github.zafarkhaja:java-semver", version.ref = 
"java-semver" }
 oshi-core = { module = "com.github.oshi:oshi-core-java11", version.ref = 
"oshi" }
@@ -426,16 +367,15 @@ dropwizardmetrics-core = { module = 
"io.dropwizard.metrics:metrics-core", versio
 dropwizardmetrics-graphite = { module = 
"io.dropwizard.metrics:metrics-graphite", version.ref = "dropwizardmetrics" }
 dropwizardmetrics-jvm = { module = "io.dropwizard.metrics:metrics-jvm", 
version.ref = "dropwizardmetrics" }
 snappy-java = { module = "org.xerial.snappy:snappy-java", version.ref = 
"snappy" }
-jspecify = { module = "org.jspecify:jspecify", version = "1.0.0" }
+jspecify = "org.jspecify:jspecify:1.0.0"
 reflections = { module = "org.reflections:reflections", version.ref = 
"reflections" }
-
 # Auth / Security
-jjwt-api = { module = "io.jsonwebtoken:jjwt-api", version.ref = "jsonwebtoken" 
}
-jjwt-impl = { module = "io.jsonwebtoken:jjwt-impl", version.ref = 
"jsonwebtoken" }
-jjwt-jackson = { module = "io.jsonwebtoken:jjwt-jackson", version.ref = 
"jsonwebtoken" }
+jjwt-bom = { module = "io.jsonwebtoken:jjwt-bom", version.ref = "jsonwebtoken" 
}
+jjwt-api = { module = "io.jsonwebtoken:jjwt-api" }
+jjwt-impl = { module = "io.jsonwebtoken:jjwt-impl" }
+jjwt-jackson = { module = "io.jsonwebtoken:jjwt-jackson" }
 auth0-java-jwt = { module = "com.auth0:java-jwt", version.ref = 
"auth0-java-jwt" }
 auth0-jwks-rsa = { module = "com.auth0:jwks-rsa", version.ref = 
"auth0-jwks-rsa" }
-
 # Jakarta
 jakarta-ws-rs-api = { module = "jakarta.ws.rs:jakarta.ws.rs-api", version.ref 
= "jakarta-ws-rs" }
 jakarta-activation-api = { module = 
"jakarta.activation:jakarta.activation-api", version.ref = "jakarta-activation" 
}
@@ -444,18 +384,16 @@ jakarta-xml-bind-api = { module = 
"jakarta.xml.bind:jakarta.xml.bind-api", versi
 javax-servlet-api = { module = "javax.servlet:javax.servlet-api", version.ref 
= "javax-servlet" }
 zt-zip = { module = "org.zeroturnaround:zt-zip", version.ref = "zt-zip" }
 ipaddress = { module = "com.github.seancfoley:ipaddress", version.ref = 
"ipaddress" }
-
 # Oxia / etcd
 oxia-client = { module = "io.github.oxia-db:oxia-client", version.ref = "oxia" 
}
 oxia-testcontainers = { module = "io.github.oxia-db:oxia-testcontainers", 
version.ref = "oxia" }
-
 # Static analysis
 spotbugs-annotations = { module = "com.github.spotbugs:spotbugs-annotations", 
version.ref = "spotbugs" }
 jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" }
-
 # Test
 testng = { module = "org.testng:testng", version.ref = "testng" }
-mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
+mockito-bom = { module = "org.mockito:mockito-bom", version.ref = "mockito" }
+mockito-core = { module = "org.mockito:mockito-core" }
 awaitility = { module = "org.awaitility:awaitility", version.ref = 
"awaitility" }
 assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
 hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
@@ -475,33 +413,35 @@ spring-jdbc = { module = 
"org.springframework:spring-jdbc", version.ref = "sprin
 spring-orm = { module = "org.springframework:spring-orm", version.ref = 
"spring" }
 kubernetes-client-java = { module = "io.kubernetes:client-java", version.ref = 
"kubernetesclient" }
 kubernetes-client-java-api-fluent = { module = 
"io.kubernetes:client-java-api-fluent", version.ref = "kubernetesclient" }
-testcontainers = { module = "org.testcontainers:testcontainers", version.ref = 
"testcontainers" }
-testcontainers-elasticsearch = { module = "org.testcontainers:elasticsearch", 
version.ref = "testcontainers" }
-testcontainers-toxiproxy = { module = "org.testcontainers:toxiproxy", 
version.ref = "testcontainers" }
-testcontainers-localstack = { module = "org.testcontainers:localstack", 
version.ref = "testcontainers" }
-testcontainers-rabbitmq = { module = "org.testcontainers:rabbitmq", 
version.ref = "testcontainers" }
-testcontainers-kafka = { module = "org.testcontainers:kafka", version.ref = 
"testcontainers" }
-testcontainers-mysql = { module = "org.testcontainers:mysql", version.ref = 
"testcontainers" }
-testcontainers-postgresql = { module = "org.testcontainers:postgresql", 
version.ref = "testcontainers" }
-testcontainers-mongodb = { module = "org.testcontainers:mongodb", version.ref 
= "testcontainers" }
-testcontainers-pulsar = { module = "org.testcontainers:pulsar", version.ref = 
"testcontainers" }
-testcontainers-cassandra = { module = "org.testcontainers:cassandra", 
version.ref = "testcontainers" }
-testcontainers-k3s = { module = "org.testcontainers:k3s", version.ref = 
"testcontainers" }
+docker-java-bom = { module = "com.github.docker-java:docker-java-bom", 
version.ref = "docker-java" }
+testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", 
version.ref = "testcontainers" }
+testcontainers = { module = "org.testcontainers:testcontainers" }
+testcontainers-elasticsearch = { module = "org.testcontainers:elasticsearch" }
+testcontainers-toxiproxy = { module = "org.testcontainers:toxiproxy" }
+testcontainers-localstack = { module = "org.testcontainers:localstack" }
+testcontainers-rabbitmq = { module = "org.testcontainers:rabbitmq" }
+testcontainers-kafka = { module = "org.testcontainers:kafka" }
+testcontainers-mysql = { module = "org.testcontainers:mysql" }
+testcontainers-postgresql = { module = "org.testcontainers:postgresql" }
+testcontainers-mongodb = { module = "org.testcontainers:mongodb" }
+testcontainers-pulsar = { module = "org.testcontainers:pulsar" }
+testcontainers-cassandra = { module = "org.testcontainers:cassandra" }
+testcontainers-k3s = { module = "org.testcontainers:k3s" }
 kerby-simplekdc = { module = "org.apache.kerby:kerb-simplekdc", version.ref = 
"kerby" }
 json = { module = "org.json:json", version.ref = "json" }
-
 # AWS SDKs
-aws-java-sdk-core = { module = "com.amazonaws:aws-java-sdk-core", version.ref 
= "aws-sdk" }
-aws-java-sdk-sts = { module = "com.amazonaws:aws-java-sdk-sts", version.ref = 
"aws-sdk" }
-aws-sdk2-regions = { module = "software.amazon.awssdk:regions", version.ref = 
"aws-sdk2" }
-aws-sdk2-sts = { module = "software.amazon.awssdk:sts", version.ref = 
"aws-sdk2" }
-aws-sdk2-kinesis = { module = "software.amazon.awssdk:kinesis", version.ref = 
"aws-sdk2" }
-aws-sdk2-utils = { module = "software.amazon.awssdk:utils", version.ref = 
"aws-sdk2" }
-dynamodb-streams-kinesis-adapter = { module = 
"com.amazonaws:dynamodb-streams-kinesis-adapter", version = "1.6.0" }
-amazon-kinesis-client = { module = 
"software.amazon.kinesis:amazon-kinesis-client", version = "2.6.0" }
-amazon-kinesis-client-v3 = { module = 
"software.amazon.kinesis:amazon-kinesis-client", version = "3.1.2" }
-amazon-kinesis-producer = { module = 
"software.amazon.kinesis:amazon-kinesis-producer", version = "1.0.4" }
-
+aws-java-sdk-bom = { module = "com.amazonaws:aws-java-sdk-bom", version.ref = 
"aws-sdk" }
+aws-java-sdk-core = { module = "com.amazonaws:aws-java-sdk-core" }
+aws-java-sdk-sts = { module = "com.amazonaws:aws-java-sdk-sts" }
+aws-sdk2-bom = { module = "software.amazon.awssdk:bom", version.ref = 
"aws-sdk2" }
+aws-sdk2-regions = { module = "software.amazon.awssdk:regions" }
+aws-sdk2-sts = { module = "software.amazon.awssdk:sts" }
+aws-sdk2-kinesis = { module = "software.amazon.awssdk:kinesis" }
+aws-sdk2-utils = { module = "software.amazon.awssdk:utils" }
+dynamodb-streams-kinesis-adapter = 
"com.amazonaws:dynamodb-streams-kinesis-adapter:1.6.0"
+amazon-kinesis-client = "software.amazon.kinesis:amazon-kinesis-client:2.6.0"
+amazon-kinesis-client-v3 = 
"software.amazon.kinesis:amazon-kinesis-client:3.1.2"
+amazon-kinesis-producer = 
"software.amazon.kinesis:amazon-kinesis-producer:1.0.4"
 # Kafka / Confluent
 kafka-clients = { module = "org.apache.kafka:kafka-clients", version.ref = 
"kafka-client" }
 kafka-connect-runtime = { module = "org.apache.kafka:connect-runtime", 
version.ref = "kafka-client" }
@@ -512,104 +452,95 @@ kafka-connect-file = { module = 
"org.apache.kafka:connect-file", version.ref = "
 kafka-schema-registry-client = { module = 
"io.confluent:kafka-schema-registry-client", version.ref = "confluent" }
 kafka-avro-serializer = { module = "io.confluent:kafka-avro-serializer", 
version.ref = "confluent" }
 kafka-connect-avro-converter = { module = 
"io.confluent:kafka-connect-avro-converter", version.ref = "confluent" }
-
 # ElasticSearch / OpenSearch
 opensearch-rest-high-level-client = { module = 
"org.opensearch.client:opensearch-rest-high-level-client", version.ref = 
"opensearch" }
 elasticsearch-java = { module = "co.elastic.clients:elasticsearch-java", 
version.ref = "elasticsearch-java" }
-
 # Debezium
-debezium-core = { module = "io.debezium:debezium-core", version.ref = 
"debezium" }
-debezium-connector-mysql = { module = "io.debezium:debezium-connector-mysql", 
version.ref = "debezium" }
-debezium-connector-mongodb = { module = 
"io.debezium:debezium-connector-mongodb", version.ref = "debezium" }
-debezium-connector-postgres = { module = 
"io.debezium:debezium-connector-postgres", version.ref = "debezium" }
-debezium-connector-oracle = { module = 
"io.debezium:debezium-connector-oracle", version.ref = "debezium" }
-debezium-connector-sqlserver = { module = 
"io.debezium:debezium-connector-sqlserver", version.ref = "debezium" }
-
+debezium-bom = { module = "io.debezium:debezium-bom", version.ref = "debezium" 
}
+debezium-core = { module = "io.debezium:debezium-core" }
+debezium-connector-mysql = { module = "io.debezium:debezium-connector-mysql" }
+debezium-connector-mongodb = { module = 
"io.debezium:debezium-connector-mongodb" }
+debezium-connector-postgres = { module = 
"io.debezium:debezium-connector-postgres" }
+debezium-connector-oracle = { module = "io.debezium:debezium-connector-oracle" 
}
+debezium-connector-sqlserver = { module = 
"io.debezium:debezium-connector-sqlserver" }
 # Database drivers
-postgresql-jdbc = { module = "org.postgresql:postgresql", version = "42.7.10" }
-sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version = "3.47.1.0" }
-clickhouse-jdbc = { module = "com.clickhouse:clickhouse-jdbc", version = 
"0.4.6" }
-mariadb-jdbc = { module = "org.mariadb.jdbc:mariadb-java-client", version = 
"3.5.5" }
-openmldb-jdbc = { module = "com.4paradigm.openmldb:openmldb-jdbc", version = 
"0.4.4-hotfix1" }
-openmldb-native = { module = "com.4paradigm.openmldb:openmldb-native", version 
= "0.4.4-hotfix1" }
-
+postgresql-jdbc = "org.postgresql:postgresql:42.7.10"
+sqlite-jdbc = "org.xerial:sqlite-jdbc:3.47.1.0"
+clickhouse-jdbc = "com.clickhouse:clickhouse-jdbc:0.4.6"
+mariadb-jdbc = "org.mariadb.jdbc:mariadb-java-client:3.5.5"
+mysql-connector-j = "com.mysql:mysql-connector-j:9.4.0"
+openmldb-jdbc = { module = "com.4paradigm.openmldb:openmldb-jdbc", version.ref 
= "openmldb" }
+openmldb-native = { module = "com.4paradigm.openmldb:openmldb-native", 
version.ref = "openmldb" }
 # JClouds
 jclouds-allblobstore = { module = "org.apache.jclouds:jclouds-allblobstore", 
version.ref = "jclouds" }
 jclouds-blobstore = { module = "org.apache.jclouds:jclouds-blobstore", 
version.ref = "jclouds" }
-
 # Hadoop
 hadoop-common = { module = "org.apache.hadoop:hadoop-common", version.ref = 
"hadoop3" }
 hadoop-hdfs-client = { module = "org.apache.hadoop:hadoop-hdfs-client", 
version.ref = "hadoop3" }
 hadoop-client = { module = "org.apache.hadoop:hadoop-client", version.ref = 
"hadoop3" }
 hadoop-minicluster = { module = "org.apache.hadoop:hadoop-minicluster", 
version.ref = "hadoop3" }
-
 # HBase
 hbase-client = { module = "org.apache.hbase:hbase-client", version.ref = 
"hbase" }
 hbase-common = { module = "org.apache.hbase:hbase-common", version.ref = 
"hbase" }
-
 # NoSQL / Search
 aerospike-client = { module = "com.aerospike:aerospike-client-bc", version.ref 
= "aerospike-client" }
 cassandra-driver = { module = "com.datastax.cassandra:cassandra-driver-core", 
version.ref = "cassandra-driver" }
 failsafe = { module = "dev.failsafe:failsafe", version.ref = "failsafe" }
-docker-java-core = { module = "com.github.docker-java:docker-java-core", 
version = "3.4.1" }
 mongodb-driver-reactivestreams = { module = 
"org.mongodb:mongodb-driver-reactivestreams", version.ref = "mongodb-driver" }
 mongodb-driver-sync = { module = "org.mongodb:mongodb-driver-sync", 
version.ref = "mongodb-driver" }
 lettuce-core = { module = "io.lettuce:lettuce-core", version.ref = "lettuce" }
 solr-solrj = { module = "org.apache.solr:solr-solrj", version.ref = "solr" }
 solr-test-framework = { module = "org.apache.solr:solr-test-framework", 
version.ref = "solr" }
 solr-core = { module = "org.apache.solr:solr-core", version.ref = "solr" }
-
 # Messaging
 rabbitmq-amqp-client = { module = "com.rabbitmq:amqp-client", version.ref = 
"rabbitmq-client" }
 nsq-j = { module = "com.sproutsocial:nsq-j", version.ref = "nsq-client" }
-
 # Time series
 influxdb-client-java = { module = "com.influxdb:influxdb-client-java", 
version.ref = "influxdb-client" }
 influxdb-java = { module = "org.influxdb:influxdb-java", version.ref = 
"influxdb-java" }
-
 # IO specific
-jackson-dataformat-cbor = { module = 
"com.fasterxml.jackson.dataformat:jackson-dataformat-cbor", version.ref = 
"jackson" }
-json-smart = { module = "net.minidev:json-smart", version = "2.5.2" }
-json-flattener = { module = "com.github.wnameless.json:json-flattener", 
version = "0.16.4" }
-flatbuffers-java = { module = "com.google.flatbuffers:flatbuffers-java", 
version = "1.9.0" }
-jfairy = { module = "io.codearte.jfairy:jfairy", version = "0.5.9" }
-embedded-redis = { module = "com.github.kstyrc:embedded-redis", version = 
"0.6" }
-
+json-smart = "net.minidev:json-smart:2.5.2"
+json-flattener = "com.github.wnameless.json:json-flattener:0.16.4"
+flatbuffers-java = "com.google.flatbuffers:flatbuffers-java:1.9.0"
+jfairy = "io.codearte.jfairy:jfairy:0.5.9"
+embedded-redis = "com.github.kstyrc:embedded-redis:0.6"
 # Auth
 athenz-zts-java-client = { module = "com.yahoo.athenz:athenz-zts-java-client", 
version.ref = "athenz" }
 athenz-cert-refresher = { module = "com.yahoo.athenz:athenz-cert-refresher", 
version.ref = "athenz" }
 athenz-auth-core = { module = "com.yahoo.athenz:athenz-auth-core", version.ref 
= "athenz" }
 athenz-zpe-java-client = { module = "com.yahoo.athenz:athenz-zpe-java-client", 
version.ref = "athenz" }
-
 # Misc
 bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = 
"bouncycastle-bcprov" }
 commons-logging = { module = "commons-logging:commons-logging", version.ref = 
"commons-logging" }
 commons-beanutils = { module = "commons-beanutils:commons-beanutils", 
version.ref = "commons-beanutils" }
 commons-configuration2 = { module = 
"org.apache.commons:commons-configuration2", version.ref = 
"commons-configuration2" }
 bookkeeper-stats-api = { module = 
"org.apache.bookkeeper.stats:bookkeeper-stats-api", version.ref = "bookkeeper" }
-datasketches-memory = { module = 
"org.apache.datasketches:datasketches-memory", version = "2.2.0" }
-datasketches-java = { module = "org.apache.datasketches:datasketches-java", 
version = "6.1.1" }
-
+datasketches-memory = "org.apache.datasketches:datasketches-memory:2.2.0"
+datasketches-java = "org.apache.datasketches:datasketches-java:6.1.1"
 # Pulsar core modules (published Maven artifacts, used by connectors)
-pulsar-io-core = { module = "org.apache.pulsar:pulsar-io-core", version.ref = 
"pulsar" }
-pulsar-io-common = { module = "org.apache.pulsar:pulsar-io-common", 
version.ref = "pulsar" }
-pulsar-common = { module = "org.apache.pulsar:pulsar-common", version.ref = 
"pulsar" }
-pulsar-client-api = { module = "org.apache.pulsar:pulsar-client-api", 
version.ref = "pulsar" }
-pulsar-client = { module = "org.apache.pulsar:pulsar-client-original", 
version.ref = "pulsar" }
-pulsar-client-admin = { module = 
"org.apache.pulsar:pulsar-client-admin-original", version.ref = "pulsar" }
-pulsar-config-validation = { module = 
"org.apache.pulsar:pulsar-config-validation", version.ref = "pulsar" }
-pulsar-functions-api = { module = "org.apache.pulsar:pulsar-functions-api", 
version.ref = "pulsar" }
-pulsar-functions-instance = { module = 
"org.apache.pulsar:pulsar-functions-instance", version.ref = "pulsar" }
-pulsar-functions-utils = { module = 
"org.apache.pulsar:pulsar-functions-utils", version.ref = "pulsar" }
-pulsar-broker = { module = "org.apache.pulsar:pulsar-broker", version.ref = 
"pulsar" }
-pulsar-broker-test = { module = "org.apache.pulsar:pulsar-broker", version.ref 
= "pulsar" }
-pulsar-testmocks = { module = "org.apache.pulsar:testmocks", version.ref = 
"pulsar" }
-pulsar-buildtools = { module = "org.apache.pulsar:buildtools", version.ref = 
"pulsar" }
+pulsar-bom = { module = "org.apache.pulsar:pulsar-bom", version.ref = "pulsar" 
}
+pulsar-io-core = { module = "org.apache.pulsar:pulsar-io-core" }
+pulsar-io-common = { module = "org.apache.pulsar:pulsar-io-common" }
+pulsar-common = { module = "org.apache.pulsar:pulsar-common" }
+pulsar-client-api = { module = "org.apache.pulsar:pulsar-client-api" }
+pulsar-client = { module = "org.apache.pulsar:pulsar-client-original" }
+pulsar-client-admin = { module = 
"org.apache.pulsar:pulsar-client-admin-original" }
+pulsar-config-validation = { module = 
"org.apache.pulsar:pulsar-config-validation" }
+pulsar-functions-api = { module = "org.apache.pulsar:pulsar-functions-api" }
+pulsar-functions-instance = { module = 
"org.apache.pulsar:pulsar-functions-instance" }
+pulsar-functions-utils = { module = "org.apache.pulsar:pulsar-functions-utils" 
}
+pulsar-broker = { module = "org.apache.pulsar:pulsar-broker" }
+pulsar-testmocks = { module = "org.apache.pulsar:testmocks" }
+pulsar-buildtools = { module = "org.apache.pulsar:buildtools" }
 
 [plugins]
 lightproto = { id = "io.streamnative.lightproto", version.ref = "lightproto" }
-nar = { id = "io.github.merlimat.nar", version = "0.1.3" }
-protobuf = { id = "com.google.protobuf", version = "0.9.6" }
+nar = "io.github.merlimat.nar:0.1.3"
+protobuf = "com.google.protobuf:0.9.6"
 shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
-rat = { id = "org.nosphere.apache.rat", version = "0.8.1" }
-license = { id = "com.github.hierynomus.license", version = "0.16.1" }
+rat = "org.nosphere.apache.rat:0.8.1"
+spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
+idea-ext = { id = "org.jetbrains.gradle.plugin.idea-ext", version.ref = 
"idea-ext" }
+version-catalog-update = "nl.littlerobots.version-catalog-update:1.1.0"
+versions = "com.github.ben-manes.versions:0.53.0"
+crlf = "com.github.vlsi.crlf:3.0.1"
diff --git a/hbase/build.gradle.kts b/hbase/build.gradle.kts
index caa37ad..be1f1e5 100644
--- a/hbase/build.gradle.kts
+++ b/hbase/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/hdfs3/build.gradle.kts b/hdfs3/build.gradle.kts
index 1f20430..82e2254 100644
--- a/hdfs3/build.gradle.kts
+++ b/hdfs3/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/http/build.gradle.kts b/http/build.gradle.kts
index f4008b9..9b101af 100644
--- a/http/build.gradle.kts
+++ b/http/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/influxdb/build.gradle.kts b/influxdb/build.gradle.kts
index a29d19b..d3a589e 100644
--- a/influxdb/build.gradle.kts
+++ b/influxdb/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.common)
diff --git a/jdbc/clickhouse/build.gradle.kts b/jdbc/clickhouse/build.gradle.kts
index da66bc4..7a36945 100644
--- a/jdbc/clickhouse/build.gradle.kts
+++ b/jdbc/clickhouse/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(project(":jdbc:pulsar-io-jdbc-core"))
diff --git a/jdbc/core/build.gradle.kts b/jdbc/core/build.gradle.kts
index 7d38cdf..5d40fba 100644
--- a/jdbc/core/build.gradle.kts
+++ b/jdbc/core/build.gradle.kts
@@ -17,6 +17,10 @@
  * under the License.
  */
 
+plugins {
+    id("pulsar-connectors.java-conventions")
+}
+
 dependencies {
     api(libs.pulsar.io.common)
     api(libs.pulsar.io.core)
diff --git a/jdbc/mariadb/build.gradle.kts b/jdbc/mariadb/build.gradle.kts
index 1691bff..ce5fe96 100644
--- a/jdbc/mariadb/build.gradle.kts
+++ b/jdbc/mariadb/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(project(":jdbc:pulsar-io-jdbc-core"))
diff --git a/jdbc/openmldb/build.gradle.kts b/jdbc/openmldb/build.gradle.kts
index a9894a8..7eae514 100644
--- a/jdbc/openmldb/build.gradle.kts
+++ b/jdbc/openmldb/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(project(":jdbc:pulsar-io-jdbc-core"))
diff --git a/jdbc/postgres/build.gradle.kts b/jdbc/postgres/build.gradle.kts
index 752366f..3aa3da1 100644
--- a/jdbc/postgres/build.gradle.kts
+++ b/jdbc/postgres/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(project(":jdbc:pulsar-io-jdbc-core"))
diff --git a/jdbc/sqlite/build.gradle.kts b/jdbc/sqlite/build.gradle.kts
index 4e1495a..66c92cf 100644
--- a/jdbc/sqlite/build.gradle.kts
+++ b/jdbc/sqlite/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(project(":jdbc:pulsar-io-jdbc-core"))
diff --git a/kafka-connect-adaptor-nar/build.gradle.kts 
b/kafka-connect-adaptor-nar/build.gradle.kts
index 4ee233d..05ebf63 100644
--- a/kafka-connect-adaptor-nar/build.gradle.kts
+++ b/kafka-connect-adaptor-nar/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 nar {
     narId.set("pulsar-io-kafka-connect-adaptor")
diff --git a/kafka-connect-adaptor/build.gradle.kts 
b/kafka-connect-adaptor/build.gradle.kts
index 4809b82..1349adb 100644
--- a/kafka-connect-adaptor/build.gradle.kts
+++ b/kafka-connect-adaptor/build.gradle.kts
@@ -17,6 +17,9 @@
  * under the License.
  */
 
+plugins {
+    id("pulsar-connectors.java-conventions")
+}
 
 dependencies {
     compileOnly(libs.pulsar.io.core)
diff --git a/kafka/build.gradle.kts b/kafka/build.gradle.kts
index 8046aa6..6d219d0 100644
--- a/kafka/build.gradle.kts
+++ b/kafka/build.gradle.kts
@@ -18,16 +18,14 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 
-
 // KafkaBytesSource uses SchemaInfoImpl from pulsar-common, which is excluded 
from
-// NAR runtimeClasspath by the global exclusion. Bundle it via a separate 
configuration
-// since the NAR classloader's parent (rootClassLoader) only has 
java-instance.jar.
-val narExtraDeps by configurations.creating {
-    isCanBeResolved = true
-    isCanBeConsumed = false
+// NAR runtimeClasspath by default. Include it in the NAR bundle.
+pulsarConnectorsNar {
+    includePulsarModule("pulsar-common")
 }
 
 dependencies {
@@ -35,7 +33,6 @@ dependencies {
     implementation(libs.pulsar.io.core)
     implementation(libs.pulsar.common)
     implementation(libs.pulsar.client)
-    narExtraDeps(libs.pulsar.common)
     implementation(libs.jackson.databind)
     implementation(libs.jackson.dataformat.yaml)
     implementation(libs.guava)
@@ -52,9 +49,3 @@ dependencies {
     testImplementation(libs.awaitility)
     testImplementation(libs.bcpkix.jdk18on)
 }
-
-tasks.named<io.github.merlimat.gradle.nar.NarTask>("nar") {
-    from(narExtraDeps) { into("META-INF/bundled-dependencies") }
-    bundledDependencies.from(narExtraDeps)
-    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
-}
diff --git a/kinesis-kpl-shaded/build.gradle.kts 
b/kinesis-kpl-shaded/build.gradle.kts
index 4197e5d..bf6ecce 100644
--- a/kinesis-kpl-shaded/build.gradle.kts
+++ b/kinesis-kpl-shaded/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.shadow)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.shadow-conventions")
 }
 
 dependencies {
@@ -34,16 +35,7 @@ configurations.all {
     }
 }
 
-// Disable the default jar task so the shadow JAR is the only artifact.
-// This avoids Gradle's implicit dependency validation errors when consumers
-// use project() to depend on this module.
-tasks.jar {
-    enabled = false
-}
-
 tasks.shadowJar {
-    archiveClassifier.set("")
-    mergeServiceFiles()
     dependencies {
         include(dependency("software.amazon.kinesis:amazon-kinesis-producer"))
         include(dependency("com.google.protobuf:protobuf-java"))
diff --git a/kinesis/build.gradle.kts b/kinesis/build.gradle.kts
index 53f4995..2a80133 100644
--- a/kinesis/build.gradle.kts
+++ b/kinesis/build.gradle.kts
@@ -18,13 +18,14 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 
 dependencies {
     // The shaded KPL project bundles amazon-kinesis-producer with relocated 
protobuf.
-    // Use shadowElements to get the shadow JAR (which has relocated protobuf).
-    implementation(project(path = ":kinesis-kpl-shaded", configuration = 
"shadowElements"))
+    // The shadow convention exposes the shadow JAR as the primary artifact.
+    implementation(project(":kinesis-kpl-shaded"))
     // CompileOnly: needed for compilation against KPL classes but NOT bundled 
in NAR.
     // At runtime, KPL classes come from the shaded project's shadow JAR.
     compileOnly(libs.amazon.kinesis.producer)
diff --git a/mongo/build.gradle.kts b/mongo/build.gradle.kts
index b760c25..d06d6c8 100644
--- a/mongo/build.gradle.kts
+++ b/mongo/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.common)
diff --git a/netty/build.gradle.kts b/netty/build.gradle.kts
index be70778..43e3e5a 100644
--- a/netty/build.gradle.kts
+++ b/netty/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/nsq/build.gradle.kts b/nsq/build.gradle.kts
index 732c22c..be75a00 100644
--- a/nsq/build.gradle.kts
+++ b/nsq/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.core)
diff --git a/pulsar-dependencies/build.gradle.kts 
b/pulsar-connectors-dependencies/build.gradle.kts
similarity index 55%
rename from pulsar-dependencies/build.gradle.kts
rename to pulsar-connectors-dependencies/build.gradle.kts
index 7bead99..0be83b0 100644
--- a/pulsar-dependencies/build.gradle.kts
+++ b/pulsar-connectors-dependencies/build.gradle.kts
@@ -19,7 +19,7 @@
 
 // Enforced platform module that declares version constraints for all 
dependencies.
 // This is the Gradle equivalent of Maven's dependencyManagement section.
-// All subprojects consume this via: 
implementation(enforcedPlatform(project(":pulsar-dependencies")))
+// All subprojects consume this via: 
implementation(enforcedPlatform(project(":pulsar-connectors-dependencies")))
 plugins {
     `java-platform`
 }
@@ -30,14 +30,20 @@ javaPlatform {
 }
 
 dependencies {
-    constraints {
-        // Iterate over all library declarations in the version catalog and 
add them as constraints.
-        // This ensures that any transitive dependency matching a catalog 
entry gets pinned to
-        // the version we specify, regardless of what version a transitive 
dependency requests.
-        val catalog = 
project.extensions.getByType<VersionCatalogsExtension>().named("libs")
-        catalog.libraryAliases.forEach { alias ->
-            catalog.findLibrary(alias).ifPresent { provider ->
-                api(provider)
+    val catalog = 
project.extensions.getByType<VersionCatalogsExtension>().named("libs")
+    // Iterate over all library declarations in the version catalog and add 
them as constraints.
+    // This ensures that any transitive dependency matching a catalog entry 
gets pinned to
+    // the version we specify, regardless of what version a transitive 
dependency requests.
+    // BOM entries (detected by alias name) are imported as platforms rather 
than constraints.
+    catalog.libraryAliases.forEach { alias ->
+        catalog.findLibrary(alias).ifPresent { provider ->
+            val dep = provider.get()
+            if (alias.endsWith(".bom")) {
+                api(platform(provider))
+            } else if (dep.versionConstraint.requiredVersion.isNotEmpty()) {
+                // Only add constraints for entries with explicit versions.
+                // Versionless entries are managed by BOMs imported above.
+                constraints.api(provider)
             }
         }
     }
diff --git a/rabbitmq/build.gradle.kts b/rabbitmq/build.gradle.kts
index d7613ce..b15ee74 100644
--- a/rabbitmq/build.gradle.kts
+++ b/rabbitmq/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.common)
diff --git a/redis/build.gradle.kts b/redis/build.gradle.kts
index eb57bee..bfd82a9 100644
--- a/redis/build.gradle.kts
+++ b/redis/build.gradle.kts
@@ -18,7 +18,8 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 dependencies {
     implementation(libs.pulsar.io.common)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9194ce0..d186f01 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -22,6 +22,7 @@ pluginManagement {
         gradlePluginPortal()
         mavenCentral()
     }
+    includeBuild("build-logic")
 }
 
 dependencyResolutionManagement {
@@ -49,8 +50,16 @@ dependencyResolutionManagement {
 
 rootProject.name = "pulsar-connectors"
 
+// This build requires Java 17 or later. Version check can be skipped with 
-PskipJavaVersionCheck parameter.
+val javaVersion = providers.provider { JavaVersion.current() }
+require(providers.gradleProperty("skipJavaVersionCheck").isPresent
+        || javaVersion.get() >= JavaVersion.VERSION_17) {
+    "This build requires Java 17 or later, but is running on Java 
${javaVersion.get()}. " +
+    "Pass -PskipJavaVersionCheck to skip this check."
+}
+
 // Enforced platform for dependency version management
-include("pulsar-dependencies")
+include("pulsar-connectors-dependencies")
 
 // Simple connectors (flat layout, top-level directories)
 include("aerospike")
diff --git a/solr/build.gradle.kts b/solr/build.gradle.kts
index d15cedc..d338d67 100644
--- a/solr/build.gradle.kts
+++ b/solr/build.gradle.kts
@@ -18,10 +18,11 @@
  */
 
 plugins {
-    alias(libs.plugins.nar)
+    id("pulsar-connectors.java-conventions")
+    id("pulsar-connectors.nar-conventions")
 }
 // Solr 9.x embeds Jetty 10.x, which is incompatible with Pulsar's Jetty 12.
-// The pulsar-dependencies platform enforces Jetty 12 strict versions, which 
override
+// The pulsar-connectors-dependencies platform enforces Jetty 12 strict 
versions, which override
 // enforcedPlatform("jetty-bom:10.0.24") because Gradle picks the highest 
strict version.
 // Use resolutionStrategy.force to downgrade Jetty to 10.0.24 for test 
configurations.
 val jetty10Version = "10.0.24"
diff --git a/src/license-header.txt b/src/license-header.txt
new file mode 100644
index 0000000..60b675e
--- /dev/null
+++ b/src/license-header.txt
@@ -0,0 +1,16 @@
+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.

Reply via email to