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

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


The following commit(s) were added to refs/heads/release-2.0 by this push:
     new c988b0850f [release] Build Java repository images in Actions
c988b0850f is described below

commit c988b0850f805170f9fb4d1e1da3d8acc17c3a2b
Author: JingsongLi <[email protected]>
AuthorDate: Fri Jul 31 23:01:31 2026 +0800

    [release] Build Java repository images in Actions
---
 .github/workflows/release-java.yml                 | 332 ++++++++++++++++++---
 docs/docs/project/creating-a-release.md            | 147 +++++----
 docs/docs/project/verifying-a-release-candidate.md |  96 +++---
 tools/releasing/deploy_maven_repository.sh         | 132 ++++++++
 tools/releasing/deploy_staging_jars.sh             |  47 ---
 tools/releasing/deploy_staging_jars_for_jdk11.sh   |  53 ----
 tools/releasing/deploy_staging_jars_for_jdk17.sh   |  52 ----
 tools/releasing/sign_maven_repository.sh           |  96 ++++++
 8 files changed, 659 insertions(+), 296 deletions(-)

diff --git a/.github/workflows/release-java.yml 
b/.github/workflows/release-java.yml
index ae4d71c49a..66dbdbf9ec 100644
--- a/.github/workflows/release-java.yml
+++ b/.github/workflows/release-java.yml
@@ -27,6 +27,7 @@ concurrency:
   cancel-in-progress: false
 
 permissions:
+  actions: read
   contents: read
 
 jobs:
@@ -75,6 +76,10 @@ jobs:
           rc_number="${GITHUB_REF_NAME##*-rc}"
           expected_tag="release-${release_version}-rc${rc_number}"
 
+          if [[ ! "${rc_number}" =~ ^[0-9]+$ ]]; then
+            echo "RC number must be numeric: ${rc_number}" >&2
+            exit 1
+          fi
           if [[ "${GITHUB_REF_NAME}" != "${expected_tag}" ]]; then
             echo "Tag ${GITHUB_REF_NAME} does not match Maven version 
${release_version}" >&2
             exit 1
@@ -130,44 +135,68 @@ jobs:
         env:
           LANE: ${{ matrix.lane }}
           MAVEN_OPTS: -Xmx4096m
+          RELEASE_VERSION: ${{ steps.release.outputs.version }}
         run: |
           set -euo pipefail
 
           mkdir -p release-java
+          repository="${RUNNER_TEMP}/paimon-${LANE}-repository"
+          alt_repository="local::default::file://${repository}"
+          effective_pom="${RUNNER_TEMP}/paimon-${LANE}-effective-pom.xml"
+          expected_projects="release-java/${LANE}-expected-projects.txt"
           log="release-java/${LANE}.log"
-          install_log="release-java/${LANE}-install.log"
+
+          capture_expected_projects() {
+            mvn -q -ntp -B help:effective-pom \
+              -Doutput="${effective_pom}" "$@"
+          }
+
           case "${LANE}" in
             jdk8)
-              mvn clean install -ntp -B \
+              capture_expected_projects \
+                -Papache-release,docs-and-source,spark3,flink1
+              mvn clean deploy -ntp -B \
                 -Papache-release,docs-and-source,spark3,flink1 \
                 -DskipTests -Dgpg.skip=true -Dstyle.color=never \
-                2>&1 | tee "${log}" "${install_log}"
+                -DdeployAtEnd=true \
+                -DaltDeploymentRepository="${alt_repository}" \
+                2>&1 | tee "${log}"
               ;;
             jdk11)
+              capture_expected_projects \
+                -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
               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 \
+              mvn deploy -ntp -B \
                 -Papache-release,docs-and-source,flink2 \
                 -DskipTests -Dgpg.skip=true -Dstyle.color=never \
+                -DdeployAtEnd=true \
+                -DaltDeploymentRepository="${alt_repository}" \
                 -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}"
+                2>&1 | tee -a "${log}"
               ;;
             jdk17)
+              capture_expected_projects \
+                -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
               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 \
+              mvn deploy -ntp -B \
                 -Papache-release,docs-and-source,spark4 \
                 -DskipTests -Dgpg.skip=true -Dstyle.color=never \
+                -DdeployAtEnd=true \
+                -DaltDeploymentRepository="${alt_repository}" \
                 -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}"
+                2>&1 | tee -a "${log}"
               ;;
             *)
               echo "Unknown Java release lane: ${LANE}" >&2
@@ -175,40 +204,134 @@ jobs:
               ;;
           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
