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

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-full-text.git


The following commit(s) were added to refs/heads/main by this push:
     new 8686ecd  Add release infrastructure for Rust/Java/Python (#9)
8686ecd is described below

commit 8686ecd61f6df0bb7e4bc9d428dc2d721824f4dc
Author: jianguotian <[email protected]>
AuthorDate: Mon Jul 6 20:42:45 2026 +0800

    Add release infrastructure for Rust/Java/Python (#9)
    
    Align the release scripts, CI, and process with paimon-vector-index and
    paimon-mosaic. Adds the Rust/Java/Python release workflows and ASF release
    scaffolding, and fixes gaps that only surface at release time:
    
    - Add release.yml + release-rust/java/python/python-publish.yml.
    - Add tools: create_release_branch.sh, create_source_release.sh,
      dependencies.py, validate_asf_yaml.py (lenient, from vector-index).
    - Fix update_branch_version.sh to strip -SNAPSHOT when matching Cargo.toml /
      pyproject.toml versions (from mosaic #57), avoiding silent version drift.
    - Add description to core/ffi/jni crates so `cargo publish` succeeds on the
      final tag (crates.io rejects missing description; --dry-run does not 
catch it).
    - Add the release profile (gpg, source, javadoc, enforcer, nexus-staging) 
and
      the ASF license header to java/pom.xml.
    - Add LICENSE (Apache-2.0) and NOTICE.
    - Add deny.toml + `cargo deny check licenses` and `.asf.yaml` validation to 
CI;
      allow Zlib (ASF Category A) for tantivy-jieba's compression stack.
---
 .github/workflows/ci.yml                           |  16 ++
 .github/workflows/release-java.yml                 | 183 +++++++++++++++++++
 .github/workflows/release-python-publish.yml       | 130 +++++++++++++
 .github/workflows/release-python.yml               | 183 +++++++++++++++++++
 .github/workflows/release-rust.yml                 |  55 ++++++
 .github/workflows/release.yml                      |  59 ++++++
 DEPENDENCIES.rust.tsv                              | 185 +++++++++++++++++++
 LICENSE                                            | 201 +++++++++++++++++++++
 NOTICE                                             |   5 +
 core/Cargo.toml                                    |   1 +
 deny.toml                                          |  31 ++++
 ffi/Cargo.toml                                     |   1 +
 java/pom.xml                                       | 132 ++++++++++++++
 .../paimon/index/fulltext/FullTextNative.java      |  84 ++++++++-
 .../fulltext/FullTextNativeRoundTripTest.java      |  56 +++++-
 jni/Cargo.toml                                     |   1 +
 python/pyproject.toml                              |  14 +-
 python/setup.py                                    | 104 ++++++++++-
 ..._branch_version.sh => create_release_branch.sh} |  36 ++--
 tools/create_source_release.sh                     |  90 +++++++++
 tools/dependencies.py                              | 124 +++++++++++++
 tools/update_branch_version.sh                     |   8 +-
 tools/validate_asf_yaml.py                         | 103 +++++++++++
 23 files changed, 1762 insertions(+), 40 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 979e43c..fa12d21 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -51,6 +51,22 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-cargo-
 
+      - name: Validate .asf.yaml
+        run: pip install pyyaml -q && python3 tools/validate_asf_yaml.py
+
+      - name: Install cargo-deny
+        uses: taiki-e/install-action@v2
+        with:
+          tool: [email protected]
+
+      - name: Check dependency licenses (Apache-compatible)
+        run: cargo deny check licenses
+
+      - name: Verify Rust dependency metadata
+        run: |
+          cargo deny list -f tsv -t 0.6 > /tmp/DEPENDENCIES.rust.tsv
+          diff -u DEPENDENCIES.rust.tsv /tmp/DEPENDENCIES.rust.tsv
+
       - name: Format
         run: cargo fmt --all -- --check
 
diff --git a/.github/workflows/release-java.yml 
b/.github/workflows/release-java.yml
new file mode 100644
index 0000000..2697214
--- /dev/null
+++ b/.github/workflows/release-java.yml
@@ -0,0 +1,183 @@
+################################################################################
+#  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: Release Java
+
+on:
+  workflow_call:
+  workflow_dispatch:
+
+env:
+  JDK_VERSION: 8
+
+concurrency:
+  group: release-java-${{ github.ref }}
+  cancel-in-progress: false
+
+jobs:
+  build-native:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - os: ubuntu-latest
+            target: x86_64-unknown-linux-gnu
+            os_name: linux
+            arch: x86_64
+            lib_name: libpaimon_ftindex_jni.so
+          - os: ubuntu-24.04-arm
+            target: aarch64-unknown-linux-gnu
+            os_name: linux
+            arch: aarch64
+            lib_name: libpaimon_ftindex_jni.so
+          - os: macos-latest
+            target: aarch64-apple-darwin
+            os_name: macos
+            arch: aarch64
+            lib_name: libpaimon_ftindex_jni.dylib
+          - os: windows-latest
+            target: x86_64-pc-windows-msvc
+            os_name: windows
+            arch: x86_64
+            lib_name: paimon_ftindex_jni.dll
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Setup Rust toolchain
+        run: |
+          rustup update stable
+          rustup default stable
+
+      - name: Setup Python
+        if: matrix.os_name == 'linux'
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+
+      - name: Setup Linux zigbuild
+        if: matrix.os_name == 'linux'
+        run: pip install cargo-zigbuild
+
+      - name: Build Linux JNI library
+        if: matrix.os_name == 'linux'
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          rustup target add "${{ matrix.target }}"
+          unset ZSTD_SYS_USE_PKG_CONFIG
+
+          cargo zigbuild --release -p paimon-ftindex-jni --target "${{ 
matrix.target }}.2.17"
+
+          artifact="target/${{ matrix.target }}/release/${{ matrix.lib_name }}"
+          test -f "$artifact"
+
+          version_info="$(readelf --version-info "$artifact")"
+          max_glibc="$(
+            printf '%s\n' "$version_info" \
+              | grep -Eo 'GLIBC_[0-9][0-9.]*' \
+              | sort -Vu \
+              | tail -n1 \
+              || true
+          )"
+          echo "Maximum required GLIBC version: ${max_glibc:-none}"
+
+          if [[ -n "$max_glibc" ]]; then
+            allowed_glibc="GLIBC_2.17"
+            highest_glibc="$(printf '%s\n%s\n' "$allowed_glibc" "$max_glibc" | 
sort -Vu | tail -n1)"
+            if [[ "$highest_glibc" != "$allowed_glibc" ]]; then
+              echo "Linux GNU artifact requires $max_glibc, expected <= 
$allowed_glibc" >&2
+              exit 1
+            fi
+          fi
+
+      - name: Build JNI library
+        if: matrix.os_name != 'linux'
+        run: |
+          rustup target add "${{ matrix.target }}"
+          cargo build --release -p paimon-ftindex-jni --target "${{ 
matrix.target }}"
+
+      - name: Upload native library
+        uses: actions/upload-artifact@v5
+        with:
+          name: native-${{ matrix.os_name }}-${{ matrix.arch }}
+          path: target/${{ matrix.target }}/release/${{ matrix.lib_name }}
+
+  deploy-staging:
+    if: github.repository == 'apache/paimon-full-text' && 
startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, '-rc')
+    runs-on: ubuntu-latest
+    needs: [build-native]
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Download linux x86_64 native library
+        uses: actions/download-artifact@v5
+        with:
+          name: native-linux-x86_64
+          path: java/src/main/resources/native/linux/x86_64
+
+      - name: Download linux aarch64 native library
+        uses: actions/download-artifact@v5
+        with:
+          name: native-linux-aarch64
+          path: java/src/main/resources/native/linux/aarch64
+
+      - name: Download macOS aarch64 native library
+        uses: actions/download-artifact@v5
+        with:
+          name: native-macos-aarch64
+          path: java/src/main/resources/native/macos/aarch64
+
+      - name: Download windows x86_64 native library
+        uses: actions/download-artifact@v5
+        with:
+          name: native-windows-x86_64
+          path: java/src/main/resources/native/windows/x86_64
+
+      - name: Verify native libraries
+        run: find java/src/main/resources/native -type f | sort
+
+      - name: Set up JDK ${{ env.JDK_VERSION }}
+        uses: actions/setup-java@v4
+        with:
+          java-version: ${{ env.JDK_VERSION }}
+          distribution: 'temurin'
+          server-id: apache.releases.https
+          server-username: MAVEN_USERNAME
+          server-password: MAVEN_PASSWORD
+          gpg-private-key: ${{ secrets.GPG_SECRET_KEY }}
+          gpg-passphrase: MAVEN_GPG_PASSPHRASE
+
+      - name: Show Maven version
+        run: mvn --version
+
+      - name: Deploy to Apache Nexus staging
+        working-directory: java
+        run: |
+          REF="${{ github.ref_name }}"
+          VERSION="${REF#v}"
+          DESC="Apache Paimon Full Text, version ${VERSION%-rc*}, release 
candidate ${VERSION#*-rc}"
+          mvn clean deploy \
+            -Prelease \
+            -DskipTests \
+            -DstagingDescription="$DESC"
+        env:
+          MAVEN_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }}
+          MAVEN_PASSWORD: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }}
+          MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
diff --git a/.github/workflows/release-python-publish.yml 
b/.github/workflows/release-python-publish.yml
new file mode 100644
index 0000000..948b92e
--- /dev/null
+++ b/.github/workflows/release-python-publish.yml
@@ -0,0 +1,130 @@
+# 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.
+
+# Publish Python wheels after the orchestrator has confirmed the wheel
+# build completed successfully.
+
+name: Release Python Publish
+
+on:
+  workflow_call:
+
+concurrency:
+  group: release-python-publish-${{ github.ref }}
+  cancel-in-progress: false
+
+permissions:
+  actions: read
+  contents: read
+
+jobs:
+  publish:
+    name: Publish to PyPI
+    if: github.repository == 'apache/paimon-full-text' && 
startsWith(github.ref, 'refs/tags/')
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/download-artifact@v5
+        with:
+          pattern: wheels-*
+          merge-multiple: true
+          path: dist
+
+      - name: Verify wheel versions
+        env:
+          TAG_NAME: ${{ github.ref_name }}
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          expected_version="${TAG_NAME#v}"
+          expected_version="${expected_version/-rc/rc}"
+          shopt -s nullglob
+          wheels=(dist/*.whl)
+
+          if [[ "${#wheels[@]}" -eq 0 ]]; then
+            echo "No wheels found in dist" >&2
+            exit 1
+          fi
+
+          for wheel in "${wheels[@]}"; do
+            base="$(basename "$wheel")"
+            case "$base" in
+              paimon_ftindex-"${expected_version}"-*.whl) ;;
+              *)
+                echo "Unexpected wheel for ${TAG_NAME}: ${base}" >&2
+                exit 1
+                ;;
+            esac
+          done
+
+      - name: Verify wheel native libraries
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          python - <<'PY'
+          from pathlib import Path
+          from zipfile import ZipFile
+
+          expected_suffixes = (".so", ".dylib", ".dll")
+          wheels = sorted(Path("dist").glob("*.whl"))
+          if not wheels:
+              raise SystemExit("No wheels found in dist")
+
+          for wheel in wheels:
+              with ZipFile(wheel) as zf:
+                  names = set(zf.namelist())
+              libs = [
+                  name
+                  for name in names
+                  if name.startswith("paimon_ftindex/libpaimon_ftindex_ffi")
+                  or name == "paimon_ftindex/paimon_ftindex_ffi.dll"
+              ]
+              required_metadata = {
+                  "paimon_ftindex/LICENSE",
+                  "paimon_ftindex/NOTICE",
+                  "paimon_ftindex/DEPENDENCIES.rust.tsv",
+              }
+              if not libs:
+                  raise SystemExit(f"{wheel.name} does not contain the native 
FFI library")
+              if not any(lib.endswith(expected_suffixes) for lib in libs):
+                  raise SystemExit(f"{wheel.name} has unexpected native 
library names: {libs}")
+              missing_metadata = sorted(required_metadata - names)
+              if missing_metadata:
+                  raise SystemExit(
+                      f"{wheel.name} is missing license metadata: 
{missing_metadata}"
+                  )
+              if any(name.startswith("tests/") for name in names):
+                  raise SystemExit(f"{wheel.name} unexpectedly contains 
tests/")
+          PY
+
+      - name: Publish to TestPyPI
+        if: contains(github.ref_name, '-rc')
+        uses: 
pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
+        with:
+          repository-url: https://test.pypi.org/legacy/
+          skip-existing: true
+          packages-dir: dist
+          password: ${{ secrets.TEST_PYPI_API_TOKEN }}
+
+      - name: Publish to PyPI
+        if: ${{ !contains(github.ref_name, '-') }}
+        uses: 
pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
+        with:
+          skip-existing: true
+          packages-dir: dist
+          password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.github/workflows/release-python.yml 
b/.github/workflows/release-python.yml
new file mode 100644
index 0000000..b7ce6ec
--- /dev/null
+++ b/.github/workflows/release-python.yml
@@ -0,0 +1,183 @@
+# 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.
+
+# Build the paimon-ftindex Python release wheels.
+#
+# Trigger: called by release.yml or manually dispatched.
+# Publishing is handled by release-python-publish.yml after wheels are built.
+
+name: Release Python Wheels
+
+on:
+  workflow_call:
+  workflow_dispatch:
+
+concurrency:
+  group: release-python-${{ github.ref }}
+  cancel-in-progress: false
+
+permissions:
+  contents: read
+
+jobs:
+  wheels-linux:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - os: ubuntu-latest
+            arch: x86_64
+          - os: ubuntu-24.04-arm
+            arch: aarch64
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Inject RC version into pyproject.toml
+        if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 
'-rc')
+        run: |
+          TAG="${GITHUB_REF#refs/tags/v}"
+          # Convert 0.1.0-rc1 to PEP 440: 0.1.0rc1
+          PEP440_VERSION=$(echo "$TAG" | sed 's/-rc/rc/')
+          sed -i "s/^version = .*/version = \"${PEP440_VERSION}\"/" 
python/pyproject.toml
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+
+      - name: Build wheels via cibuildwheel
+        uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55
+        with:
+          package-dir: python
+          output-dir: wheelhouse
+        env:
+          CIBW_BUILD: "cp39-manylinux_${{ matrix.arch }}"
+          CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
+          CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28
+          CIBW_BEFORE_ALL_LINUX: >
+            curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s 
-- -y &&
+            source $HOME/.cargo/env &&
+            cargo build --release -p paimon-ftindex-ffi &&
+            cp target/release/libpaimon_ftindex_ffi.so 
{package}/paimon_ftindex/
+
+      - name: Upload wheels
+        uses: actions/upload-artifact@v5
+        with:
+          name: wheels-linux-${{ matrix.arch }}
+          path: wheelhouse/*.whl
+
+  wheels-macos:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - os: macos-latest
+            target: aarch64-apple-darwin
+            lib_name: libpaimon_ftindex_ffi.dylib
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Inject RC version into pyproject.toml
+        if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 
'-rc')
+        run: |
+          TAG="${GITHUB_REF#refs/tags/v}"
+          PEP440_VERSION=$(echo "$TAG" | sed 's/-rc/rc/')
+          sed -i '' "s/^version = .*/version = \"${PEP440_VERSION}\"/" 
python/pyproject.toml
+
+      - name: Setup Rust toolchain
+        run: |
+          rustup update stable
+          rustup default stable
+
+      - name: Build native library
+        run: cargo build --release -p paimon-ftindex-ffi
+
+      - name: Copy native library into package
+        run: cp target/release/${{ matrix.lib_name }} python/paimon_ftindex/
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+
+      - name: Install build tools
+        run: pip install build wheel setuptools delocate
+
+      - name: Build wheel
+        working-directory: python
+        env:
+          _PYTHON_HOST_PLATFORM: macosx-11.0-arm64
+          ARCHFLAGS: "-arch arm64"
+          MACOSX_DEPLOYMENT_TARGET: "11.0"
+        run: python -m build --wheel
+
+      - name: Repair wheel
+        run: |
+          delocate-wheel --require-archs arm64 -w python/dist/repaired 
python/dist/*.whl
+          rm python/dist/*.whl
+          mv python/dist/repaired/*.whl python/dist/
+
+      - name: Upload wheel
+        uses: actions/upload-artifact@v5
+        with:
+          name: wheels-${{ matrix.os }}-${{ matrix.target }}
+          path: python/dist/*.whl
+
+  wheels-windows:
+    runs-on: windows-latest
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Inject RC version into pyproject.toml
+        if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 
'-rc')
+        shell: bash
+        run: |
+          TAG="${GITHUB_REF#refs/tags/v}"
+          PEP440_VERSION=$(echo "$TAG" | sed 's/-rc/rc/')
+          sed -i "s/^version = .*/version = \"${PEP440_VERSION}\"/" 
python/pyproject.toml
+
+      - name: Setup Rust toolchain
+        run: |
+          rustup update stable
+          rustup default stable
+
+      - name: Build native library
+        run: cargo build --release -p paimon-ftindex-ffi
+
+      - name: Copy native library into package
+        shell: bash
+        run: cp target/release/paimon_ftindex_ffi.dll python/paimon_ftindex/
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+
+      - name: Install build tools
+        run: pip install build wheel setuptools
+
+      - name: Build wheel
+        working-directory: python
+        run: python -m build --wheel
+
+      - name: Upload wheel
+        uses: actions/upload-artifact@v5
+        with:
+          name: wheels-windows-x86_64
+          path: python/dist/*.whl
diff --git a/.github/workflows/release-rust.yml 
b/.github/workflows/release-rust.yml
new file mode 100644
index 0000000..99d3bca
--- /dev/null
+++ b/.github/workflows/release-rust.yml
@@ -0,0 +1,55 @@
+# 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.
+
+# Publish paimon-ftindex-core crate to crates.io.
+#
+# Trigger: called by release.yml or manually dispatched.
+# Pre-release tags (containing '-') only run dry-run checks without publishing.
+#
+# Token auth: add secret CARGO_REGISTRY_TOKEN for crates.io publishing.
+
+name: Release Rust
+
+on:
+  workflow_call:
+  workflow_dispatch:
+
+concurrency:
+  group: release-rust-${{ github.ref }}
+  cancel-in-progress: false
+
+jobs:
+  publish:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Setup Rust toolchain
+        run: |
+          rustup update stable
+          rustup default stable
+
+      - name: Dry run
+        run: cargo publish -p paimon-ftindex-core --dry-run
+
+      - name: Publish paimon-ftindex-core to crates.io
+        if: github.repository == 'apache/paimon-full-text' && 
startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-')
+        run: cargo publish -p paimon-ftindex-core
+        env:
+          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..7c1f516
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,59 @@
+# 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.
+
+# Orchestrate the full release while keeping each language release workflow
+# independently runnable through workflow_dispatch.
+
+name: Release
+
+on:
+  push:
+    tags:
+      - "v[0-9]+.[0-9]+.[0-9]+"
+      - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+"
+  workflow_dispatch:
+
+concurrency:
+  group: release-${{ github.ref }}
+  cancel-in-progress: false
+
+permissions:
+  actions: read
+  contents: read
+
+jobs:
+  rust:
+    name: Rust release
+    uses: ./.github/workflows/release-rust.yml
+    secrets: inherit
+
+  java:
+    name: Java release
+    uses: ./.github/workflows/release-java.yml
+    secrets: inherit
+
+  python-wheels:
+    name: Python wheels
+    uses: ./.github/workflows/release-python.yml
+    secrets: inherit
+
+  python-publish:
+    name: Python publish
+    needs: [rust, java, python-wheels]
+    if: startsWith(github.ref, 'refs/tags/')
+    uses: ./.github/workflows/release-python-publish.yml
+    secrets: inherit
diff --git a/DEPENDENCIES.rust.tsv b/DEPENDENCIES.rust.tsv
new file mode 100644
index 0000000..3fd9454
--- /dev/null
+++ b/DEPENDENCIES.rust.tsv
@@ -0,0 +1,185 @@
+crate  Apache-2.0      Apache-2.0 WITH LLVM-exception  BSD-2-Clause    
BSD-3-Clause    LGPL-2.1-or-later       MIT     MPL-2.0 Unicode-3.0     
Unlicense       Zlib    zlib-acknowledgement
[email protected]                                                                  
        X       
[email protected]                                             X               
        X               
[email protected]  X                                       X               
                        
[email protected] X                                       X                       
                
[email protected]     X                                       X               
                        
[email protected]  X                                       X                       
                
[email protected]  X                                       X                       
                
[email protected]        X                                       X               
                        
[email protected]                                               X               
                        
[email protected]      X                                       X                       
                
[email protected]       X                                       X               
                        
[email protected]        X                                       X               
                        
[email protected]        X                                       X               
                X       
[email protected]                                                X               
        X               
[email protected]                                           X                       
                
[email protected]      X                                       X                       
                
[email protected]                        X                                       
                        
[email protected]                                           X                       
                
[email protected]    X                                       X                       
                
[email protected]   X                                       X                       
                
[email protected]                                          X                       
                
[email protected]        X                                       X               
                        
[email protected]       X                                       X       
                                
[email protected]  X                                       X               
                        
[email protected] X                                       X               
                        
[email protected] X                                       X               
                        
[email protected]                                          X                       
                
[email protected]                                         X                       
                
[email protected]                                            X               
                        
[email protected]                                           X               
                        
[email protected]        X                                       X               
                        
[email protected]     X                                                       
                        
[email protected] X                                       X                       
                
[email protected]      X                                       X               
                        
[email protected]  X                                       X                       
                
[email protected]       X                                       X               
                        
[email protected]    X                                       X               
                        
[email protected]   X                                       X                       
                
[email protected]                                               X               
                        X
[email protected] X                                       X                       
                
[email protected]  X                                       X               
                        
[email protected]      X                                       X                       
                
[email protected]                                                                 
        X       
[email protected]     X                                       X                       
                
[email protected]    X                                       X               
                        
[email protected]   X                                       X               
                        
[email protected]    X                                       X               
                        
[email protected]    X                                       X               
                        
[email protected]        X                                       X               
                        
[email protected]        X                                       X               
                        
[email protected]       X                                       X               
                        
[email protected]       X                                       X       X       
                        
[email protected]       X                                       X               
                        
[email protected]    X                                                       
                        
[email protected]    X                                               
                                
[email protected]   X                                               
                                
[email protected]       X                                       X               
                        
[email protected]       X                                       X               
                        
[email protected]    X                                       X                       
                
[email protected]                                            X               
                        
[email protected]                                                X               
                        
[email protected]     X                                       X                       
                
[email protected]  X                                       X                       
                
[email protected]  X                                       X                       
                
[email protected]   X                                       X               
                        
[email protected]       X                                       X               
                        
[email protected]      X                                       X               
                        
[email protected]                                             X       
                                
[email protected]   X                                       X                       
                
[email protected]                                         X                       
                
[email protected]                                            X               
                        
[email protected]   X       X                               X               
                        
[email protected]     X                                       X                       
                
[email protected]                                             X                       
                
[email protected]                                                X               
                        
[email protected]                                             X               
                        
[email protected]                                           X                       
X               
[email protected] X                                       X                       
                
[email protected]  X                                       X               
                        
[email protected]                                             X               
                        
[email protected]       X                                       X               
                        
[email protected]                                              X                       
                
[email protected] X                                       X                       
                
[email protected]      X                                       X               
                        
[email protected]       X                                       X               
                        
[email protected] X                                       X                       
                
[email protected]                                            X               
                        
[email protected]                                               X               
                        
[email protected]      X                                               
                                
[email protected]       X                                               
                                
[email protected]       X                                               
                                
[email protected]                                             X                       
                
[email protected]                                             X               
                        
[email protected]                                           X               
                        
[email protected]                                              X               
                        
[email protected]        X                                       X       
                                
[email protected]      X                                       X               
                        
[email protected] X                                       X                       
                
[email protected]    X                                       X               
                        
[email protected]   X                                       X       
                                
[email protected]        X                                       X       
                                
[email protected]    X                                       X               
                        
[email protected]   X                                       X                       
                
[email protected]    X                               X       X                       
                
[email protected]    X                               X       X                       
                
[email protected]   X                                       X                       
                
[email protected]      X                                       X               
                        
[email protected]   X                                       X                       
                
[email protected]  X                                       X               
                        
[email protected]    X                                       X               
                        
[email protected]  X                                       X               
                        
[email protected] X                                       X                       
                
[email protected]                            X               X               
                        
[email protected]       X                                       X               
                        
[email protected]   X       X                               X                       
                
[email protected]     X                                       X               
                        
[email protected]                                                X               
        X               
[email protected]  X                                       X                       
                
[email protected]     X                                       X               
                        
[email protected]   X                                       X               
                        
[email protected]     X                                       X               
                        
[email protected]    X                                       X                       
                
[email protected]        X                                       X               
                        
[email protected]        X                                               
                                
[email protected]                                            X                       
                
[email protected]        X                                       X               
                        
[email protected]       X                                       X       
                                
[email protected]                                          X                       
                
[email protected]    X                                       X                       
                
[email protected]                                         X                       
                
[email protected]                                               X       
                                
[email protected]                                         X               
                        
[email protected]                                          X               
                        
[email protected]                                              X               
        X               
[email protected]                                           X               
                        
[email protected]                                           X       
                                
[email protected]                                          X               
                        
[email protected]                                          X               
                        
[email protected]                                            X       
                                
[email protected]        X                                       X               
                        
[email protected]       X                                       X               
                        
[email protected]       X                                       X               
                        
[email protected]  X                                       X               
                        
[email protected]  X                                       X               
                        
[email protected]    X                                       X                       
                
[email protected]        X                                       X               
                        
[email protected]   X                                       X                       
                
[email protected] X                                       X                       
                
[email protected]    X                                       X               
                        
[email protected]   X                                       X               
X                       
[email protected]                                              X               
        X               
[email protected]    X                                       X                       
                
[email protected]                                          X                       
X               
[email protected]+wasi-0.2.12       X       X                               X       
                                
[email protected]   X                                       X                       
                
[email protected]       X                                       
X                                       
[email protected]                                             X               
        X               
[email protected]     X                                       
X                                       
[email protected]     X                                       X               
                        
[email protected]     X                                       X               
                        
[email protected]     X                                       X               
                        
[email protected]     X                                       X               
                        
[email protected] X                                       X               
                        
[email protected] X                                       X               
                        
[email protected] X                                       X       
                                
[email protected] X                                       X       
                                
[email protected]    X                                       X       
                                
[email protected]    X                                       X       
                                
[email protected]        X                                       X       
                                
[email protected]        X                                       X       
                                
[email protected]    X                                       X       
                                
[email protected]       X                                       X       
                                
[email protected]       X                                       X       
                                
[email protected]      X                                       X       
                                
[email protected]      X                                       X       
                                
[email protected]  X                                       X       
                                
[email protected]  X                                       X       
                                
[email protected]     X                                       X       
                                
[email protected]     X                                       X       
                                
[email protected]     X       X                               X               
                        
[email protected]                                            X                       
                
[email protected]                                            X                       
                
[email protected]        X                                       X               
                        
[email protected]+zstd.1.5.7     X                                       X       
                                
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..f2bbd24
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,5 @@
+Apache Paimon Full Text
+Copyright 2026 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/core/Cargo.toml b/core/Cargo.toml
index e498c71..b7690a5 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -1,6 +1,7 @@
 [package]
 name = "paimon-ftindex-core"
 version = "0.1.0"
+description = "Apache Paimon Full Text — core Rust library"
 edition.workspace = true
 license.workspace = true
 repository.workspace = true
diff --git a/deny.toml b/deny.toml
new file mode 100644
index 0000000..2e37fb4
--- /dev/null
+++ b/deny.toml
@@ -0,0 +1,31 @@
+# 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.
+
+[licenses]
+allow = [
+    "Apache-2.0",
+    "Apache-2.0 WITH LLVM-exception",
+    "BSD-2-Clause",
+    "BSD-3-Clause",
+    "CC0-1.0",
+    "ISC",
+    "MIT",
+    "Unicode-3.0",
+    # tantivy-jieba's compression stack (libflate/hashbrown) pulls in 
Zlib-licensed
+    # crates (adler32, foldhash). Zlib is ASF Category A (permissive).
+    "Zlib",
+]
diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml
index b8f479d..eafe046 100644
--- a/ffi/Cargo.toml
+++ b/ffi/Cargo.toml
@@ -1,6 +1,7 @@
 [package]
 name = "paimon-ftindex-ffi"
 version = "0.1.0"
+description = "Apache Paimon Full Text C FFI bindings"
 edition.workspace = true
 license.workspace = true
 repository.workspace = true
diff --git a/java/pom.xml b/java/pom.xml
index e569e4b..6acbcac 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -1,4 +1,22 @@
 <?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
 <project xmlns="http://maven.apache.org/POM/4.0.0";
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
@@ -52,6 +70,18 @@
     </properties>
 
     <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+            </resource>
+            <resource>
+                <directory>${project.basedir}/..</directory>
+                <includes>
+                    <include>DEPENDENCIES.rust.tsv</include>
+                </includes>
+                <targetPath>META-INF</targetPath>
+            </resource>
+        </resources>
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
@@ -74,4 +104,106 @@
             <scope>test</scope>
         </dependency>
     </dependencies>
+
+    <profiles>
+        <profile>
+            <id>release</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-gpg-plugin</artifactId>
+                        <version>3.2.8</version>
+                        <executions>
+                            <execution>
+                                <id>sign-artifacts</id>
+                                <phase>verify</phase>
+                                <goals>
+                                    <goal>sign</goal>
+                                </goals>
+                                <configuration>
+                                    <bestPractices>true</bestPractices>
+                                    <gpgArguments>
+                                        <arg>--pinentry-mode</arg>
+                                        <arg>loopback</arg>
+                                    </gpgArguments>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-source-plugin</artifactId>
+                        <version>3.3.1</version>
+                        <configuration>
+                            <!-- The native libraries are downloaded into
+                                 src/main/resources/native during the release 
build and belong only
+                                 in the binary jar. Exclude them so the 
sources jar contains source
+                                 code (and META-INF metadata), not platform 
binaries. -->
+                            <excludes>
+                                <exclude>native/**</exclude>
+                            </excludes>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <id>attach-sources</id>
+                                <goals>
+                                    <goal>jar-no-fork</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-javadoc-plugin</artifactId>
+                        <version>3.6.3</version>
+                        <configuration>
+                            <quiet>true</quiet>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <id>attach-javadocs</id>
+                                <goals>
+                                    <goal>jar</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-enforcer-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>enforce-maven</id>
+                                <goals>
+                                    <goal>enforce</goal>
+                                </goals>
+                                <configuration>
+                                    <rules>
+                                        <requireJavaVersion>
+                                            <version>1.8.0</version>
+                                        </requireJavaVersion>
+                                        <requireMavenVersion>
+                                            <version>[3.1.1,)</version>
+                                        </requireMavenVersion>
+                                    </rules>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.sonatype.plugins</groupId>
+                        <artifactId>nexus-staging-maven-plugin</artifactId>
+                        <version>1.7.0</version>
+                        <extensions>true</extensions>
+                        <configuration>
+                            <serverId>apache.releases.https</serverId>
+                            <nexusUrl>https://repository.apache.org/</nexusUrl>
+                            
<autoReleaseAfterClose>false</autoReleaseAfterClose>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
 </project>
diff --git 
a/java/src/main/java/org/apache/paimon/index/fulltext/FullTextNative.java 
b/java/src/main/java/org/apache/paimon/index/fulltext/FullTextNative.java
index 2016dcc..5c68c3e 100644
--- a/java/src/main/java/org/apache/paimon/index/fulltext/FullTextNative.java
+++ b/java/src/main/java/org/apache/paimon/index/fulltext/FullTextNative.java
@@ -1,17 +1,95 @@
 package org.apache.paimon.index.fulltext;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+
 final class FullTextNative {
 
+    private static final String LIB_NAME = "paimon_ftindex_jni";
+
     static {
+        loadNativeLibrary();
+    }
+
+    private FullTextNative() {}
+
+    private static void loadNativeLibrary() {
         String libPath = System.getenv("PAIMON_FTINDEX_JNI_LIB_PATH");
         if (libPath != null && !libPath.isEmpty()) {
             System.load(libPath);
-        } else {
-            System.loadLibrary("paimon_ftindex_jni");
+            return;
+        }
+
+        UnsatisfiedLinkError loadLibraryError = null;
+        try {
+            System.loadLibrary(LIB_NAME);
+            return;
+        } catch (UnsatisfiedLinkError e) {
+            loadLibraryError = e;
+        }
+
+        String os = normalizeOs(System.getProperty("os.name", ""));
+        String arch = normalizeArch(System.getProperty("os.arch", ""));
+        String libFileName = mapLibraryName(os);
+        String resourcePath = "/native/" + os + "/" + arch + "/" + libFileName;
+
+        try (InputStream in = 
FullTextNative.class.getResourceAsStream(resourcePath)) {
+            if (in == null) {
+                UnsatisfiedLinkError error =
+                        new UnsatisfiedLinkError("Native library not found in 
JAR: " + resourcePath);
+                error.addSuppressed(loadLibraryError);
+                throw error;
+            }
+            File tempFile = File.createTempFile("paimon_ftindex_jni", 
libFileName);
+            tempFile.deleteOnExit();
+            Files.copy(in, tempFile.toPath(), 
StandardCopyOption.REPLACE_EXISTING);
+            System.load(tempFile.getAbsolutePath());
+        } catch (IOException e) {
+            UnsatisfiedLinkError error =
+                    new UnsatisfiedLinkError(
+                            "Failed to extract native library " + resourcePath 
+ ": " + e.getMessage());
+            error.addSuppressed(loadLibraryError);
+            throw error;
         }
     }
 
-    private FullTextNative() {}
+    private static String normalizeOs(String osName) {
+        String lower = osName.toLowerCase();
+        if (lower.contains("linux")) {
+            return "linux";
+        } else if (lower.contains("mac") || lower.contains("darwin")) {
+            return "macos";
+        } else if (lower.contains("win")) {
+            return "windows";
+        }
+        throw new UnsatisfiedLinkError("Unsupported OS: " + osName);
+    }
+
+    private static String normalizeArch(String archName) {
+        String lower = archName.toLowerCase();
+        if (lower.equals("amd64") || lower.equals("x86_64")) {
+            return "x86_64";
+        } else if (lower.equals("aarch64") || lower.equals("arm64")) {
+            return "aarch64";
+        }
+        throw new UnsatisfiedLinkError("Unsupported architecture: " + 
archName);
+    }
+
+    private static String mapLibraryName(String os) {
+        switch (os) {
+            case "linux":
+                return "libpaimon_ftindex_jni.so";
+            case "macos":
+                return "libpaimon_ftindex_jni.dylib";
+            case "windows":
+                return "paimon_ftindex_jni.dll";
+            default:
+                throw new UnsatisfiedLinkError("Unsupported OS: " + os);
+        }
+    }
 
     static native long createWriter(String[] keys, String[] values);
 
diff --git 
a/java/src/test/java/org/apache/paimon/index/fulltext/FullTextNativeRoundTripTest.java
 
b/java/src/test/java/org/apache/paimon/index/fulltext/FullTextNativeRoundTripTest.java
index 41701b9..6b35280 100644
--- 
a/java/src/test/java/org/apache/paimon/index/fulltext/FullTextNativeRoundTripTest.java
+++ 
b/java/src/test/java/org/apache/paimon/index/fulltext/FullTextNativeRoundTripTest.java
@@ -20,8 +20,9 @@ public class FullTextNativeRoundTripTest {
     @Test
     public void testJavaNativeRoundTrip() throws Exception {
         assumeTrue(
-                "PAIMON_FTINDEX_JNI_LIB_PATH must point to the built JNI 
library",
-                nativeLibraryConfigured());
+                "PAIMON_FTINDEX_JNI_LIB_PATH must point to the built JNI 
library "
+                        + "or the native library must be present in classpath 
resources",
+                nativeLibraryAvailable());
 
         ByteArrayOutputStream output = new ByteArrayOutputStream();
         try (FullTextIndexWriter writer = 
FullTextIndexWriter.create(Collections.emptyMap())) {
@@ -72,8 +73,9 @@ public class FullTextNativeRoundTripTest {
     @Test
     public void testJavaNativeMultiFieldRoundTrip() throws Exception {
         assumeTrue(
-                "PAIMON_FTINDEX_JNI_LIB_PATH must point to the built JNI 
library",
-                nativeLibraryConfigured());
+                "PAIMON_FTINDEX_JNI_LIB_PATH must point to the built JNI 
library "
+                        + "or the native library must be present in classpath 
resources",
+                nativeLibraryAvailable());
 
         Map<String, String> options = Collections.singletonMap("text-fields", 
"title,body");
         ByteArrayOutputStream output = new ByteArrayOutputStream();
@@ -100,9 +102,51 @@ public class FullTextNativeRoundTripTest {
         }
     }
 
-    private static boolean nativeLibraryConfigured() {
+    private static boolean nativeLibraryAvailable() {
         String path = System.getenv("PAIMON_FTINDEX_JNI_LIB_PATH");
-        return path != null && !path.isEmpty() && new File(path).isFile();
+        if (path != null && !path.isEmpty() && new File(path).isFile()) {
+            return true;
+        }
+        return 
FullTextNativeRoundTripTest.class.getResource(nativeResourcePath()) != null;
+    }
+
+    private static String nativeResourcePath() {
+        String os = normalizeOs(System.getProperty("os.name", ""));
+        String arch = normalizeArch(System.getProperty("os.arch", ""));
+        return "/native/" + os + "/" + arch + "/" + mapLibraryName(os);
+    }
+
+    private static String normalizeOs(String osName) {
+        String lower = osName.toLowerCase();
+        if (lower.contains("linux")) {
+            return "linux";
+        } else if (lower.contains("mac") || lower.contains("darwin")) {
+            return "macos";
+        } else if (lower.contains("win")) {
+            return "windows";
+        }
+        return "unsupported";
+    }
+
+    private static String normalizeArch(String archName) {
+        String lower = archName.toLowerCase();
+        if (lower.equals("amd64") || lower.equals("x86_64")) {
+            return "x86_64";
+        } else if (lower.equals("aarch64") || lower.equals("arm64")) {
+            return "aarch64";
+        }
+        return "unsupported";
+    }
+
+    private static String mapLibraryName(String os) {
+        if (os.equals("linux")) {
+            return "libpaimon_ftindex_jni.so";
+        } else if (os.equals("macos")) {
+            return "libpaimon_ftindex_jni.dylib";
+        } else if (os.equals("windows")) {
+            return "paimon_ftindex_jni.dll";
+        }
+        return "unsupported";
     }
 
     private static String matchQuery(String terms, String column) {
diff --git a/jni/Cargo.toml b/jni/Cargo.toml
index 1ae2df2..13b4750 100644
--- a/jni/Cargo.toml
+++ b/jni/Cargo.toml
@@ -1,6 +1,7 @@
 [package]
 name = "paimon-ftindex-jni"
 version = "0.1.0"
+description = "Apache Paimon Full Text — JNI bindings for Java"
 edition.workspace = true
 license.workspace = true
 repository.workspace = true
diff --git a/python/pyproject.toml b/python/pyproject.toml
index a941384..85fb0f2 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -1,5 +1,5 @@
 [build-system]
-requires = ["setuptools>=61"]
+requires = ["setuptools>=64", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
@@ -13,4 +13,14 @@ license = { text = "Apache-2.0" }
 test = ["pytest"]
 
 [tool.setuptools.packages.find]
-where = ["."]
+include = ["paimon_ftindex*"]
+
+[tool.setuptools.package-data]
+paimon_ftindex = [
+    "libpaimon_ftindex_ffi.dylib",
+    "libpaimon_ftindex_ffi.so",
+    "paimon_ftindex_ffi.dll",
+    "LICENSE",
+    "NOTICE",
+    "DEPENDENCIES.rust.tsv",
+]
diff --git a/python/setup.py b/python/setup.py
index 6068493..793725e 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -1,3 +1,103 @@
-from setuptools import setup
+#
+# 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.
+#
 
-setup()
+"""Build helper: copies the pre-built native FFI library into the package."""
+
+import os
+import platform
+import shutil
+
+from setuptools import Distribution, setup
+from setuptools.command.build_py import build_py
+from wheel.bdist_wheel import bdist_wheel
+
+
+def _project_root():
+    return 
os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
+
+
+def _package_dir():
+    return os.path.join(os.path.dirname(os.path.abspath(__file__)), 
"paimon_ftindex")
+
+
+def _lib_name():
+    system = platform.system()
+    if system == "Darwin":
+        return "libpaimon_ftindex_ffi.dylib"
+    if system == "Windows":
+        return "paimon_ftindex_ffi.dll"
+    return "libpaimon_ftindex_ffi.so"
+
+
+def _find_native_lib():
+    here = os.path.dirname(os.path.abspath(__file__))
+    lib = _lib_name()
+
+    env_path = os.environ.get("PAIMON_FTINDEX_LIB_PATH")
+    if env_path:
+        if os.path.isfile(env_path):
+            return env_path
+        candidate = os.path.join(env_path, lib)
+        if os.path.isfile(candidate):
+            return candidate
+
+    for profile in ["release", "debug"]:
+        candidate = os.path.join(here, "..", "target", profile, lib)
+        if os.path.isfile(candidate):
+            return candidate
+
+    return None
+
+
+class BuildPyWithNativeLib(build_py):
+    def run(self):
+        src = _find_native_lib()
+        if src:
+            dst = os.path.join(_package_dir(), _lib_name())
+            shutil.copy2(src, dst)
+        for metadata_file in ["LICENSE", "NOTICE", "DEPENDENCIES.rust.tsv"]:
+            metadata_path = os.path.join(_project_root(), metadata_file)
+            if os.path.isfile(metadata_path):
+                shutil.copy2(metadata_path, os.path.join(_package_dir(), 
metadata_file))
+        super().run()
+
+
+class PlatformWheel(bdist_wheel):
+    """Tag wheel as py3-none-{platform} since this is a ctypes package."""
+
+    def finalize_options(self):
+        bdist_wheel.finalize_options(self)
+        self.root_is_pure = False
+
+    def get_tag(self):
+        _, _, plat = bdist_wheel.get_tag(self)
+        return "py3", "none", plat
+
+
+class BinaryDistribution(Distribution):
+    """Force the wheel to be platform-specific."""
+
+    def has_ext_modules(self):
+        return True
+
+
+setup(
+    cmdclass={"build_py": BuildPyWithNativeLib, "bdist_wheel": PlatformWheel},
+    distclass=BinaryDistribution,
+)
diff --git a/tools/update_branch_version.sh b/tools/create_release_branch.sh
similarity index 52%
copy from tools/update_branch_version.sh
copy to tools/create_release_branch.sh
index 03ec8a8..71fd87c 100755
--- a/tools/update_branch_version.sh
+++ b/tools/create_release_branch.sh
@@ -20,6 +20,7 @@
 ##
 ## Variables with defaults (if not overwritten by environment)
 ##
+RELEASE_CANDIDATE=${RELEASE_CANDIDATE:-none}
 MVN=${MVN:-mvn}
 
 # fail immediately
@@ -29,41 +30,28 @@ set -o nounset
 set -o xtrace
 
 CURR_DIR=`pwd`
-if [[ `basename $CURR_DIR` != "tools" ]] ; then
+if [[ `basename ${CURR_DIR}` != "tools" ]] ; then
   echo "You have to call the script from the tools/ dir"
   exit 1
 fi
 
 ###########################
 
-OLD_VERSION=${OLD_VERSION}
-NEW_VERSION=${NEW_VERSION}
-
-
-if [ -z "${OLD_VERSION}" ]; then
-       echo "OLD_VERSION is unset"
-       exit 1
-fi
-
-if [ -z "${NEW_VERSION}" ]; then
-       echo "NEW_VERSION is unset"
+if [ -z "${RELEASE_VERSION}" ]; then
+       echo "RELEASE_VERSION is unset"
        exit 1
 fi
 
 cd ..
 
-# For Cargo.toml and pyproject.toml, strip any -SNAPSHOT suffix (not valid in 
those ecosystems)
-NEW_VERSION_CLEAN=$(echo "$NEW_VERSION" | sed 's/-SNAPSHOT//')
-
-#change version in all pom files (match both exact and -SNAPSHOT suffix)
-find . -name 'pom.xml' -type f -exec perl -pi -e 
's#<version>'$OLD_VERSION'(-SNAPSHOT)?</version>#<version>'$NEW_VERSION'</version>#'
 {} \;
-
-#change version in Cargo.toml files
-find . -name 'Cargo.toml' -not -path '*/target/*' -type f -exec perl -pi -e 
's#^version = "'$OLD_VERSION'"#version = "'$NEW_VERSION_CLEAN'"#' {} \;
+target_branch=release-${RELEASE_VERSION}
+if [ "${RELEASE_CANDIDATE}" != "none" ]; then
+  target_branch=${target_branch}-rc${RELEASE_CANDIDATE}
+fi
 
-#change version in pyproject.toml
-perl -pi -e 's#^version = "'$OLD_VERSION'"#version = "'$NEW_VERSION_CLEAN'"#' 
python/pyproject.toml
+git checkout -b ${target_branch}
 
-git commit -am "Update version to $NEW_VERSION"
+RELEASE_HASH=`git rev-parse HEAD`
+echo "Echo created release hash $RELEASE_HASH"
 
-echo "Don't forget to push the change."
+echo "Done. Don't forget to create the release tag on GitHub and push the 
changes."
diff --git a/tools/create_source_release.sh b/tools/create_source_release.sh
new file mode 100755
index 0000000..70f4684
--- /dev/null
+++ b/tools/create_source_release.sh
@@ -0,0 +1,90 @@
+#!/usr/bin/env bash
+
+#
+# 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.
+#
+
+# Create ASF source release artifacts under tools/release/:
+#   apache-paimon-full-text-{version}-src.tgz
+#   apache-paimon-full-text-{version}-src.tgz.asc
+#   apache-paimon-full-text-{version}-src.tgz.sha512
+#
+# Usage: cd tools && RELEASE_VERSION=0.1.0 ./create_source_release.sh
+
+##
+## Variables with defaults (if not overwritten by environment)
+##
+MVN=${MVN:-mvn}
+
+# fail immediately
+set -o errexit
+set -o nounset
+set -o pipefail
+# print command before executing
+set -o xtrace
+
+CURR_DIR=`pwd`
+if [[ `basename $CURR_DIR` != "tools" ]] ; then
+  echo "You have to call the script from the tools/ dir"
+  exit 1
+fi
+
+if [ "$(uname)" == "Darwin" ]; then
+    SHASUM="shasum -a 512"
+else
+    SHASUM="sha512sum"
+fi
+
+###########################
+
+RELEASE_VERSION=${RELEASE_VERSION}
+
+if [ -z "${RELEASE_VERSION}" ]; then
+       echo "RELEASE_VERSION is unset"
+       exit 1
+fi
+
+rm -rf release
+mkdir release
+cd ..
+
+echo "Creating source package"
+
+ARCHIVE="apache-paimon-full-text-${RELEASE_VERSION}-src.tgz"
+# Archive from Git objects so filesystem metadata such as macOS xattrs is not 
included.
+git archive --format=tar --prefix="paimon-full-text-${RELEASE_VERSION}/" 
'HEAD^{tree}' . \
+  ':(exclude).gitignore' ':(exclude).gitattributes' \
+  ':(exclude).asf.yaml' ':(exclude).github' \
+  ':(exclude)deploysettings.xml' ':(exclude)target' \
+  ':(exclude).idea' ':(exclude)*.iml' ':(exclude).DS_Store' \
+  | gzip -n > "tools/release/${ARCHIVE}"
+
+cd tools/release
+
+gpg --armor --detach-sig "${ARCHIVE}"
+$SHASUM "${ARCHIVE}" > "${ARCHIVE}.sha512"
+
+echo "Verifying GPG signature"
+gpg --verify "${ARCHIVE}.asc" "${ARCHIVE}"
+
+echo "Verifying tarball integrity"
+tar tzf "${ARCHIVE}" > /dev/null
+
+echo ""
+echo "Source release created successfully. Artifacts in tools/release/:"
+ls -la ${CURR_DIR}/release/apache-paimon-full-text-*
+echo ""
+echo "Next: upload contents to SVN (see docs/creating-a-release.html)."
diff --git a/tools/dependencies.py b/tools/dependencies.py
new file mode 100755
index 0000000..a4f7be1
--- /dev/null
+++ b/tools/dependencies.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+
+#
+# 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.
+#
+
+"""Check and generate Rust dependency license information for ASF release 
compliance.
+
+Requires cargo-deny: cargo install cargo-deny
+Requires Python 3.11+ (uses tomllib).
+
+Usage:
+    python3 tools/dependencies.py check      # Verify all deps have approved 
licenses
+    python3 tools/dependencies.py generate   # Generate DEPENDENCIES.rust.tsv
+"""
+
+import sys
+
+if sys.version_info < (3, 11):
+    sys.exit(
+        "This script requires Python 3.11 or newer (uses tomllib). "
+        f"Current: {sys.version}."
+    )
+
+import subprocess
+import tomllib
+from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
+from pathlib import Path
+
+ROOT_DIR = Path(__file__).resolve().parent.parent
+
+PACKAGES = ["."]
+root_cargo = ROOT_DIR / "Cargo.toml"
+if root_cargo.exists():
+    with open(root_cargo, "rb") as f:
+        data = tomllib.load(f)
+    members = data.get("workspace", {}).get("members", [])
+    if isinstance(members, list):
+        for m in members:
+            if isinstance(m, str) and m:
+                PACKAGES.append(m)
+
+
+def check_single_package(root):
+    pkg_dir = ROOT_DIR / root if root != "." else ROOT_DIR
+    if (pkg_dir / "Cargo.toml").exists():
+        print(f"Checking dependencies of {root}")
+        subprocess.run(
+            ["cargo", "deny", "check", "license"],
+            cwd=pkg_dir,
+            check=True,
+        )
+    else:
+        print(f"Skipping {root} as Cargo.toml does not exist")
+
+
+def check_deps():
+    for d in PACKAGES:
+        check_single_package(d)
+
+
+def generate_single_package(root):
+    pkg_dir = ROOT_DIR / root if root != "." else ROOT_DIR
+    if (pkg_dir / "Cargo.toml").exists():
+        print(f"Generating dependencies for {root}")
+        result = subprocess.run(
+            ["cargo", "deny", "list", "-f", "tsv", "-t", "0.6"],
+            cwd=pkg_dir,
+            capture_output=True,
+            text=True,
+        )
+        if result.returncode != 0:
+            raise RuntimeError(
+                f"cargo deny list failed in {root}: {result.stderr or 
result.stdout}"
+            )
+        out_file = pkg_dir / "DEPENDENCIES.rust.tsv"
+        out_file.write_text(result.stdout)
+        print(f"  Written to {out_file}")
+    else:
+        print(f"Skipping {root} as Cargo.toml does not exist")
+
+
+def generate_deps():
+    for d in PACKAGES:
+        generate_single_package(d)
+
+
+if __name__ == "__main__":
+    parser = ArgumentParser(
+        description="Check and generate Rust dependency license information",
+        formatter_class=ArgumentDefaultsHelpFormatter,
+    )
+    parser.set_defaults(func=parser.print_help)
+    subparsers = parser.add_subparsers()
+
+    parser_check = subparsers.add_parser(
+        "check", description="Check dependencies", help="Check dependency 
licenses"
+    )
+    parser_check.set_defaults(func=check_deps)
+
+    parser_generate = subparsers.add_parser(
+        "generate",
+        description="Generate dependencies",
+        help="Generate DEPENDENCIES.rust.tsv",
+    )
+    parser_generate.set_defaults(func=generate_deps)
+
+    args = parser.parse_args()
+    arg_dict = dict(vars(args))
+    del arg_dict["func"]
+    args.func(**arg_dict)
diff --git a/tools/update_branch_version.sh b/tools/update_branch_version.sh
index 03ec8a8..affe599 100755
--- a/tools/update_branch_version.sh
+++ b/tools/update_branch_version.sh
@@ -52,17 +52,19 @@ fi
 
 cd ..
 
-# For Cargo.toml and pyproject.toml, strip any -SNAPSHOT suffix (not valid in 
those ecosystems)
+# Cargo.toml and pyproject.toml never carry the -SNAPSHOT suffix, so strip it
+# from both old and new versions when matching/replacing there.
+OLD_VERSION_CLEAN=$(echo "$OLD_VERSION" | sed 's/-SNAPSHOT//')
 NEW_VERSION_CLEAN=$(echo "$NEW_VERSION" | sed 's/-SNAPSHOT//')
 
 #change version in all pom files (match both exact and -SNAPSHOT suffix)
 find . -name 'pom.xml' -type f -exec perl -pi -e 
's#<version>'$OLD_VERSION'(-SNAPSHOT)?</version>#<version>'$NEW_VERSION'</version>#'
 {} \;
 
 #change version in Cargo.toml files
-find . -name 'Cargo.toml' -not -path '*/target/*' -type f -exec perl -pi -e 
's#^version = "'$OLD_VERSION'"#version = "'$NEW_VERSION_CLEAN'"#' {} \;
+find . -name 'Cargo.toml' -not -path '*/target/*' -type f -exec perl -pi -e 
's#^version = "'$OLD_VERSION_CLEAN'"#version = "'$NEW_VERSION_CLEAN'"#' {} \;
 
 #change version in pyproject.toml
-perl -pi -e 's#^version = "'$OLD_VERSION'"#version = "'$NEW_VERSION_CLEAN'"#' 
python/pyproject.toml
+perl -pi -e 's#^version = "'$OLD_VERSION_CLEAN'"#version = 
"'$NEW_VERSION_CLEAN'"#' python/pyproject.toml
 
 git commit -am "Update version to $NEW_VERSION"
 
diff --git a/tools/validate_asf_yaml.py b/tools/validate_asf_yaml.py
new file mode 100644
index 0000000..e189125
--- /dev/null
+++ b/tools/validate_asf_yaml.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+
+#
+# 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.
+#
+
+"""Validate .asf.yaml structural correctness.
+
+Checks YAML syntax, root-level structure, and type constraints for
+known fields. Does NOT reject unknown keys — the ASF infra schema
+evolves independently and unrecognized keys are silently ignored by
+the ASF gitbox service.
+
+Reference: https://github.com/apache/infrastructure-asfyaml
+"""
+
+import sys
+
+try:
+    import yaml
+except ImportError:
+    sys.exit(
+        "PyYAML is required: pip install pyyaml\n"
+        "Or use: python3 -c \"import pip; pip.main(['install', 'pyyaml'])\""
+    )
+
+ASF_YAML_PATH = ".asf.yaml"
+
+
+def validate():
+    errors = []
+
+    try:
+        with open(ASF_YAML_PATH, "r") as f:
+            data = yaml.safe_load(f)
+    except FileNotFoundError:
+        print(f"SKIP: {ASF_YAML_PATH} not found")
+        return 0
+    except yaml.YAMLError as e:
+        print(f"ERROR: Invalid YAML syntax in {ASF_YAML_PATH}: {e}")
+        return 1
+
+    if not isinstance(data, dict):
+        print(f"ERROR: {ASF_YAML_PATH} root must be a mapping")
+        return 1
+
+    # Validate types of known sections
+    for key in ("github", "notifications", "staging", "publish"):
+        if key in data and not isinstance(data[key], dict):
+            errors.append(f"'{key}' must be a mapping, got 
{type(data[key]).__name__}")
+
+    github = data.get("github")
+    if isinstance(github, dict):
+        # features values must be booleans
+        features = github.get("features")
+        if isinstance(features, dict):
+            for key, val in features.items():
+                if not isinstance(val, bool):
+                    errors.append(
+                        f"'github.features.{key}' must be a boolean, "
+                        f"got {type(val).__name__}"
+                    )
+
+        # enabled_merge_buttons values must be booleans or strings
+        merge_buttons = github.get("enabled_merge_buttons")
+        if isinstance(merge_buttons, dict):
+            for key, val in merge_buttons.items():
+                if not isinstance(val, (bool, str)):
+                    errors.append(
+                        f"'github.enabled_merge_buttons.{key}' must be bool or 
string, "
+                        f"got {type(val).__name__}"
+                    )
+
+        # labels must be a list of strings
+        labels = github.get("labels")
+        if labels is not None and not isinstance(labels, list):
+            errors.append(f"'github.labels' must be a list, got 
{type(labels).__name__}")
+
+    if errors:
+        print(f"ERROR: {ASF_YAML_PATH} validation failed:")
+        for err in errors:
+            print(f"  - {err}")
+        return 1
+
+    print(f"OK: {ASF_YAML_PATH} is valid")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(validate())

Reply via email to