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."