+          python3 - \
+            "${effective_pom}" "${expected_projects}" \
+            "${RELEASE_VERSION}" <<'PY'
+          import sys
+          import xml.etree.ElementTree as ET
+
+          effective_pom, output, release_version = sys.argv[1:]
+          root = ET.parse(effective_pom).getroot()
+          projects = list(root) if root.tag == "projects" else [root]
+          namespace = "{http://maven.apache.org/POM/4.0.0}";
+          expected = []
+          for project in projects:
+              def required(name):
+                  element = project.find(namespace + name)
+                  if element is None or not element.text:
+                      raise ValueError("Effective POM is missing " + name)
+                  return element.text.strip()
+
+              deploy_skipped = False
+              plugins = project.findall(
+                  "./%sbuild/%splugins/%splugin"
+                  % (namespace, namespace, namespace)
+              )
+              for plugin in plugins:
+                  artifact = plugin.find(namespace + "artifactId")
+                  if artifact is None or artifact.text != 
"maven-deploy-plugin":
+                      continue
+                  skip = plugin.find(
+                      "./%sconfiguration/%sskip" % (namespace, namespace)
+                  )
+                  if (
+                      skip is not None
+                      and skip.text
+                      and skip.text.strip().lower() == "true"
+                  ):
+                      deploy_skipped = True
+                      break
+              if deploy_skipped:
+                  continue
+
+              group_id = required("groupId")
+              artifact_id = required("artifactId")
+              version = required("version")
+              packaging_element = project.find(namespace + "packaging")
+              packaging = (
+                  packaging_element.text.strip()
+                  if packaging_element is not None and packaging_element.text
+                  else "jar"
+              )
+              if group_id != "org.apache.paimon":
+                  raise ValueError("Unexpected release groupId: " + group_id)
+              if version != release_version:
+                  raise ValueError(
+                      "Unexpected project version for %s: %s"
+                      % (artifact_id, version)
+                  )
+              if packaging not in ("jar", "pom"):
+                  raise ValueError(
+                      "Unsupported release packaging for %s: %s"
+                      % (artifact_id, packaging)
+                  )
+              expected.append((group_id, artifact_id, version, packaging))
+
+          if not expected:
+              raise ValueError("Effective POM contains no release projects")
+          if len(expected) != len(set(expected)):
+              raise ValueError("Effective POM contains duplicate projects")
+          with open(output, "w", encoding="utf-8") as result:
+              for project in sorted(expected):
+                  result.write("\t".join(project) + "\n")
+          PY
+
+          while IFS=$'\t' read -r group_id artifact_id version packaging; do
+            
artifact_directory="${repository}/${group_id//.//}/${artifact_id}/${version}"
+            artifact_base="${artifact_directory}/${artifact_id}-${version}"
+            if [[ ! -f "${artifact_base}.pom" ]]; then
+              echo "Missing release POM: ${artifact_base}.pom" >&2
               exit 1
             fi
-            if [[ ! -f "${artifact}" ]]; then
-              echo "Installed artifact does not exist: ${artifact}" >&2
-              exit 1
+            if [[ "${packaging}" == "jar" ]]; then
+              for classifier in '' '-sources' '-javadoc'; do
+                if [[ ! -f "${artifact_base}${classifier}.jar" ]]; then
+                  echo "Missing release JAR: 
${artifact_base}${classifier}.jar" >&2
+                  exit 1
+                fi
+              done
             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
+          done < "${expected_projects}"
+
+          unexpected_artifact="$(
+            find "${repository}" -type f \
+              \( -name '*.jar' -o -name '*.pom' \) \
+              ! -path "*/${RELEASE_VERSION}/*" -print -quit
+          )"
+          if [[ -n "${unexpected_artifact}" ]]; then
+            echo "Artifact is outside the release version: 
${unexpected_artifact}" >&2
             exit 1
           fi
 
-          while IFS= read -r artifact; do
-            sha512sum "${artifact}"
-          done < "${files}" \
-            > "release-java/${LANE}-sha512.txt"
+          files="${GITHUB_WORKSPACE}/release-java/${LANE}-files.txt"
+          (
+            cd "${repository}"
+            find . -type f | LC_ALL=C sort > "${files}"
+          )
+          if [[ ! -s "${files}" ]] ||
+             ! grep -q '\.pom$' "${files}" ||
+             ! grep -q '\.jar$' "${files}"; then
+            echo "Incomplete Maven repository image for ${LANE}" >&2
+            exit 1
+          fi
+          if grep -q '\.asc$' "${files}"; then
+            echo "Unsigned repository image unexpectedly contains signatures" 
>&2
+            exit 1
+          fi
+
+          (
+            cd "${repository}"
+            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}"
+          tar -czf "release-java/${LANE}-repository.tar.gz" \
+            -C "${repository}" .
           (
             cd release-java
-            sha512sum "${LANE}-packages.tar.gz" \
-              > "${LANE}-packages.tar.gz.sha512"
+            sha512sum "${LANE}-repository.tar.gz" \
+              > "${LANE}-repository.tar.gz.sha512"
           )
 
           {
@@ -219,12 +342,151 @@ jobs:
             echo "commit=${GITHUB_SHA}"
             echo "signed=false"
             echo "staged=false"
-            echo "artifact_source=maven-install-log"
+            echo "artifact_source=maven-file-repository"
           } > "release-java/${LANE}-manifest.txt"
 
-      - name: Upload Java packages
+      - name: Upload Java repository lane
         uses: actions/upload-artifact@v5
         with:
-          name: java-package-${{ matrix.lane }}
+          name: java-repository-${{ matrix.lane }}
           path: release-java/
           if-no-files-found: error
