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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new dab9cfe1e7 [release] Add release workflows and documentation (#8948)
dab9cfe1e7 is described below

commit dab9cfe1e75e423bf1210884dfa8d124cf0fd0d4
Author: Jingsong Lee <[email protected]>
AuthorDate: Fri Jul 31 09:00:03 2026 +0800

    [release] Add release workflows and documentation (#8948)
---
 .github/workflows/release-java.yml                 | 227 ++++++++++
 .github/workflows/release-python-publish.yml       | 118 ++++++
 .github/workflows/release-python.yml               | 129 ++++++
 .github/workflows/release.yml                      | 129 ++++++
 docs/docs/project/creating-a-release.md            | 466 +++++++++++++++++++++
 docs/docs/project/verifying-a-release-candidate.md | 380 +++++++++++++++++
 docs/sidebars.js                                   |   2 +
 7 files changed, 1451 insertions(+)

diff --git a/.github/workflows/release-java.yml 
b/.github/workflows/release-java.yml
new file mode 100644
index 0000000000..ade3ea7e3b
--- /dev/null
+++ b/.github/workflows/release-java.yml
@@ -0,0 +1,227 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+################################################################################
+
+name: Release Java
+
+on:
+  workflow_call:
+  workflow_dispatch:
+
+concurrency:
+  group: release-java-${{ github.ref }}
+  cancel-in-progress: false
+
+permissions:
+  contents: read
+
+jobs:
+  package:
+    name: Package ${{ matrix.lane }}
+    if: >-
+      github.repository == 'apache/paimon' &&
+      startsWith(github.ref, 'refs/tags/') &&
+      contains(github.ref_name, '-rc')
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - lane: jdk8
+            java_version: 8
+          - lane: jdk11
+            java_version: 11
+          - lane: jdk17
+            java_version: 17
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Set up JDK ${{ matrix.java_version }}
+        uses: actions/setup-java@v5
+        with:
+          java-version: ${{ matrix.java_version }}
+          distribution: temurin
+          cache: maven
+
+      - name: Set up Maven
+        uses: stCarolas/setup-maven@d6af6abeda15e98926a57b5aa970a96bb37f97d1 # 
v5
+        with:
+          maven-version: 3.8.8
+
+      - name: Validate release tag and version
+        id: release
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          release_version="$(
+            mvn -q -DforceStdout help:evaluate \
+              -Dexpression=project.version
+          )"
+          rc_number="${GITHUB_REF_NAME##*-rc}"
+          expected_tag="release-${release_version}-rc${rc_number}"
+
+          if [[ "${GITHUB_REF_NAME}" != "${expected_tag}" ]]; then
+            echo "Tag ${GITHUB_REF_NAME} does not match Maven version 
${release_version}" >&2
+            exit 1
+          fi
+          if [[ "${release_version}" == *-SNAPSHOT ]]; then
+            echo "Release version must not contain -SNAPSHOT" >&2
+            exit 1
+          fi
+
+          echo "version=${release_version}" >> "${GITHUB_OUTPUT}"
+          echo "rc_number=${rc_number}" >> "${GITHUB_OUTPUT}"
+
+          java -version
+          mvn -version
+
+      - name: Reject SNAPSHOT project versions and dependencies
+        shell: bash
+        env:
+          LANE: ${{ matrix.lane }}
+        run: |
+          set -euo pipefail
+
+          
enforcer_goal="org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce"
+          enforcer_rules="requireReleaseVersion,requireReleaseDeps"
+          case "${LANE}" in
+            jdk8)
+              mvn -ntp -B "${enforcer_goal}" \
+                -Denforcer.rules="${enforcer_rules}" \
+                -Papache-release,docs-and-source,spark3,flink1
+              ;;
+            jdk11)
+              mvn -ntp -B "${enforcer_goal}" \
+                -Denforcer.rules="${enforcer_rules}" \
+                -Papache-release,docs-and-source,flink2 \
+                -pl 
org.apache.paimon:paimon-flink-2.0,org.apache.paimon:paimon-flink-2.1,org.apache.paimon:paimon-flink-2.2,org.apache.paimon:paimon-flink2-common,org.apache.paimon:paimon-iceberg
 \
+                -am
+              ;;
+            jdk17)
+              mvn -ntp -B "${enforcer_goal}" \
+                -Denforcer.rules="${enforcer_rules}" \
+                -Papache-release,docs-and-source,spark4 \
+                -pl 
org.apache.paimon:paimon-spark-common_2.13,org.apache.paimon:paimon-spark4-common_2.13,org.apache.paimon:paimon-spark-4.0_2.13,org.apache.paimon:paimon-spark-4.1_2.13
 \
+                -am
+              ;;
+            *)
+              echo "Unknown Java release lane: ${LANE}" >&2
+              exit 1
+              ;;
+          esac
+
+      - name: Package Java release artifacts
+        shell: bash
+        env:
+          LANE: ${{ matrix.lane }}
+          MAVEN_OPTS: -Xmx4096m
+        run: |
+          set -euo pipefail
+
+          mkdir -p release-java
+          log="release-java/${LANE}.log"
+          install_log="release-java/${LANE}-install.log"
+          case "${LANE}" in
+            jdk8)
+              mvn clean install -ntp -B \
+                -Papache-release,docs-and-source,spark3,flink1 \
+                -DskipTests -Dgpg.skip=true -Dstyle.color=never \
+                2>&1 | tee "${log}" "${install_log}"
+              ;;
+            jdk11)
+              mvn clean install -ntp -B \
+                -Pdocs-and-source,flink2 \
+                -DskipTests -Dstyle.color=never \
+                -pl 
org.apache.paimon:paimon-flink-2.0,org.apache.paimon:paimon-flink-2.1,org.apache.paimon:paimon-flink-2.2,org.apache.paimon:paimon-iceberg
 \
+                -am \
+                2>&1 | tee "${log}"
+              mvn install -ntp -B \
+                -Papache-release,docs-and-source,flink2 \
+                -DskipTests -Dgpg.skip=true -Dstyle.color=never \
+                -pl 
org.apache.paimon:paimon-flink-2.0,org.apache.paimon:paimon-flink-2.1,org.apache.paimon:paimon-flink-2.2,org.apache.paimon:paimon-flink2-common,org.apache.paimon:paimon-iceberg
 \
+                2>&1 | tee -a "${log}" "${install_log}"
+              ;;
+            jdk17)
+              mvn clean install -ntp -B \
+                -Pdocs-and-source,spark4 \
+                -DskipTests -Dstyle.color=never \
+                -pl 
paimon-spark/paimon-spark-4.0,paimon-spark/paimon-spark-4.1 \
+                -am \
+                2>&1 | tee "${log}"
+              mvn install -ntp -B \
+                -Papache-release,docs-and-source,spark4 \
+                -DskipTests -Dgpg.skip=true -Dstyle.color=never \
+                -pl 
org.apache.paimon:paimon-spark-common_2.13,org.apache.paimon:paimon-spark4-common_2.13,org.apache.paimon:paimon-spark-4.0_2.13,org.apache.paimon:paimon-spark-4.1_2.13
 \
