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())