+          retention-days: 1
+
+  combine:
+    name: Combine Java repository
+    needs: package
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/download-artifact@v5
+        with:
+          pattern: java-repository-*
+          path: release-java/lanes
+          merge-multiple: true
+
+      - name: Merge repository lanes
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          mkdir -p release-java/final/evidence
+          combined="${RUNNER_TEMP}/paimon-maven-repository"
+          mkdir -p "${combined}"
+
+          for lane in jdk8 jdk11 jdk17; do
+            archive="release-java/lanes/${lane}-repository.tar.gz"
+            checksum="release-java/lanes/${lane}-repository.tar.gz.sha512"
+            test -f "${archive}"
+            test -f "${checksum}"
+            (
+              cd release-java/lanes
+              sha512sum -c "${lane}-repository.tar.gz.sha512"
+            )
+
+            candidate="${RUNNER_TEMP}/paimon-${lane}-candidate"
+            mkdir -p "${candidate}"
+            tar -xzf "${archive}" -C "${candidate}"
+            while IFS= read -r source; do
+              relative="${source#"${candidate}/"}"
+              target="${combined}/${relative}"
+              if [[ -f "${target}" ]]; then
+                if ! cmp -s "${source}" "${target}"; then
+                  echo "Conflicting repository file from ${lane}: ${relative}" 
>&2
+                  exit 1
+                fi
+              else
+                mkdir -p "$(dirname "${target}")"
+                cp -p "${source}" "${target}"
+              fi
+            done < <(find "${candidate}" -type f | LC_ALL=C sort)
+
+            cp "release-java/lanes/${lane}-manifest.txt" \
+              "release-java/final/evidence/"
+            cp "release-java/lanes/${lane}.log" \
+              "release-java/final/evidence/"
+            cp "release-java/lanes/${lane}-expected-projects.txt" \
+              "release-java/final/evidence/"
+          done
+
+          version="$(
+            sed -n 's/^version=//p' \
+              release-java/lanes/jdk8-manifest.txt
+          )"
+          rc_number="$(
+            sed -n 's/^rc=//p' \
+              release-java/lanes/jdk8-manifest.txt
+          )"
+          if [[ -z "${version}" ]]; then
+            echo "Missing release version from JDK 8 manifest" >&2
+            exit 1
+          fi
+          if [[ -z "${rc_number}" ]]; then
+            echo "Missing RC number from JDK 8 manifest" >&2
+            exit 1
+          fi
+          for lane in jdk8 jdk11 jdk17; do
+            expected_jdk="${lane#jdk}"
+            grep -qx "lane=${lane}" \
+              "release-java/lanes/${lane}-manifest.txt"
+            grep -qx "jdk=${expected_jdk}" \
+              "release-java/lanes/${lane}-manifest.txt"
+            grep -qx "version=${version}" \
+              "release-java/lanes/${lane}-manifest.txt"
+            grep -qx "rc=${rc_number}" \
+              "release-java/lanes/${lane}-manifest.txt"
+            grep -qx "commit=${GITHUB_SHA}" \
+              "release-java/lanes/${lane}-manifest.txt"
+            grep -qx 'signed=false' \
+              "release-java/lanes/${lane}-manifest.txt"
+            grep -qx 'staged=false' \
+              "release-java/lanes/${lane}-manifest.txt"
+          done
+
+          
files="${GITHUB_WORKSPACE}/release-java/final/paimon-maven-repository-files.txt"
+          (
+            cd "${combined}"
+            find . -type f | LC_ALL=C sort > "${files}"
+          )
+          if [[ ! -s "${files}" ]] ||
+             ! grep -q '\.pom$' "${files}" ||
+             ! grep -q '\.jar$' "${files}"; then
+            echo "Combined Maven repository image is incomplete" >&2
+            exit 1
+          fi
+          if grep -q '\.asc$' "${files}"; then
+            echo "Combined repository image is unexpectedly signed" >&2
+            exit 1
+          fi
+
+          (
+            cd "${combined}"
+            while IFS= read -r artifact; do
+              sha512sum "${artifact}"
+            done < "${files}"
+          ) > release-java/final/paimon-maven-repository-sha512.txt
+
+          archive="paimon-${version}-maven-repository.tar.gz"
+          tar -czf "release-java/final/${archive}" \
+            -C "${combined}" .
+          (
+            cd release-java/final
+            sha512sum "${archive}" > "${archive}.sha512"
+          )
+
+          {
+            echo "version=${version}"
+            echo "rc=${rc_number}"
+            echo "tag=${GITHUB_REF_NAME}"
+            echo "commit=${GITHUB_SHA}"
+            echo "lanes=jdk8,jdk11,jdk17"
+            echo "signed=false"
+            echo "staged=false"
+            echo "artifact_source=combined-maven-file-repository"
+          } > release-java/final/paimon-maven-repository-manifest.txt
+
+      - name: Upload combined Java repository
+        uses: actions/upload-artifact@v5
+        with:
+          name: java-release-repository
+          path: release-java/final/
+          if-no-files-found: error
diff --git a/docs/docs/project/creating-a-release.md 
b/docs/docs/project/creating-a-release.md
index 71a48ee702..9e501d4803 100644
--- a/docs/docs/project/creating-a-release.md
+++ b/docs/docs/project/creating-a-release.md
@@ -73,18 +73,21 @@ compiler target is not a substitute for running the JDK 8 
and JDK 11 lanes.
 
 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.