+                2>&1 | tee -a "${log}" "${install_log}"
+              ;;
+            *)
+              echo "Unknown Java release lane: ${LANE}" >&2
+              exit 1
+              ;;
+          esac
+
+          files="release-java/${LANE}-files.txt"
+          while IFS= read -r artifact; do
+            if [[ "${artifact}" == "${GITHUB_WORKSPACE}/"* ]]; then
+              artifact="./${artifact#"${GITHUB_WORKSPACE}/"}"
+            elif [[ "${artifact}" != ./* ]]; then
+              echo "Unexpected installed artifact path: ${artifact}" >&2
+              exit 1
+            fi
+            if [[ ! -f "${artifact}" ]]; then
+              echo "Installed artifact does not exist: ${artifact}" >&2
+              exit 1
+            fi
+            printf '%s\n' "${artifact}"
+          done < <(
+            sed -n \
+              's/^\[INFO\] Installing \(.*\.jar\) to .*$/\1/p' \
+              "${install_log}"
+          ) | LC_ALL=C sort -u > "${files}"
+          if [[ ! -s "${files}" ]]; then
+            echo "No installed Java release artifacts were found for ${LANE}" 
>&2
+            exit 1
+          fi
+
+          while IFS= read -r artifact; do
+            sha512sum "${artifact}"
+          done < "${files}" \
+            > "release-java/${LANE}-sha512.txt"
+
+          tar -czf "release-java/${LANE}-packages.tar.gz" \
+            -T "${files}"
+          sha512sum "release-java/${LANE}-packages.tar.gz" \
+            > "release-java/${LANE}-packages.tar.gz.sha512"
+
+          {
+            echo "lane=${LANE}"
+            echo "jdk=${{ matrix.java_version }}"
+            echo "version=${{ steps.release.outputs.version }}"
+            echo "rc=${{ steps.release.outputs.rc_number }}"
+            echo "commit=${GITHUB_SHA}"
+            echo "signed=false"
+            echo "staged=false"
+            echo "artifact_source=maven-install-log"
+          } > "release-java/${LANE}-manifest.txt"
+
+      - name: Upload Java packages
+        uses: actions/upload-artifact@v5
+        with:
+          name: java-package-${{ matrix.lane }}
+          path: release-java/
+          if-no-files-found: error
diff --git a/.github/workflows/release-python-publish.yml 
b/.github/workflows/release-python-publish.yml
new file mode 100644
index 0000000000..eaa2af9eb1
--- /dev/null
+++ b/.github/workflows/release-python-publish.yml
@@ -0,0 +1,118 @@
+################################################################################
+#  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 PyPaimon Publish
+
+on:
+  workflow_call:
+
+concurrency:
+  group: release-python-publish-${{ github.ref }}
+  cancel-in-progress: false
+
+permissions:
+  actions: read
+  contents: read
+
+jobs:
+  publish:
+    name: Publish PyPaimon
+    if: >-
+      github.repository == 'apache/paimon' &&
+      startsWith(github.ref, 'refs/tags/')
+    environment:
+      name: ${{ contains(github.ref_name, '-rc') && 'testpypi' || 'pypi' }}
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v6
+
+      - uses: actions/download-artifact@v5
+        with:
+          name: pypaimon-distributions
+          path: dist
+
+      - name: Verify package versions
+        id: package_version
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          base_version="$(
+            sed -n 's/^VERSION = "\(.*\)"/\1/p' \
+              paimon-python/setup.py
+          )"
+          expected_version="${base_version}"
+          if [[ "${GITHUB_REF_NAME}" == *-rc* ]]; then
+            expected_version="${base_version}rc${GITHUB_REF_NAME##*-rc}"
+          fi
+
+          test -f "dist/pypaimon-${expected_version}.tar.gz"
+          compgen -G "dist/pypaimon-${expected_version}-*.whl" > /dev/null
+
+          shopt -s nullglob
+          distributions=(dist/*)
+          if [[ "${#distributions[@]}" -ne 2 ]]; then
+            echo "Expected one sdist and one wheel, found ${#distributions[@]} 
files" >&2
+            printf '%s\n' "${distributions[@]}" >&2
+            exit 1
+          fi
+
+          echo "version=${expected_version}" >> "${GITHUB_OUTPUT}"
+
+      - name: Reject existing TestPyPI RC version
+        if: contains(github.ref_name, '-rc')
+        shell: bash
+        env:
+          EXPECTED_VERSION: ${{ steps.package_version.outputs.version }}
+        run: |
+          set -euo pipefail
+
+          status="$(
+            curl --silent --show-error --output /dev/null \
+              --write-out '%{http_code}' \
+              "https://test.pypi.org/pypi/pypaimon/${EXPECTED_VERSION}/json";
+          )"
+
+          case "${status}" in
+            404)
+              ;;
+            200)
+              echo "pypaimon ${EXPECTED_VERSION} already exists on TestPyPI; 
create a new RC" >&2
+              exit 1
+              ;;
+            *)
+              echo "TestPyPI version check returned HTTP ${status}; refusing 
to publish" >&2
+              exit 1
+              ;;
+          esac
+
+      - name: Publish RC 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: false
+          packages-dir: dist
+          password: ${{ secrets.TEST_PYPI_API_TOKEN }}
+
+      - name: Publish final release to PyPI
+        if: ${{ !contains(github.ref_name, '-rc') }}
+        uses: 
pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
+        with:
+          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 0000000000..5f264aa137
--- /dev/null
+++ b/.github/workflows/release-python.yml
@@ -0,0 +1,129 @@
+################################################################################
+#  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 PyPaimon source distribution and universal wheel. Publishing is
+# handled by release-python-publish.yml after all required release jobs pass.
+
+name: Release PyPaimon Package
+
+on:
+  workflow_call:
+  workflow_dispatch:
+
+concurrency:
+  group: release-python-${{ github.ref }}
+  cancel-in-progress: false
+
+permissions:
+  contents: read
+
+jobs:
+  package:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.11"
+          cache: pip
+          cache-dependency-path: paimon-python/dev/requirements.txt
+
+      - name: Set package version
+        id: package_version
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          base_version="$(
+            sed -n 's/^VERSION = "\(.*\)"/\1/p' \
+              paimon-python/setup.py
+          )"
+          if [[ -z "${base_version}" || "${base_version}" == *dev* || 
"${base_version}" == *rc* ]]; then
+            echo "PyPaimon release base version is invalid: 
${base_version:-missing}" >&2
+            exit 1
+          fi
+
+          package_version="${base_version}"
+          if [[ "${GITHUB_REF}" == refs/tags/*-rc* ]]; then
+            rc_number="${GITHUB_REF_NAME##*-rc}"
+            package_version="${base_version}rc${rc_number}"
+            sed -i \
+              "s/^VERSION = \".*\"/VERSION = \"${package_version}\"/" \
+              paimon-python/setup.py
+          fi
+
+          echo "version=${package_version}" >> "${GITHUB_OUTPUT}"
+          echo "Building PyPaimon ${package_version}"
+
+      - name: Build distributions
+        working-directory: paimon-python
+        run: |
+          python -m pip install --upgrade pip setuptools wheel twine
+          python setup.py sdist bdist_wheel
+          python -m twine check dist/*
+
+      - name: Verify distribution contents
+        working-directory: paimon-python
+        env:
+          EXPECTED_VERSION: ${{ steps.package_version.outputs.version }}
+        run: |
+          python - <<'PY'
+          import glob
+          import os
+          import tarfile
+          import zipfile
+
+          expected = os.environ["EXPECTED_VERSION"]
+          sdist = "dist/pypaimon-{}.tar.gz".format(expected)
+          wheels = glob.glob("dist/pypaimon-{}-*.whl".format(expected))
+          if not os.path.isfile(sdist):
+              raise SystemExit("Missing expected sdist: {}".format(sdist))
+          if len(wheels) != 1:
+              raise SystemExit("Expected one wheel, found: {}".format(wheels))
+
+          with tarfile.open(sdist, "r:gz") as archive:
+              names = archive.getnames()
+              prefix = "pypaimon-{}/".format(expected)
+              for required in ("LICENSE", "NOTICE", "README.md", "setup.py"):
+                  if prefix + required not in names:
+                      raise SystemExit("Missing {} from 
sdist".format(required))
+
+          with zipfile.ZipFile(wheels[0]) as archive:
+              names = archive.namelist()
+              license_files = [
+                  name for name in names
+                  if ".dist-info/" in name and name.endswith("/LICENSE")
+              ]
+              notice_files = [
+                  name for name in names
+                  if ".dist-info/" in name and name.endswith("/NOTICE")
+              ]
+              if not license_files or not notice_files:
+                  raise SystemExit("Wheel is missing LICENSE or NOTICE")
+          PY
+
+      - name: Upload PyPaimon distributions
+        uses: actions/upload-artifact@v5
+        with:
+          name: pypaimon-distributions
+          path: |
+            paimon-python/dist/*.tar.gz
+            paimon-python/dist/*.whl
+          if-no-files-found: error
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000000..e9f14bf483
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,129 @@
+################################################################################
+#  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 Java and PyPaimon packaging. Java signing and Nexus staging, plus
+# the signed ASF source archives, remain Release Manager responsibilities.
+
+name: Release
+
+on:
+  push:
+    tags:
+      - "release-[0-9]+.[0-9]+.[0-9]+"
+      - "release-[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:
+  validation:
+    name: Validate release tag
+    runs-on: ubuntu-latest
+    outputs:
+      release_version: ${{ steps.release.outputs.release_version }}
+      release_kind: ${{ steps.release.outputs.release_kind }}
+      rc_number: ${{ steps.release.outputs.rc_number }}
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Set up JDK 8
+        uses: actions/setup-java@v5
+        with:
+          java-version: 8
+          distribution: temurin
+          cache: maven
+
+      - name: Set up Maven
+        uses: stCarolas/setup-maven@d6af6abeda15e98926a57b5aa970a96bb37f97d1 # 
v5
+        with:
+          maven-version: 3.8.8
+
+      - name: Validate tag against project version
+        id: release
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          if [[ "${GITHUB_REF}" != refs/tags/* ]]; then
+            echo "Release workflow must run from a tag, got ${GITHUB_REF}" >&2
+            exit 1
+          fi
+
+          release_version="$(
+            mvn -q -DforceStdout help:evaluate \
+              -Dexpression=project.version
+          )"
+          if [[ -z "${release_version}" || "${release_version}" == *-SNAPSHOT 
]]; then
+            echo "Project release version is invalid: 
${release_version:-missing}" >&2
+            exit 1
+          fi
+
+          final_tag="release-${release_version}"
+          rc_prefix="${final_tag}-rc"
+          release_kind=""
+          rc_number=""
+          if [[ "${GITHUB_REF_NAME}" == "${final_tag}" ]]; then
+            release_kind="final"
+          elif [[ "${GITHUB_REF_NAME}" == "${rc_prefix}"* ]]; then
+            rc_number="${GITHUB_REF_NAME#"${rc_prefix}"}"
+            if [[ ! "${rc_number}" =~ ^[0-9]+$ ]]; then
+              echo "RC tag must be ${rc_prefix}N, got ${GITHUB_REF_NAME}" >&2
+              exit 1
+            fi
+            release_kind="rc"
+          else
+            echo "Tag must be ${final_tag} or ${rc_prefix}N, got 
${GITHUB_REF_NAME}" >&2
+            exit 1
+          fi
+
+          echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}"
+          echo "release_kind=${release_kind}" >> "${GITHUB_OUTPUT}"
+          echo "rc_number=${rc_number}" >> "${GITHUB_OUTPUT}"
+
+          echo "Validated ${release_kind} tag ${GITHUB_REF_NAME}"
+          java -version
+          mvn -version
+
+  java:
+    name: Java packages
+    needs: validation
+    if: needs.validation.outputs.release_kind == 'rc'
+    uses: ./.github/workflows/release-java.yml
+
+  python-package:
+    name: PyPaimon package
+    needs: validation
+    uses: ./.github/workflows/release-python.yml
+
+  python-publish:
+    name: PyPaimon publish
+    needs: [validation, java, python-package]
+    if: >-
+      always() &&
+      needs.validation.result == 'success' &&
+      startsWith(github.ref, 'refs/tags/') &&
+      needs.python-package.result == 'success' &&
+      (needs.java.result == 'success' ||
+       needs.validation.outputs.release_kind == 'final')
+    uses: ./.github/workflows/release-python-publish.yml
diff --git a/docs/docs/project/creating-a-release.md 
b/docs/docs/project/creating-a-release.md
new file mode 100644
index 0000000000..bfe20952e6
--- /dev/null
+++ b/docs/docs/project/creating-a-release.md
@@ -0,0 +1,466 @@
+---
+title: "Creating a Release"
+sidebar_position: 2
+---
+
+<!--
+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.
+-->
+
+# Creating a Release
+
+This guide is for the Release Manager (RM) of Apache Paimon and PyPaimon. It
+follows the [ASF Release 
Policy](https://www.apache.org/legal/release-policy.html)
+and the [ASF Release Distribution 
Policy](https://infra.apache.org/release-distribution).
+
+:::warning
+
+The signed source archives are the Apache releases. Maven and PyPI packages are
+convenience artifacts. The final release must promote the source files and Java
+staging repositories approved by the community; do not rebuild or replace them
+after the vote.
+
+:::
+
+## Release model
+
+Paimon contains two independently versioned deliverables. Do not assume that
+their version numbers are equal.
+
+| Deliverable | Candidate | Published location |
+| --- | --- | --- |
+| Paimon source | `apache-paimon-PAIMON_VERSION-src.tgz`, `.asc`, `.sha512` | 
ASF distribution |
+| Java convenience artifacts | Maven artifacts built in the JDK 8, 11, and 17 
lanes | Apache Nexus staging, then Maven Central |
+| PyPaimon source | `pypaimon-PYPAIMON_VERSION.tar.gz`, `.asc`, `.sha512` | 
ASF distribution |
+| Python convenience package | `pypaimon==PYPAIMON_VERSIONrcRC_NUMBER` for an 
RC | TestPyPI, then `pypaimon==PYPAIMON_VERSION` on PyPI |
+
+A combined release vote covers both signed source candidates. This guide does
+not define an independent PyPaimon release. Before releasing PyPaimon
+separately, the PMC must define a Python-only tag and workflow which do not
+depend on the Maven version or Java jobs, and must provide a signed source
+package which is independently sufficient to build and test the release.
+
+### Java build matrix
+
+The three Java lanes are different release targets, not interchangeable build
+JDKs:
+
+| JDK | Maven profiles and scope | Main artifacts |
+| --- | --- | --- |
+| 8 | `spark3,flink1` and the default reactor | Paimon core, Flink 1.x, Spark 
3.x, Hive, filesystems, bundles, and other Java 8 artifacts |
+| 11 | `flink2` plus `paimon-iceberg` | Flink 2.x, `paimon-flink2-common`, and 
Iceberg integration |
+| 17 | `spark4` | Spark 4.x and its Scala 2.13 common artifacts |
+
+Each lane must use the matching JDK. Building everything on JDK 17 with a lower
+compiler target is not a substitute for running the JDK 8 and JDK 11 lanes.
+
+## GitHub Actions release workflow
+
+The release process uses the
+[Release 
workflow](https://github.com/apache/paimon/actions/workflows/release.yml)
+to package Java and PyPaimon from every signed RC tag. The RM signs and stages
+Java locally, and creates and signs the two ASF source archives locally from
+the same tag. The RM's GPG private key is never stored in GitHub Actions.
+
+The workflow has the following contract:
+
+| Job | Required behavior |
+| --- | --- |
+| Validation | Require an RC tag named `release-PAIMON_VERSION-rcN` or a final 
tag named `release-PAIMON_VERSION`, where `PAIMON_VERSION` exactly equals the 
root Maven `project.version` |
+| Java 8 | Use Temurin 8 to package the default reactor with Spark 3 and Flink 
1, then upload the package, checksums, manifest, and log |
+| Java 11 | Use Temurin 11 to package Flink 2 and Iceberg, then upload the 
package, checksums, manifest, and log |
+| Java 17 | Use Temurin 17 to package Spark 4, then upload the package, 
checksums, manifest, and log |
+| Python package | Build and validate the PyPaimon source distribution and 
universal wheel, then upload them as workflow artifacts |
+| Python publish | Publish an RC to TestPyPI only after all Java and Python 
packaging jobs pass; publish a final tag to PyPI |
+
+Before packaging, every Java lane runs Maven Enforcer's
+`requireReleaseVersion` and `requireReleaseDeps` rules over its complete 
reactor
+scope. The latter includes transitive dependencies. Any remaining
+`-SNAPSHOT` project, parent, direct dependency, or transitive dependency is a
+release blocker.
+
+The Java jobs use `-Dgpg.skip=true` and never receive Nexus credentials or a
+GPG private key. Their artifacts are build evidence, not the Maven staging
+repositories used for the vote. The Python RC job uses the protected
+`testpypi` environment to publish `PYPAIMON_VERSIONrcRC_NUMBER` to TestPyPI.
+The final job uses the separate protected `pypi` environment.
+
+Configure both environments with an RM approval and a deployment policy which
+allows only the corresponding signed release tags. Store
+`TEST_PYPI_API_TOKEN` only in `testpypi` and `PYPI_API_TOKEN` only in `pypi`.
+Do not store either token as a repository or organization secret, and do not
+pass unrelated secrets to the reusable publishing workflow.
+
+## One-time RM setup
+
+Before managing the first release:
+
+1. Create a GPG key associated with your `@apache.org` identity and publish it
+   to a public key server.
+2. Append the public key to the Paimon
+   [KEYS](https://downloads.apache.org/paimon/KEYS) file. Never remove keys
+   required to verify an older release.
+3. Configure Git and local GPG to sign tags and Maven artifacts with the same
+   key.
+4. Configure the local Maven server ID `apache.releases.https` with the RM's
+   Apache Nexus credentials. Do not copy the GPG key or Nexus credentials into
+   GitHub Actions.
+5. Confirm access to the ASF distribution SVN repository, Apache Nexus,
+   TestPyPI, and PyPI.
+6. Create the protected GitHub environments `testpypi` and `pypi`. Configure
+   required RM reviewers and release-tag deployment policies, store only the
+   matching token in each environment, and remove any repository- or
+   organization-level copies of those tokens.
+7. Verify that the Python release secrets are configured without printing
+   their values in an Actions log.
+
+```shell
+gpg --list-secret-keys --keyid-format LONG
+git config user.signingkey
+svn --version
+```
+
+## Prepare the release
+
+### Agree on the release
+
+Discuss the release on `[email protected]`, select an RM, resolve release
+blockers, review incompatible changes and upgrade notes, and prepare release
+notes. Ensure CI is green on the commit from which the candidate will be cut.
+
+### Set the release variables
+
+The following example deliberately uses different Java and Python versions:
+
+```shell
+PAIMON_VERSION="2.0.0"
+PYPAIMON_VERSION="1.5.0"
+RC_NUMBER="1"
+
+RC_REF="release-${PAIMON_VERSION}-rc${RC_NUMBER}"
+RELEASE_TAG="release-${PAIMON_VERSION}"
+```
+
+Use these exact values in the branch, tag, workflow inputs, SVN directories,
+vote email, and Java package manifests.
+
+### Work from a clean clone
+
+```shell
+git clone https://github.com/apache/paimon.git paimon-release
+cd paimon-release
+git checkout master
+git pull --ff-only origin master
+git status --short
+```
+
+The last command must produce no output.
+
+### Create the RC branch and set versions
+
+Create the RC branch with the existing helper:
+
+```shell
+RELEASE_VERSION="${PAIMON_VERSION}" \
+RELEASE_CANDIDATE="${RC_NUMBER}" \
+  ./tools/releasing/create_release_branch.sh
+```
+
+This creates `release-PAIMON_VERSION-rcRC_NUMBER`.
+
+Change all Maven modules from `PAIMON_VERSION-SNAPSHOT` to
+`PAIMON_VERSION`. The helper commits the Maven version change:
+
+```shell
+NEW_VERSION="${PAIMON_VERSION}" \
+  ./tools/releasing/update_branch_version.sh
+```
+
+Set `VERSION` in `paimon-python/setup.py` to the final
+`PYPAIMON_VERSION`, without `.dev` or an RC suffix. The release workflow
+derives the TestPyPI version by appending `rcRC_NUMBER`; the source candidate
+keeps the final version.
+
+Review and commit the Python version, dependency declarations, release notes,
+`LICENSE`, `NOTICE`, and any generated legal files. Then confirm that the
+candidate contains no snapshot or development version:
+
+```shell
+mvn -q -DforceStdout help:evaluate -Dexpression=project.version
+python3 paimon-python/setup.py --version
+rg --glob 'pom.xml' "<version>${PAIMON_VERSION}-SNAPSHOT</version>"
+rg '^VERSION = ".*\.dev' paimon-python/setup.py
+git status --short
+```
+
+The first two commands must print the requested release versions, the `rg`
+commands must find no release-version marker, and the worktree must be clean.
+
+### Sign and push the RC tag
+
+The historical Paimon convention uses the same name for the RC branch and tag.
+Use explicit refs when pushing so Git cannot select the wrong one:
+
+```shell
+git tag -s "${RC_REF}" \
+  -m "Apache Paimon ${PAIMON_VERSION} and PyPaimon ${PYPAIMON_VERSION} 
RC${RC_NUMBER}"
+git tag -v "${RC_REF}"
+
+git push origin \
+  "refs/heads/${RC_REF}:refs/heads/${RC_REF}"
+git push origin \
+  "refs/tags/${RC_REF}:refs/tags/${RC_REF}"
+```
+
+Pushing the signed tag starts the Release workflow. The common validation job
+must succeed before any Java or Python packaging or publishing job can run.
+Wait for every required job to succeed. Record:
+
+- the workflow run URL and `head_sha`;
+- the JDK 8, 11, and 17 package artifact names, manifests, and SHA-512
+  checksums;
+- the TestPyPI project/version URL.
+
+Do not start the vote when a required lane is missing or has been rerun from a
+different commit.
+
+## Stage the Java convenience artifacts locally
+
+Check out the exact signed RC tag in a clean clone. Use the RM machine's local
+GPG key and Maven credentials; do not download a private key into a CI runner.
+Use Maven 3.8.8, matching the Release workflow. Export `GPG_TTY` when the local
+GPG agent needs terminal access:
+
+```shell
+git checkout --detach "refs/tags/${RC_REF}"
+export GPG_TTY="$(tty)"
+
+gpg --list-secret-keys --keyid-format LONG
+mvn -q -DforceStdout help:evaluate -Dexpression=project.version
+```
+
+Download the three Java package artifacts from the recorded workflow run and
+verify each `*-packages.tar.gz.sha512` file. Keep the downloaded
+`jdk8-sha512.txt`, `jdk11-sha512.txt`, and `jdk17-sha512.txt` files for the
+reproducibility check after each local build.
+
+Run each existing staging script under its required JDK. Confirm the output of
+`java -version` and `mvn -version` before every command:
+
+```shell
+# JDK 8: default reactor, Flink 1.x, and Spark 3.x
+./tools/releasing/deploy_staging_jars.sh
+sha512sum -c /path/to/java-package-jdk8/jdk8-sha512.txt
+
+# JDK 11: Flink 2.x and Iceberg
+./tools/releasing/deploy_staging_jars_for_jdk11.sh
+sha512sum -c /path/to/java-package-jdk11/jdk11-sha512.txt
+
+# JDK 17: Spark 4.x
+./tools/releasing/deploy_staging_jars_for_jdk17.sh
+sha512sum -c /path/to/java-package-jdk17/jdk17-sha512.txt
+```
+
+On macOS, replace `sha512sum -c` with `shasum -a 512 -c`. A checksum mismatch
+between the CI package and the locally signed build is a release blocker.
+Maven signs the release artifacts with the RM's local GPG key and deploys them
+using the local `apache.releases.https` server credentials. After each command,
+record the `orgapachepaimon-XXXX` repository ID and confirm that it contains
+only the intended lane. Close each repository and resolve every close-time rule
+failure before starting the vote. Closing freezes the candidate that voters
+inspect. Do not release a repository before the vote passes.
+
+## Stage the source candidates
+
+Create both source candidates locally from the exact signed tag in a fresh
+clone. The Paimon helper creates, signs, and checksums the main source archive.
+Build the PyPaimon source distribution separately and sign it with the same RM
+key:
+
+```shell
+git checkout --detach "refs/tags/${RC_REF}"
+
+RELEASE_VERSION="${PAIMON_VERSION}" \
+  ./tools/releasing/create_source_release.sh
+
+cd paimon-python
+python3 setup.py sdist
+gpg --armor --detach-sig \
+  "dist/pypaimon-${PYPAIMON_VERSION}.tar.gz"
+
+if command -v sha512sum >/dev/null 2>&1; then
+  sha512sum "dist/pypaimon-${PYPAIMON_VERSION}.tar.gz" \
+    > "dist/pypaimon-${PYPAIMON_VERSION}.tar.gz.sha512"
+else
+  shasum -a 512 "dist/pypaimon-${PYPAIMON_VERSION}.tar.gz" \
+    > "dist/pypaimon-${PYPAIMON_VERSION}.tar.gz.sha512"
+fi
+
+cp "dist/pypaimon-${PYPAIMON_VERSION}.tar.gz"* ../release/
+cd ..
+```
+
+Verify both signatures and checksum files locally before uploading them to ASF
+dist dev.
+
+```shell
+svn checkout --depth=immediates \
+  https://dist.apache.org/repos/dist/dev/paimon/ paimon-dist-dev
+
+mkdir "paimon-dist-dev/paimon-${PAIMON_VERSION}-rc${RC_NUMBER}"
+cp "release/apache-paimon-${PAIMON_VERSION}-src.tgz"* \
+  "paimon-dist-dev/paimon-${PAIMON_VERSION}-rc${RC_NUMBER}/"
+
+mkdir "paimon-dist-dev/pypaimon-${PYPAIMON_VERSION}-rc${RC_NUMBER}"
+cp "release/pypaimon-${PYPAIMON_VERSION}.tar.gz"* \
+  "paimon-dist-dev/pypaimon-${PYPAIMON_VERSION}-rc${RC_NUMBER}/"
+
+svn add \
+  "paimon-dist-dev/paimon-${PAIMON_VERSION}-rc${RC_NUMBER}" \
+  "paimon-dist-dev/pypaimon-${PYPAIMON_VERSION}-rc${RC_NUMBER}"
+svn commit -m \
+  "Stage Paimon ${PAIMON_VERSION} and PyPaimon ${PYPAIMON_VERSION} 
RC${RC_NUMBER}" \
+  paimon-dist-dev
+```
+
+Never overwrite an existing RC directory. TestPyPI versions are immutable for
+this release process as well: the workflow checks for an existing version 
before
+upload and fails instead of skipping files. The uploader also rejects duplicate
+files to cover races after the check. If any byte changes, create a new RC with
+a new RC number.
+
+## Call the vote
+
+Send a plain-text vote to `[email protected]`. Keep it open for at least
+72 hours. The vote must receive at least three binding `+1` votes and more
+binding `+1` than binding `-1` votes.
+
+```text
+Subject: [VOTE] Release Apache Paimon ${PAIMON_VERSION} and PyPaimon 
${PYPAIMON_VERSION} (RC${RC_NUMBER})
+
+Hi everyone,
+
+Please review and vote on Apache Paimon ${PAIMON_VERSION} and
+PyPaimon ${PYPAIMON_VERSION}, release candidate ${RC_NUMBER}.
+
+[ ] +1 Approve
+[ ]  0 No opinion
+[ ] -1 Do not approve (please explain)
+
+Paimon source candidate:
+https://dist.apache.org/repos/dist/dev/paimon/paimon-${PAIMON_VERSION}-rc${RC_NUMBER}/
+
+PyPaimon source candidate:
+https://dist.apache.org/repos/dist/dev/paimon/pypaimon-${PYPAIMON_VERSION}-rc${RC_NUMBER}/
+
+Signed Git tag:
+release-${PAIMON_VERSION}-rc${RC_NUMBER}
+
+Commit:
+https://github.com/apache/paimon/commit/<RC_COMMIT_SHA>
+
+GitHub Actions release run:
+<WORKFLOW_RUN_URL>
+
+KEYS:
+https://downloads.apache.org/paimon/KEYS
+
+Closed Java staging repositories:
+<JDK_8_NEXUS_URL>
+<JDK_11_NEXUS_URL>
+<JDK_17_NEXUS_URL>
+
+PyPaimon RC:
+https://test.pypi.org/project/pypaimon/${PYPAIMON_VERSION}rc${RC_NUMBER}/
+
+Verification guide:
+https://github.com/apache/paimon/blob/release-${PAIMON_VERSION}-rc${RC_NUMBER}/docs/docs/project/verifying-a-release-candidate.md
+
+The vote will remain open for at least 72 hours.
+```
+
+After the deadline, tally binding and non-binding votes separately and send
+`[RESULT][VOTE]` to the same thread.
+
+## Replace a failed candidate
+
+If the vote finds a problem:
+
+1. Fix it through the normal review process.
+2. Drop every Nexus staging repository belonging to the failed RC.
+3. Remove the superseded dist-dev directories, or retain them temporarily when
+   useful to the vote discussion. Never replace their contents.
+4. Increment `RC_NUMBER`; never reuse the failed candidate's TestPyPI version.
+5. Create a new RC branch and signed tag, run every workflow lane again, stage
+   new source candidates, and start a new 72-hour vote.
+
+## Finalize an approved release
+
+### Create the final signed tag
+
+The final tag must point to exactly the approved RC commit:
+
+```shell
+git tag -s "${RELEASE_TAG}" "refs/tags/${RC_REF}^{commit}" \
+  -m "Release Apache Paimon ${PAIMON_VERSION}"
+
+test "$(git rev-parse "refs/tags/${RC_REF}^{commit}")" = \
+     "$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")"
+git tag -v "${RELEASE_TAG}"
+git push origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
+```
+
+### Promote the source releases
+
+Move, rather than copy or rebuild, both approved candidate directories:
+
+```shell
+svn mv -m "Release Apache Paimon ${PAIMON_VERSION}" \
+  
"https://dist.apache.org/repos/dist/dev/paimon/paimon-${PAIMON_VERSION}-rc${RC_NUMBER}";
 \
+  "https://dist.apache.org/repos/dist/release/paimon/paimon-${PAIMON_VERSION}";
+
+svn mv -m "Release PyPaimon ${PYPAIMON_VERSION}" \
+  
"https://dist.apache.org/repos/dist/dev/paimon/pypaimon-${PYPAIMON_VERSION}-rc${RC_NUMBER}";
 \
+  
"https://dist.apache.org/repos/dist/release/paimon/pypaimon-${PYPAIMON_VERSION}";
+```
+
+### Promote convenience artifacts
+
+1. In Nexus, confirm that every recorded JDK 8, 11, and 17 staging repository
+   is still closed and has the exact artifact tree approved by the vote.
+2. Release those exact closed repositories to Maven Central. Do not run Maven
+   deploy again.
+3. Approve the protected `pypi` promotion job for the final tag. It must build
+   `pypaimon==PYPAIMON_VERSION` from the approved tag commit and must not 
change
+   project source.
+4. Verify Maven Central and PyPI before announcing the release.
+
+### Publish and announce
+
+Create a GitHub release from `release-PAIMON_VERSION`, review the generated
+notes, and link both source releases. Update the Paimon download page and
+versioned documentation. After ASF mirrors, Maven Central, and PyPI are all
+available, announce the release to `[email protected]` and
+`[email protected]` from an `@apache.org` address.
+
+Remove superseded releases from the live ASF distribution area when required;
+they remain available from the
+[Apache archive](https://archive.apache.org/dist/paimon/).
+
+See [Verifying a Release Candidate](./verifying-a-release-candidate.md) for the
+voter checklist.
diff --git a/docs/docs/project/verifying-a-release-candidate.md 
b/docs/docs/project/verifying-a-release-candidate.md
new file mode 100644
index 0000000000..6910c909f4
--- /dev/null
+++ b/docs/docs/project/verifying-a-release-candidate.md
@@ -0,0 +1,380 @@
+---
+title: "Verifying a Release Candidate"
+sidebar_position: 3
+---
+
+<!--
+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.
+-->
+
+# Verifying a Release Candidate
+
+Verify a release candidate independently before voting. The signed Paimon and
+PyPaimon source archives are the release. Maven, TestPyPI, and GitHub Actions
+checks supplement source verification but do not replace it.
+
+Report only the checks, platforms, and tool versions that you actually used.
+
+## Collect the candidate inputs
+
+Take all values and URLs from the vote email:
+
+```shell
+PAIMON_VERSION="2.0.0"
+PYPAIMON_VERSION="1.5.0"
+RC_NUMBER="1"
+RC_TAG="release-${PAIMON_VERSION}-rc${RC_NUMBER}"
+
+PAIMON_RC_DIR="paimon-${PAIMON_VERSION}-rc${RC_NUMBER}"
+PAIMON_ARCHIVE="apache-paimon-${PAIMON_VERSION}-src.tgz"
+PYPAIMON_RC_DIR="pypaimon-${PYPAIMON_VERSION}-rc${RC_NUMBER}"
+PYPAIMON_ARCHIVE="pypaimon-${PYPAIMON_VERSION}.tar.gz"
+```
+
+Download the candidates from ASF dist dev, not from a third-party mirror:
+
+```shell
+curl -O 
"https://dist.apache.org/repos/dist/dev/paimon/${PAIMON_RC_DIR}/${PAIMON_ARCHIVE}";
+curl -O 
"https://dist.apache.org/repos/dist/dev/paimon/${PAIMON_RC_DIR}/${PAIMON_ARCHIVE}.asc";
+curl -O 
"https://dist.apache.org/repos/dist/dev/paimon/${PAIMON_RC_DIR}/${PAIMON_ARCHIVE}.sha512";
+
+curl -O 
"https://dist.apache.org/repos/dist/dev/paimon/${PYPAIMON_RC_DIR}/${PYPAIMON_ARCHIVE}";
+curl -O 
"https://dist.apache.org/repos/dist/dev/paimon/${PYPAIMON_RC_DIR}/${PYPAIMON_ARCHIVE}.asc";
+curl -O 
"https://dist.apache.org/repos/dist/dev/paimon/${PYPAIMON_RC_DIR}/${PYPAIMON_ARCHIVE}.sha512";
+
+curl -O https://downloads.apache.org/paimon/KEYS
+```
+
+Keep the workflow run URL, announced commit SHA, three Nexus staging URLs, and
+TestPyPI URL beside these files. All of them must identify this RC.
+
+## Verify signatures and checksums
+
+```shell
+gpg --import KEYS
+gpg --verify "${PAIMON_ARCHIVE}.asc" "${PAIMON_ARCHIVE}"
+gpg --verify "${PYPAIMON_ARCHIVE}.asc" "${PYPAIMON_ARCHIVE}"
+```
+
+Confirm that both signatures are good and that the full signing-key fingerprint
+belongs to the RM and appears in the downloaded `KEYS` file.
+
+On Linux:
+
+```shell
+sha512sum -c "${PAIMON_ARCHIVE}.sha512"
+sha512sum -c "${PYPAIMON_ARCHIVE}.sha512"
+```
+
+On macOS:
+
+```shell
+shasum -a 512 -c "${PAIMON_ARCHIVE}.sha512"
+shasum -a 512 -c "${PYPAIMON_ARCHIVE}.sha512"
+```
+
+## Verify Git provenance
+
+```shell
+git clone https://github.com/apache/paimon.git paimon-candidate-git
+git -C paimon-candidate-git fetch --tags
+git -C paimon-candidate-git tag -v "${RC_TAG}"
+git -C paimon-candidate-git rev-parse "refs/tags/${RC_TAG}^{commit}"
+```
+
+The resolved commit must equal both the SHA in the vote email and the
+`head_sha` of the successful Release workflow run. Inspect the commits since 
the
+previous release and confirm that the RC tag is attached to the intended 
branch.
+
+## Inspect the source archives
+
+List the archives before extracting them:
+
+```shell
+tar tzf "${PAIMON_ARCHIVE}" | sed -n '1,100p'
+tar tzf "${PYPAIMON_ARCHIVE}" | sed -n '1,100p'
+```
+
+Check at least the following:
+
+- The Paimon archive has exactly one top-level directory named
+  `paimon-PAIMON_VERSION`.
+- The PyPaimon archive has exactly one top-level directory named
+  `pypaimon-PYPAIMON_VERSION`.
+- `LICENSE`, `NOTICE`, README files, build files, dependency declarations, and
+  required source files are present and correct.
+- No Git metadata, IDE state, credentials, Maven `target` directories, Python
+  `dist` or `__pycache__` directories, compiled classes, generated JARs, or
+  unrelated binary files are included.
+- All bundled third-party material is compatible with the Apache License 2.0
+  and is recorded in `LICENSE` or `NOTICE` where required.
+- Maven POMs use `PAIMON_VERSION` without `-SNAPSHOT`.
+- `paimon-python/setup.py` and PyPaimon package metadata use
+  `PYPAIMON_VERSION` without `.dev`.
+
+Extract the Paimon candidate:
+
+```shell
+tar xzf "${PAIMON_ARCHIVE}"
+```
+
+Run the repository licensing checks. The PyPaimon checker temporarily extracts
+its source package and removes that temporary directory when it finishes:
+
+```shell
+cd "paimon-${PAIMON_VERSION}"
+mvn -ntp -DskipTests apache-rat:check
+cd ..
+
+SOURCE_PACKAGE="${PYPAIMON_ARCHIVE}" \
+  "./paimon-${PAIMON_VERSION}/paimon-python/dev/check-licensing.sh"
+```
+
+Review the generated reports rather than relying only on the exit codes.
+After the licensing check, extract a fresh PyPaimon source tree for the build
+and test steps below:
+
+```shell
+tar xzf "${PYPAIMON_ARCHIVE}"
+```
+
+## Build Java from the source archive
+
+Use three clean extracted copies or three clean containers so that class files
+and Maven state from one JDK cannot leak into another lane. Record `java
+-version` and `mvn -version` for each build.
+
+### JDK 8 lane
+
+This lane covers the default reactor, Flink 1.x, and Spark 3.x:
+
+```shell
+(
+  cd "paimon-${PAIMON_VERSION}"
+  mvn -ntp clean verify -Pspark3,flink1
+)
+```
+
+For supplementary or non-binding verification, you may build the same scope
+used for staging and state explicitly that tests were skipped:
+
+```shell
+(
+  cd "paimon-${PAIMON_VERSION}"
+  mvn -ntp clean install -DskipTests \
+    -Pdocs-and-source,spark3,flink1
+)
+```
+
+### JDK 11 lane
+
+This lane covers Flink 2.x and Iceberg:
+
+```shell
+(
+  cd "paimon-${PAIMON_VERSION}"
+  mvn -ntp clean verify -Pflink2 \
+    -pl 
org.apache.paimon:paimon-flink-2.0,org.apache.paimon:paimon-flink-2.1,org.apache.paimon:paimon-flink-2.2,org.apache.paimon:paimon-iceberg
 \
+    -am
+)
+```
+
+The packaging-equivalent build is:
+
+```shell
+(
+  cd "paimon-${PAIMON_VERSION}"
+  mvn -ntp clean install -DskipTests -Pdocs-and-source,flink2 \
+    -pl 
org.apache.paimon:paimon-flink-2.0,org.apache.paimon:paimon-flink-2.1,org.apache.paimon:paimon-flink-2.2,org.apache.paimon:paimon-iceberg
 \
+    -am
+)
+```
+
+### JDK 17 lane
+
+This lane covers Spark 4.x:
+
+```shell
+(
+  cd "paimon-${PAIMON_VERSION}"
+  mvn -ntp clean verify -Pspark4 \
+    -pl paimon-spark/paimon-spark-4.0,paimon-spark/paimon-spark-4.1 \
+    -am
+)
+```
+
+The packaging-equivalent build is:
+
+```shell
+(
+  cd "paimon-${PAIMON_VERSION}"
+  mvn -ntp clean install -DskipTests -Pdocs-and-source,spark4 \
+    -pl paimon-spark/paimon-spark-4.0,paimon-spark/paimon-spark-4.1 \
+    -am
+)
+```
+
+Investigate warnings and skipped modules. A successful compiler exit does not,
+by itself, establish that the candidate is suitable for release. The
+`-DskipTests` commands above do not by themselves satisfy the requirements for
+a binding `+1`. A binding voter must compile the signed source package and test
+the result on their own platform; run an appropriate test or smoke test against
+the artifacts produced by that source build and report the exact scope.
+
+## Verify Java staging repositories
+
+Use all three exact `orgapachepaimon-XXXX` staging URLs from the vote email.
+Do not resolve artifacts from Maven Central or a local cache when testing the
+candidate.
+
+For every staging repository:
+
+- Confirm that its status is closed, so the candidate cannot change during the
+  vote.
+- Confirm that the version is `PAIMON_VERSION`, never a snapshot.
+- Confirm that each published module has its POM, main artifact, source JAR,
+  Javadoc JAR, detached signatures, and checksums where applicable.
+- Inspect shaded and bundled JARs for the required `META-INF/LICENSE` and
+  `META-INF/NOTICE` content and for unexpected duplicate or bundled
+  dependencies.
+- Resolve representative artifacts in a clean Maven repository and run a small
+  consumer smoke test.
+
+Confirm the lane boundaries:
+
+| Lane | Must contain |
+| --- | --- |
+| JDK 8 | Default Java 8 artifacts, Flink 1.x, and Spark 3.x |
+| JDK 11 | Flink 2.x, `paimon-flink2-common`, and `paimon-iceberg` |
+| JDK 17 | Spark 4.x and its Scala 2.13 common artifacts |
+
+Check representative class-file targets. Java class major versions are 52 for
+Java 8, 55 for Java 11, and 61 for Java 17:
+
+```shell
+JAR_FILE="/path/to/a/representative.jar"
+CLASS_NAME="$(jar tf "${JAR_FILE}" | grep '\.class$' | grep -v 'module-info' | 
head -1 | sed 's#/#.#g;s#\.class$##')"
+javap -verbose -classpath "${JAR_FILE}" "${CLASS_NAME}" | grep 'major version'
+```
+
+Run this against at least one representative JAR from each lane. Also check
+that Java 8 artifacts can be loaded by a JDK 8 runtime; class-file inspection
+alone does not detect every use of a newer JDK API.
+
+## Build and test PyPaimon from source
+
+First build the official PyPaimon source candidate in an isolated environment:
+
+```shell
+python3 -m venv pypaimon-rc-venv
+. pypaimon-rc-venv/bin/activate
+python -m pip install --upgrade pip setuptools wheel
+
+cd "pypaimon-${PYPAIMON_VERSION}"
+python setup.py sdist bdist_wheel
+python -m pip install "dist/pypaimon-${PYPAIMON_VERSION}"*.whl
+python -c "import pypaimon; print('PyPaimon import OK')"
+cd ..
+```
+
+Inspect the generated sdist and wheel. They must contain the expected Python
+sources, `LICENSE`, `NOTICE`, README, and package metadata, with no tests,
+caches, credentials, or unrelated build output.
+
+This guide covers a combined Paimon and PyPaimon release. The complete Python
+test suite for the PyPaimon candidate is supplied in the signed Paimon source
+archive; the PyPaimon source distribution must not be used by itself for an
+independent release. Run the tests from
+`paimon-PAIMON_VERSION/paimon-python` on as many supported Python versions as
+your environment allows. The project CI selects Python 3.6, 3.7, 3.10, and
+3.11 for its main compatibility lanes:
+
+```shell
+(
+  cd "paimon-${PAIMON_VERSION}/paimon-python"
+  python -m pip install -r dev/requirements.txt
+  python -m pip install -r dev/requirements-dev.txt
+  python -m pytest pypaimon/tests -v
+)
+```
+
+Some optional integration tests need additional services or packages. Record
+which tests ran, skipped, or failed.
+
+## Verify the TestPyPI candidate
+
+Install the exact RC version in a new environment:
+
+```shell
+python3 -m venv pypaimon-testpypi-venv
+. pypaimon-testpypi-venv/bin/activate
+python -m pip install \
+  --index-url https://test.pypi.org/simple/ \
+  --extra-index-url https://pypi.org/simple/ \
+  "pypaimon==${PYPAIMON_VERSION}rc${RC_NUMBER}"
+python -c "import pypaimon; print('TestPyPI package OK')"
+```
+
+Check the available files, package metadata, Python requirement, dependencies,
+license files, and a representative read/write operation. The RC suffix is the
+only intended version change from the final PyPaimon source candidate; any
+source-code difference is a release blocker.
+
+## Verify GitHub Actions evidence
+
+Open the workflow run linked in the vote email and confirm:
+
+- it was triggered from the signed `RC_TAG`;
+- `head_sha` equals the announced commit;
+- the common validation job confirmed that `RC_TAG` is exactly
+  `release-PAIMON_VERSION-rcRC_NUMBER` and that `PAIMON_VERSION` equals the
+  root Maven `project.version`;
+- JDK 8, JDK 11, JDK 17, Python packaging, and Python publishing jobs all
+  succeeded;
+- the logs show the expected JDK and Python versions;
+- each Java package artifact contains the expected manifest and SHA-512
+  checksums for its lane;
+- the TestPyPI version equals the version in the vote email;
+- no later rerun silently replaced a failed lane with artifacts from another
+  commit.
+
+GitHub Actions evidence is useful for reviewing the complete platform matrix,
+but voters should still perform independent source, signature, build, and smoke
+checks.
+
+## Report your vote
+
+Reply to the vote thread with `+1`, `0`, or `-1`, state whether your vote is
+binding, and list the checks you completed.
+
+```text
++1 (binding/non-binding)
+
+Verified:
+- signed RC tag and announced commit SHA
+- Paimon and PyPaimon GPG signatures and SHA-512 checksums
+- LICENSE, NOTICE, source-only archive contents, and release versions
+- Paimon source build on <OS/ARCH>, JDK <8/11/17>, Maven <version>
+- Java staging repositories and representative class-file targets
+- PyPaimon source build/tests on Python <versions>
+- TestPyPI installation and smoke test
+- GitHub Actions run provenance, Java package manifests, and Python artifacts
+```
+
+For a `-1`, describe the failure precisely enough for the RM to reproduce it.
+See [Creating a Release](./creating-a-release.md) for the RM workflow.
diff --git a/docs/sidebars.js b/docs/sidebars.js
index ed0db484e7..56a9c3e211 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -318,6 +318,8 @@ const sidebars = {
     },
     "items": [
       "project/download",
+      "project/creating-a-release",
+      "project/verifying-a-release-candidate",
       "project/contributing",
       "project/committer",
       "project/security"

Reply via email to