blob: c4a8413f541dceb27252a7a15d5849a272ab6efd [file]
#!/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 -Eeuo pipefail
CURRENT_STEP=""
VERIFY_SUCCESS="no"
VERIFY_OWNS_TMPDIR="0"
VERIFY_TMPDIR=""
on_error() {
local status=$?
if [ -n "${CURRENT_STEP}" ]; then
echo "FAILED: ${CURRENT_STEP}" >&2
else
echo "FAILED" >&2
fi
exit "${status}"
}
cleanup() {
if [ -z "${VERIFY_TMPDIR}" ]; then
return
fi
if [ "${VERIFY_SUCCESS}" = "yes" ]; then
if [ "${VERIFY_OWNS_TMPDIR}" = "1" ]; then
rm -rf "${VERIFY_TMPDIR}"
fi
else
echo "Failed to verify release candidate. See ${VERIFY_TMPDIR} for details." >&2
fi
}
trap on_error ERR
trap cleanup EXIT
usage() {
cat <<USAGE
Usage:
$0 <version> <rc> [options]
Arguments:
<version>
Release version in <major>.<minor>.<patch> format.
Example: 0.9.1
<rc>
Numeric release candidate round.
Example: 2 verifies apache-iceberg-rust-0.9.1-rc2.
Options:
--dist_dir <dir>
Local artifact root used when --download is 0.
Relative paths are resolved from the repository root.
Default: dist
--download <0|1>
Whether to download artifacts from ASF dev dist. Use 0 to verify local artifacts.
Default: 1
--verify_signature <0|1>
Whether to verify the .asc signature with the local GPG keyring.
Default: 1
--import_gpg_keys <0|1>
Whether to download and import Apache Iceberg release keys before signature verification.
Default: 0
--verify_git_tag <0|1>
Whether to verify that the source archive matches the git tag v<version>-rc.<rc>.
The tag is cloned and re-archived, then compared against the extracted archive.
Default: 1
--git_repo_url <url>
Git repository URL used to clone the RC tag for the git tag comparison.
Default: https://github.com/apache/iceberg-rust.git
--check_headers <0|1>
Whether to check Apache license headers against the extracted source archive.
Default: 1
--build <0|1>
Whether to build and test the Rust source distribution.
Default: 1
--python <0|1>
Whether to build and test pyiceberg-core.
Default: 1
--tmp_dir <dir>
Verification sandbox. If omitted, a temporary directory is created and deleted on success.
Default: auto-created temporary directory
-h, --help
Show this help message.
Examples:
$0 0.9.1 2
$0 0.9.1 2 --download 0
$0 0.9.1 2 --import_gpg_keys 1
$0 0.9.1 2 --verify_git_tag 0
$0 0.9.1 2 --download 0 --build 0 --python 0 --check_headers 0 --verify_signature 0
USAGE
}
show_help_if_requested() {
while [ "$#" -gt 0 ]; do
case "$1" in
-h | --help)
usage
exit 0
;;
esac
shift
done
}
start_step() {
CURRENT_STEP="$1"
echo "==> ${CURRENT_STEP}"
}
finish_step() {
echo "OK: ${CURRENT_STEP}"
CURRENT_STEP=""
}
run_step() {
local step="$1"
shift
start_step "${step}"
"$@"
finish_step
}
skip_step() {
local step="$1"
start_step "${step}"
echo "OK: ${CURRENT_STEP} (skipped)"
CURRENT_STEP=""
}
enabled() {
[ "$1" != "0" ]
}
require_command() {
local command_name="$1"
if ! command -v "${command_name}" >/dev/null 2>&1; then
echo "This step requires '${command_name}', but it is not installed." >&2
return 1
fi
}
require_checksum_command() {
if ! command -v shasum >/dev/null 2>&1 && ! command -v sha512sum >/dev/null 2>&1; then
echo "This step requires either 'shasum' or 'sha512sum'." >&2
return 1
fi
}
require_option_value() {
local option_name="$1"
local option_value="${2:-}"
if [ -z "${option_value}" ]; then
echo "Missing value for ${option_name}." >&2
usage >&2
return 1
fi
}
validate_bool_option() {
local option_name="$1"
local option_value="$2"
case "${option_value}" in
0 | 1) ;;
*)
echo "${option_name} must be 0 or 1, got '${option_value}'." >&2
return 1
;;
esac
}
parse_args() {
local positional=()
while [ "$#" -gt 0 ]; do
case "$1" in
-h | --help)
usage
exit 0
;;
--dist_dir | --dist-dir)
require_option_value "$1" "${2:-}"
RELEASE_DIST_DIR="$2"
shift 2
;;
--download)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
DOWNLOAD="$2"
shift 2
;;
--verify_signature | --verify-signature)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
VERIFY_SIGNATURE="$2"
shift 2
;;
--import_gpg_keys | --import-gpg-keys)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
IMPORT_GPG_KEYS="$2"
shift 2
;;
--verify_git_tag | --verify-git-tag)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
VERIFY_GIT_TAG="$2"
shift 2
;;
--git_repo_url | --git-repo-url)
require_option_value "$1" "${2:-}"
GIT_REPO_URL="$2"
shift 2
;;
--check_headers | --check-headers)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
CHECK_HEADERS="$2"
shift 2
;;
--build)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
BUILD="$2"
shift 2
;;
--python)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
PYTHON="$2"
shift 2
;;
--tmp_dir | --tmp-dir)
require_option_value "$1" "${2:-}"
VERIFY_TMPDIR="$2"
shift 2
;;
--*)
echo "Unknown option: $1" >&2
usage >&2
return 1
;;
*)
positional+=("$1")
shift
;;
esac
done
if [ "${#positional[@]}" -ne 2 ]; then
usage >&2
return 1
fi
VERSION="${positional[0]}"
RC="${positional[1]}"
}
validate_args() {
if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Version '${VERSION}' must use format <major>.<minor>.<patch>." >&2
return 1
fi
if [[ ! "${RC}" =~ ^[0-9]+$ ]]; then
echo "RC '${RC}' must be a number, for example: 0, 1, or 2." >&2
return 1
fi
}
derive_release_names() {
RC_TAG="v${VERSION}-rc.${RC}"
RC_ID="apache-iceberg-rust-${VERSION}-rc${RC}"
ARCHIVE_BASE_NAME="apache-iceberg-rust-${VERSION}"
ARCHIVE_FILE_NAME="${ARCHIVE_BASE_NAME}.tar.gz"
ICEBERG_DIST_BASE_URL="https://downloads.apache.org/iceberg"
DOWNLOAD_RC_BASE_URL="https://dist.apache.org/repos/dist/dev/iceberg/${RC_ID}"
if [[ "${RELEASE_DIST_DIR}" = /* ]]; then
LOCAL_DIST_ROOT="${RELEASE_DIST_DIR}"
else
LOCAL_DIST_ROOT="${REPO_ROOT}/${RELEASE_DIST_DIR}"
fi
LOCAL_RC_DIR="${LOCAL_DIST_ROOT}/${RC_ID}"
}
setup_tmpdir() {
if [ -z "${VERIFY_TMPDIR}" ]; then
require_command mktemp
VERIFY_TMPDIR="$(mktemp -d -t "iceberg-rust-${VERSION}-rc${RC}.XXXXXX")"
VERIFY_OWNS_TMPDIR="1"
else
mkdir -p "${VERIFY_TMPDIR}"
fi
}
enter_tmpdir() {
cd "${VERIFY_TMPDIR}"
}
download_file() {
require_command curl
local file_name="$1"
curl \
--fail \
--location \
--remote-name \
--show-error \
--silent \
"${DOWNLOAD_RC_BASE_URL}/${file_name}"
}
copy_local_file() {
require_command cp
local file_name="$1"
cp "${LOCAL_RC_DIR}/${file_name}" "${file_name}"
}
fetch_file() {
local file_name="$1"
if enabled "${DOWNLOAD}"; then
download_file "${file_name}"
else
copy_local_file "${file_name}"
fi
}
import_gpg_keys() {
require_command curl
require_command gpg
curl \
--fail \
--location \
--output KEYS \
--show-error \
--silent \
"${ICEBERG_DIST_BASE_URL}/KEYS"
gpg --import KEYS
}
verify_source_signature() {
require_command gpg
gpg --verify "${ARCHIVE_FILE_NAME}.asc" "${ARCHIVE_FILE_NAME}"
}
verify_checksum() {
require_checksum_command
if command -v shasum >/dev/null 2>&1; then
shasum -a 512 -c "${ARCHIVE_FILE_NAME}.sha512"
else
sha512sum -c "${ARCHIVE_FILE_NAME}.sha512"
fi
}
extract_source_archive() {
require_command tar
tar -xzf "${ARCHIVE_FILE_NAME}"
}
verify_git_tag_matches_archive() {
require_command git
require_command tar
require_command diff
local tag_checkout_dir="git-tag-checkout"
local tag_archive_dir="git-tag-archive"
local tag_archive_file="git-tag-source.tar.gz"
# Clone only the RC tag from the git repository.
git clone --quiet --depth 1 --branch "${RC_TAG}" "${GIT_REPO_URL}" "${tag_checkout_dir}"
# Recreate the source archive from the tag using the same layout as create_rc.sh
# (git archive honors .gitattributes export-ignore rules, e.g. the website dir).
git -C "${tag_checkout_dir}" archive \
--format=tar.gz \
--prefix="${ARCHIVE_BASE_NAME}/" \
--output="${VERIFY_TMPDIR}/${tag_archive_file}" \
HEAD
mkdir -p "${tag_archive_dir}"
tar -xzf "${tag_archive_file}" -C "${tag_archive_dir}"
# Fail if the released archive differs from what the git tag produces.
if ! diff -r "${tag_archive_dir}/${ARCHIVE_BASE_NAME}" "${ARCHIVE_BASE_NAME}"; then
echo "Source archive does not match git tag ${RC_TAG}." >&2
return 1
fi
}
check_license_headers() {
require_command docker
docker run --rm -v "${VERIFY_TMPDIR}/${ARCHIVE_BASE_NAME}:/github/workspace" apache/skywalking-eyes header check
}
test_source_distribution() {
require_command make
(
trap - ERR
cd "${ARCHIVE_BASE_NAME}"
make build
make test
)
}
test_python_distribution() {
require_command make
require_command uv
(
trap - ERR
cd "${ARCHIVE_BASE_NAME}/bindings/python"
make install
make test
)
}
mark_success() {
VERIFY_SUCCESS="yes"
echo "RC looks good!"
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
VERSION=""
RC=""
RELEASE_DIST_DIR="dist"
DOWNLOAD="1"
VERIFY_SIGNATURE="1"
IMPORT_GPG_KEYS="0"
VERIFY_GIT_TAG="1"
GIT_REPO_URL="https://github.com/apache/iceberg-rust.git"
CHECK_HEADERS="1"
BUILD="1"
PYTHON="1"
show_help_if_requested "$@"
run_step "Parse command arguments" parse_args "$@"
run_step "Validate release arguments" validate_args
derive_release_names
run_step "Create verification sandbox" setup_tmpdir
run_step "Enter verification sandbox ${VERIFY_TMPDIR}" enter_tmpdir
run_step "Fetch source archive ${ARCHIVE_FILE_NAME}" fetch_file "${ARCHIVE_FILE_NAME}"
if enabled "${VERIFY_SIGNATURE}"; then
run_step "Fetch source archive signature" fetch_file "${ARCHIVE_FILE_NAME}.asc"
if enabled "${IMPORT_GPG_KEYS}"; then
run_step "Import Apache Iceberg GPG keys" import_gpg_keys
else
skip_step "Import Apache Iceberg GPG keys"
fi
run_step "Verify source archive signature" verify_source_signature
else
skip_step "Fetch source archive signature"
skip_step "Import Apache Iceberg GPG keys"
skip_step "Verify source archive signature"
fi
run_step "Fetch source archive checksum" fetch_file "${ARCHIVE_FILE_NAME}.sha512"
run_step "Verify source archive checksum" verify_checksum
run_step "Extract source archive" extract_source_archive
if enabled "${VERIFY_GIT_TAG}"; then
run_step "Verify source archive matches git tag ${RC_TAG}" verify_git_tag_matches_archive
else
skip_step "Verify source archive matches git tag ${RC_TAG}"
fi
if enabled "${CHECK_HEADERS}"; then
run_step "Check license headers" check_license_headers
else
skip_step "Check license headers"
fi
if enabled "${BUILD}"; then
run_step "Build and test Rust source distribution" test_source_distribution
else
skip_step "Build and test Rust source distribution"
fi
if enabled "${PYTHON}"; then
run_step "Build and test pyiceberg-core" test_python_distribution
else
skip_step "Build and test pyiceberg-core"
fi
run_step "Print verification summary" mark_success