+to package the JDK 8, JDK 11, and JDK 17 Java lanes and PyPaimon from every
+signed RC tag. The Java lanes are merged into one unsigned Maven repository
+image. The RM downloads that image, signs it, and stages it in Nexus. The RM
+also 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 |
+| Java 8 | Use Temurin 8 to deploy the default reactor with Spark 3 and Flink 
1 into a local Maven repository image |
+| Java 11 | Use Temurin 11 to deploy Flink 2 and Iceberg into a local Maven 
repository image |
+| Java 17 | Use Temurin 17 to deploy Spark 4 into a local Maven repository 
image |
+| Java repository | Require every deploy-enabled effective-POM project and its 
POM, main JAR, source JAR, and Javadoc JAR; merge all three lanes; reject 
conflicting coordinates; then upload the complete unsigned Maven repository 
image, checksums, manifests, and logs |
 | 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 or a final tag to PyPI after 
Python packaging passes, without waiting for Java packaging |
 
@@ -95,10 +98,11 @@ scope. The latter includes transitive dependencies. Any 
remaining
 release blocker.
 
 The Java jobs run independently of the common validation and Python jobs. They
-use `-Dgpg.skip=true` and never receive Nexus credentials or a GPG private key.
-Their artifacts are optional build evidence, not a prerequisite for the RM's
-local Java staging and not the Maven staging repositories used for the vote.
-The Python RC job uses the
+use `-Dgpg.skip=true`, deploy only to runner-local file repositories, and never
+receive Nexus credentials or a GPG private key. The combined repository image
+contains POMs, main artifacts, source JARs, Javadoc JARs, and Maven-generated
+checksums. It is the input to the RM's local signing and Nexus
+staging steps, not itself an ASF release. The Python RC job uses the
 `TEST_PYPI_API_TOKEN` repository Actions secret to publish
 `PAIMON_VERSIONrcRC_NUMBER` to TestPyPI. The final job uses the
 `PYPI_API_TOKEN` repository Actions secret to publish to PyPI. The release
@@ -118,9 +122,11 @@ Before managing the first release:
 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,
+5. Record Paimon's Apache Nexus staging profile ID. The local repository upload
+   script requires it as `STAGING_PROFILE_ID`.
+6. Confirm access to the ASF distribution SVN repository, Apache Nexus,
    TestPyPI, and PyPI.
-6. Verify that the repository Actions secrets `TEST_PYPI_API_TOKEN` and
+7. Verify that the repository Actions secrets `TEST_PYPI_API_TOKEN` and
    `PYPI_API_TOKEN` are configured without printing their values in an Actions
    log.
 
@@ -221,20 +227,19 @@ git push origin \
   "refs/tags/${RC_REF}:refs/tags/${RC_REF}"
 ```
 
-Pushing the signed tag starts the Release workflow. Java packaging runs
-independently. Python publishing starts as soon as common validation and Python
-packaging succeed; it does not wait for the Java packaging jobs. Record:
+Pushing the signed tag starts all three Java packaging lanes from the exact 
same
+commit. Python publishing starts as soon as common validation and Python
+packaging succeed; it does not wait for Java packaging. Record:
 
 - the workflow run URL and `head_sha`;
-- any available JDK 8, 11, and 17 package artifacts, manifests, and SHA-512
-  checksums which the RM chooses to use as additional build evidence;
+- the `java-release-repository` artifact name, manifest, and SHA-512 checksum;
 - the TestPyPI project/version URL.
 
-CI status is not an automatic gate for local Java staging or the release vote.
-The RM evaluates the available CI results together with the local release
-checks and decides whether the candidate is ready.
+Unrelated CI status is not an automatic release gate; the RM decides whether a
+failure blocks the candidate. The three Java packaging lanes and repository
+merge must succeed before their repository image can be signed and staged.
 
-## Stage the Java convenience artifacts locally
+## Sign and 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.
@@ -249,34 +254,76 @@ gpg --list-secret-keys --keyid-format LONG
 mvn -q -DforceStdout help:evaluate -Dexpression=project.version
 ```
 
-If the RM chooses to use the Java workflow artifacts as additional evidence,
-download the available packages, verify each `*-packages.tar.gz.sha512` file,
-and inspect each manifest and artifact inventory. Confirm that the lane,
-version, commit, JDK, and module scope match the signed RC tag. Do not compare
-their individual JAR checksums with the locally built and signed JARs. Archive
-entry order and other build metadata can make independently built JARs differ
-at the byte level.
+Download the combined repository image from the recorded workflow run. With
+GitHub CLI, set `RUN_ID` to the numeric run ID from the workflow URL:
 
-Run each existing staging script under its required JDK. Confirm the output of
-`java -version` and `mvn -version` before every command:
+```shell
+RUN_ID="<GITHUB_WORKFLOW_RUN_ID>"
+JAVA_REPOSITORY_ARCHIVE="paimon-${PAIMON_VERSION}-maven-repository.tar.gz"
+
+gh run download "${RUN_ID}" \
+  --name java-release-repository \
+  --dir java-release-repository
+```
+
+Verify the archive and every file in the repository image before signing it:
+
+```shell
+(
+  cd java-release-repository
+  sha512sum -c "${JAVA_REPOSITORY_ARCHIVE}.sha512"
+)
+
+mkdir paimon-maven-repository
+tar -xzf "java-release-repository/${JAVA_REPOSITORY_ARCHIVE}" \
+  -C paimon-maven-repository
+(
+  cd paimon-maven-repository
+  sha512sum -c \
+    ../java-release-repository/paimon-maven-repository-sha512.txt
+)
+
+grep -Fx "version=${PAIMON_VERSION}" \
+  java-release-repository/paimon-maven-repository-manifest.txt
+grep -Fx "tag=${RC_REF}" \
+  java-release-repository/paimon-maven-repository-manifest.txt
+grep -Fx "commit=$(git rev-parse HEAD)" \
+  java-release-repository/paimon-maven-repository-manifest.txt
+```
+
+On macOS, replace `sha512sum` with `shasum -a 512`. Sign every JAR and POM in
+the extracted repository with the RM's local key. `GPG_KEY_ID` is optional when
+the default GPG key is the release key:
 
 ```shell
-# JDK 8: default reactor, Flink 1.x, and Spark 3.x
-./tools/releasing/deploy_staging_jars.sh
+REPOSITORY_DIRECTORY="${PWD}/paimon-maven-repository" \
+GPG_KEY_ID="<RELEASE_GPG_KEY_ID>" \
+  ./tools/releasing/sign_maven_repository.sh
+```
 
-# JDK 11: Flink 2.x and Iceberg
-./tools/releasing/deploy_staging_jars_for_jdk11.sh
+Upload the complete signed image with the Nexus Staging Maven Plugin:
 
-# JDK 17: Spark 4.x
-./tools/releasing/deploy_staging_jars_for_jdk17.sh
+```shell
+REPOSITORY_DIRECTORY="${PWD}/paimon-maven-repository" \
+STAGING_PROFILE_ID="<PAIMON_STAGING_PROFILE_ID>" \
+  ./tools/releasing/deploy_maven_repository.sh
 ```
 
-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.
+The upload plugin creates one staging repository and closes it after a complete
+upload. The script explicitly disables automatic release. On a transport
+failure it drops the partial staging repository, so a retry starts clean 
instead
+of splitting coordinates across repositories. On a close-rule failure the
+script keeps the repository for inspection. Record the single
+`orgapachepaimon-XXXX` repository ID. If closing fails, inspect and drop that
+repository, correct the cause, and upload the clean signed image again. Start
+the vote only after one staging repository is closed successfully. Do not
+release it before the vote passes.
+
+If signing is interrupted, discard the extracted repository and extract the
+verified archive again before retrying. If uploading is interrupted, first
+confirm in Nexus that the plugin dropped the partial staging repository; drop
+it manually if necessary, then rerun the upload with the unchanged signed
+repository image.
 
 ## Stage the source candidates
 
@@ -376,10 +423,8 @@ GitHub Actions release run:
 KEYS:
 https://downloads.apache.org/paimon/KEYS
 
-Closed Java staging repositories:
-<JDK_8_NEXUS_URL>
-<JDK_11_NEXUS_URL>
-<JDK_17_NEXUS_URL>
+Closed Java staging repository:
+<JAVA_NEXUS_URL>
 
 PyPaimon RC:
 https://test.pypi.org/project/pypaimon/${PAIMON_VERSION}rc${RC_NUMBER}/
@@ -398,7 +443,7 @@ After the deadline, tally binding and non-binding votes 
separately and send
 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.
+2. Drop the 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.
@@ -438,10 +483,10 @@ svn mv -m "Release PyPaimon ${PAIMON_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.
+1. In Nexus, confirm that the recorded Java staging repository is still closed
+   and has the exact artifact tree approved by the vote.
+2. Release that exact closed repository to Maven Central. Do not upload or
+   rebuild the Java artifacts again.
 3. Confirm that the final tag's PyPI publish job builds
    `pypaimon==PAIMON_VERSION` from the approved tag commit and does not change
    project source.
diff --git a/docs/docs/project/verifying-a-release-candidate.md 
b/docs/docs/project/verifying-a-release-candidate.md
index 9853d9cbea..6a4196b5ef 100644
--- a/docs/docs/project/verifying-a-release-candidate.md
+++ b/docs/docs/project/verifying-a-release-candidate.md
@@ -59,7 +59,7 @@ curl -O 
"https://dist.apache.org/repos/dist/dev/paimon/${PYPAIMON_RC_DIR}/${PYPA
 curl -O https://downloads.apache.org/paimon/KEYS
 ```
 
-Keep the workflow run URL, announced commit SHA, three Nexus staging URLs, and
+Keep the workflow run URL, announced commit SHA, Java Nexus staging URL, and
 TestPyPI URL beside these files. All of them must identify this RC.
 
 ## Verify signatures and checksums
@@ -154,23 +154,17 @@ 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.
+Compile the signed source archive and run an appropriate test or smoke test on
+at least one supported JDK. Record `java -version`, `mvn -version`, and the
+exact scope. Voters do not need to reproduce the complete CI matrix or run any
+full JDK test lane locally. The RM is responsible for reviewing the relevant CI
+results before starting the vote.
 
 ### 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:
+This lane covers the default reactor, Flink 1.x, and Spark 3.x. As an optional
+compatibility check, voters may run the packaging-equivalent build without
+reproducing the full JDK 8 test lane:
 
 ```shell
 (
@@ -182,18 +176,9 @@ used for staging and state explicitly that tests were 
skipped:
 
 ### 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:
+This lane covers Flink 2.x and Iceberg. As an optional compatibility check,
+voters may run the packaging-equivalent build without reproducing the full JDK
+11 test lane:
 
 ```shell
 (
@@ -206,18 +191,8 @@ The packaging-equivalent build is:
 
 ### 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:
+This lane covers Spark 4.x. As an optional compatibility check, voters may run
+the packaging-equivalent build without reproducing the full JDK 17 test lane:
 
 ```shell
 (
@@ -229,19 +204,18 @@ The packaging-equivalent build is:
 ```
 
 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.
+by itself, establish that the candidate is suitable for release. The optional
+`-DskipTests` commands do not by themselves satisfy the requirements for a
+binding `+1`; report the actual source build, test, or smoke-test scope you
+completed. This does not require covering every JDK lane.
 
-## Verify Java staging repositories
+## Verify the Java staging repository
 
-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
+Use the exact `orgapachepaimon-XXXX` staging URL from the vote email. Do not
+resolve artifacts from Maven Central or a local cache when testing the
 candidate.
 
-For every staging repository:
+For the staging repository:
 
 - Confirm that its status is closed, so the candidate cannot change during the
   vote.
@@ -334,7 +308,7 @@ 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
+## Review GitHub Actions evidence
 
 Open the workflow run linked in the vote email and confirm:
 
@@ -343,18 +317,24 @@ Open the workflow run linked in the vote email and 
confirm:
 - 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 JDK 8, JDK 11, and JDK 17 repository lanes completed successfully;
+- the combined Java repository artifact has the expected tag, commit,
+  manifest, and SHA-512 checksums and identifies all three lanes;
+- each lane's deploy-enabled expected-project inventory matches its POMs, main
+  JARs, source JARs, and Javadoc JARs in the combined repository;
+- the Python packaging and Python publishing job results are clearly recorded;
 - 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.
+The Java repository image is unsigned workflow output; verify that the closed
+Nexus repository contains the same artifacts plus the RM's signatures. The
+repository lanes package release artifacts; they are not full JDK test lanes.
+Voters do not need to require or reproduce a full test run for any JDK lane.
+The RM manually confirms that the relevant CI checks have passed before
+starting the vote; unrelated CI failures are assessed separately and are not
+an automatic release gate.
 
 ## Report your vote
 
@@ -368,8 +348,8 @@ 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
+- Paimon source build/test scope on <OS/ARCH>, JDK <version>, Maven <version>
+- Java staging repository 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
diff --git a/tools/releasing/deploy_maven_repository.sh 
b/tools/releasing/deploy_maven_repository.sh
new file mode 100755
index 0000000000..6b1ab81ad3
--- /dev/null
+++ b/tools/releasing/deploy_maven_repository.sh
@@ -0,0 +1,132 @@
+#!/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.
+#
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+MVN=${MVN:-mvn}
+GPG=${GPG:-gpg}
+REPOSITORY_DIRECTORY=${REPOSITORY_DIRECTORY:-}
+STAGING_PROFILE_ID=${STAGING_PROFILE_ID:-}
+NEXUS_URL=${NEXUS_URL:-https://repository.apache.org/}
+SERVER_ID=${SERVER_ID:-apache.releases.https}
+NEXUS_STAGING_PLUGIN_VERSION=${NEXUS_STAGING_PLUGIN_VERSION:-1.7.0}
+CUSTOM_OPTIONS=${CUSTOM_OPTIONS:-}
+
+if [ -z "${REPOSITORY_DIRECTORY}" ]; then
+    echo "REPOSITORY_DIRECTORY was not set" >&2
+    exit 1
+fi
+if [ ! -d "${REPOSITORY_DIRECTORY}" ]; then
+    echo "Maven repository does not exist: ${REPOSITORY_DIRECTORY}" >&2
+    exit 1
+fi
+if [ -z "${STAGING_PROFILE_ID}" ]; then
+    echo "STAGING_PROFILE_ID was not set" >&2
+    exit 1
+fi
+
+if command -v md5sum >/dev/null 2>&1; then
+    MD5_COMMAND=md5sum
+else
+    MD5_COMMAND=md5
+fi
+if command -v sha1sum >/dev/null 2>&1; then
+    SHA1_COMMAND=sha1sum
+else
+    SHA1_COMMAND=shasum
+fi
+
+md5_value() {
+    file=$1
+    if [ "${MD5_COMMAND}" = "md5sum" ]; then
+        md5sum "${file}" | awk '{print $1}'
+    else
+        md5 -q "${file}"
+    fi
+}
+
+sha1_value() {
+    file=$1
+    if [ "${SHA1_COMMAND}" = "sha1sum" ]; then
+        sha1sum "${file}" | awk '{print $1}'
+    else
+        shasum -a 1 "${file}" | awk '{print $1}'
+    fi
+}
+
+verify_checksums() {
+    file=$1
+    expected_md5=$(awk 'NR == 1 {print $1}' "${file}.md5")
+    expected_sha1=$(awk 'NR == 1 {print $1}' "${file}.sha1")
+    actual_md5=$(md5_value "${file}")
+    actual_sha1=$(sha1_value "${file}")
+    if [ "${actual_md5}" != "${expected_md5}" ]; then
+        echo "MD5 checksum does not match: ${file}" >&2
+        exit 1
+    fi
+    if [ "${actual_sha1}" != "${expected_sha1}" ]; then
+        echo "SHA-1 checksum does not match: ${file}" >&2
+        exit 1
+    fi
+}
+
+artifact_count=0
+while IFS= read -r artifact; do
+    if [ ! -f "${artifact}.asc" ]; then
+        echo "Maven artifact is not signed: ${artifact}" >&2
+        exit 1
+    fi
+    if [ ! -f "${artifact}.md5" ] || [ ! -f "${artifact}.sha1" ]; then
+        echo "Maven artifact is missing a checksum: ${artifact}" >&2
+        exit 1
+    fi
+    if [ ! -f "${artifact}.asc.md5" ] || [ ! -f "${artifact}.asc.sha1" ]; then
+        echo "Maven signature is missing a checksum: ${artifact}.asc" >&2
+        exit 1
+    fi
+    "${GPG}" --verify "${artifact}.asc" "${artifact}"
+    verify_checksums "${artifact}"
+    verify_checksums "${artifact}.asc"
+    artifact_count=$((artifact_count + 1))
+done < <(
+    find "${REPOSITORY_DIRECTORY}" -type f \
+        \( -name '*.jar' -o -name '*.pom' \) |
+        LC_ALL=C sort
+)
+
+if [ "${artifact_count}" -eq 0 ]; then
+    echo "Maven repository contains no JAR or POM artifacts" >&2
+    exit 1
+fi
+
+echo "Verified ${artifact_count} signed Maven artifacts."
+echo "Uploading the signed Maven repository image to ${NEXUS_URL}"
+${MVN} -ntp \
+    
org.sonatype.plugins:nexus-staging-maven-plugin:${NEXUS_STAGING_PLUGIN_VERSION}:deploy-staged-repository
 \
+    -DrepositoryDirectory="${REPOSITORY_DIRECTORY}" \
+    -DnexusUrl="${NEXUS_URL}" \
+    -DserverId="${SERVER_ID}" \
+    -DstagingProfileId="${STAGING_PROFILE_ID}" \
+    ${CUSTOM_OPTIONS} \
+    -DautoReleaseAfterClose=false \
+    -DkeepStagingRepositoryOnFailure=false \
+    -DkeepStagingRepositoryOnCloseRuleFailure=true
diff --git a/tools/releasing/deploy_staging_jars.sh 
b/tools/releasing/deploy_staging_jars.sh
deleted file mode 100755
index bcfde30624..0000000000
--- a/tools/releasing/deploy_staging_jars.sh
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/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.
-#
-
-##
-## Variables with defaults (if not overwritten by environment)
-##
-MVN=${MVN:-mvn}
-CUSTOM_OPTIONS=${CUSTOM_OPTIONS:-}
-
-# fail immediately
-set -o errexit
-set -o nounset
-
-CURR_DIR=`pwd`
-BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
-PROJECT_ROOT="${BASE_DIR}/../../"
-
-# Sanity check to ensure that resolved paths are valid; a LICENSE file should 
always exist in project root
-if [ ! -f ${PROJECT_ROOT}/LICENSE ]; then
-    echo "Project root path ${PROJECT_ROOT} is not valid; script may be in the 
wrong directory."
-    exit 1
-fi
-
-###########################
-
-cd ${PROJECT_ROOT}
-
-echo "Deploying to repository.apache.org"
-${MVN} clean deploy -ntp -Papache-release,docs-and-source,spark3,flink1 
-DskipTests -DretryFailedDeploymentCount=10 $CUSTOM_OPTIONS
-
-cd ${CURR_DIR}
diff --git a/tools/releasing/deploy_staging_jars_for_jdk11.sh 
b/tools/releasing/deploy_staging_jars_for_jdk11.sh
deleted file mode 100755
index 7c28eb7562..0000000000
--- a/tools/releasing/deploy_staging_jars_for_jdk11.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/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.
-#
-
-##
-## Variables with defaults (if not overwritten by environment)
-##
-MVN=${MVN:-mvn}
-CUSTOM_OPTIONS=${CUSTOM_OPTIONS:-}
-
-# fail immediately
-set -o errexit
-set -o nounset
-
-CURR_DIR=`pwd`
-BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
-PROJECT_ROOT="${BASE_DIR}/../../"
-
-# Sanity check to ensure that resolved paths are valid; a LICENSE file should 
always exist in project root
-if [ ! -f ${PROJECT_ROOT}/LICENSE ]; then
-    echo "Project root path ${PROJECT_ROOT} is not valid; script may be in the 
wrong directory."
-    exit 1
-fi
-
-###########################
-
-cd ${PROJECT_ROOT}
-
-echo "Building flink2 and iceberg modules"
-${MVN} clean install -ntp -Pdocs-and-source,flink2 -DskipTests \
--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 $CUSTOM_OPTIONS
-
-echo "Deploying flink2 and iceberg modules to repository.apache.org"
-${MVN} deploy -ntp -Papache-release,docs-and-source,flink2 -DskipTests 
-DretryFailedDeploymentCount=10 \
--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
 $CUSTOM_OPTIONS
-
-cd ${CURR_DIR}
diff --git a/tools/releasing/deploy_staging_jars_for_jdk17.sh 
b/tools/releasing/deploy_staging_jars_for_jdk17.sh
deleted file mode 100755
index 36fe2ab40b..0000000000
--- a/tools/releasing/deploy_staging_jars_for_jdk17.sh
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/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.
-#
-
-##
-## Variables with defaults (if not overwritten by environment)
-##
-MVN=${MVN:-mvn}
-CUSTOM_OPTIONS=${CUSTOM_OPTIONS:-}
-
-# fail immediately
-set -o errexit
-set -o nounset
-
-CURR_DIR=`pwd`
-BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
-PROJECT_ROOT="${BASE_DIR}/../../"
-
-# Sanity check to ensure that resolved paths are valid; a LICENSE file should 
always exist in project root
-if [ ! -f ${PROJECT_ROOT}/LICENSE ]; then
-    echo "Project root path ${PROJECT_ROOT} is not valid; script may be in the 
wrong directory."
-    exit 1
-fi
-
-###########################
-
-cd ${PROJECT_ROOT}
-
-echo "Building spark4 module"
-${MVN} clean install -ntp -Pdocs-and-source,spark4 -DskipTests -pl 
paimon-spark/paimon-spark-4.0,paimon-spark/paimon-spark-4.1 -am $CUSTOM_OPTIONS
-
-echo "Deploying spark4 module to repository.apache.org"
-${MVN} deploy -ntp -Papache-release,docs-and-source,spark4 -DskipTests 
-DretryFailedDeploymentCount=10 \
- -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
 $CUSTOM_OPTIONS
-
-cd ${CURR_DIR}
diff --git a/tools/releasing/sign_maven_repository.sh 
b/tools/releasing/sign_maven_repository.sh
new file mode 100755
index 0000000000..7c7153c269
--- /dev/null
+++ b/tools/releasing/sign_maven_repository.sh
@@ -0,0 +1,96 @@
+#!/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.
+#
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+REPOSITORY_DIRECTORY=${REPOSITORY_DIRECTORY:-}
+GPG=${GPG:-gpg}
+GPG_KEY_ID=${GPG_KEY_ID:-}
+
+if [ -z "${REPOSITORY_DIRECTORY}" ]; then
+    echo "REPOSITORY_DIRECTORY was not set" >&2
+    exit 1
+fi
+if [ ! -d "${REPOSITORY_DIRECTORY}" ]; then
+    echo "Maven repository does not exist: ${REPOSITORY_DIRECTORY}" >&2
+    exit 1
+fi
+if find "${REPOSITORY_DIRECTORY}" -type f -name '*.asc' -print -quit |
+    grep -q .; then
+    echo "Maven repository already contains signatures" >&2
+    exit 1
+fi
+
+if command -v md5sum >/dev/null 2>&1; then
+    MD5_COMMAND=md5sum
+else
+    MD5_COMMAND=md5
+fi
+if command -v sha1sum >/dev/null 2>&1; then
+    SHA1_COMMAND=sha1sum
+else
+    SHA1_COMMAND=shasum
+fi
+
+write_md5() {
+    file=$1
+    if [ "${MD5_COMMAND}" = "md5sum" ]; then
+        md5sum "${file}" | awk '{print $1}' > "${file}.md5"
+    else
+        md5 -q "${file}" > "${file}.md5"
+    fi
+}
+
+write_sha1() {
+    file=$1
+    if [ "${SHA1_COMMAND}" = "sha1sum" ]; then
+        sha1sum "${file}" | awk '{print $1}' > "${file}.sha1"
+    else
+        shasum -a 1 "${file}" | awk '{print $1}' > "${file}.sha1"
+    fi
+}
+
+artifact_count=0
+while IFS= read -r artifact; do
+    gpg_arguments=(--armor --detach-sign)
+    if [ -n "${GPG_KEY_ID}" ]; then
+        gpg_arguments+=(--local-user "${GPG_KEY_ID}")
+    fi
+
+    "${GPG}" "${gpg_arguments[@]}" \
+        --output "${artifact}.asc" "${artifact}"
+    "${GPG}" --verify "${artifact}.asc" "${artifact}"
+    write_md5 "${artifact}.asc"
+    write_sha1 "${artifact}.asc"
+    artifact_count=$((artifact_count + 1))
+done < <(
+    find "${REPOSITORY_DIRECTORY}" -type f \
+        \( -name '*.jar' -o -name '*.pom' \) |
+        LC_ALL=C sort
+)
+
+if [ "${artifact_count}" -eq 0 ]; then
+    echo "No Maven artifacts found to sign" >&2
+    exit 1
+fi
+
+echo "Signed and verified ${artifact_count} Maven artifacts."

Reply via email to