blob: 8f193c5ce030e6147a4deea56243b86c2a1a6063 [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=""
on_error() {
local status=$?
if [ -n "${CURRENT_STEP}" ]; then
echo "FAILED: ${CURRENT_STEP}" >&2
else
echo "FAILED" >&2
fi
exit "${status}"
}
trap on_error ERR
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 creates tag v0.9.1-rc.2 and dist dir apache-iceberg-rust-0.9.1-rc2.
Options:
--release_ref <ref>
Git commit-ish to archive and tag.
Default: HEAD
--dist_dir <dir>
Directory where RC artifacts are written.
Relative paths are resolved from the repository root.
Default: dist
--create_rc_tag <0|1>
Whether to create the signed annotated RC git tag as the final release step.
Default: 1
--check_headers <0|1>
Whether to check Apache license headers against the generated source archive.
Default: 1
--check_deps <0|1>
Whether to run the dependency license check before creating artifacts.
Default: 1
--sign <0|1>
Whether to create and verify the detached GPG signature for the source archive.
Default: 1
--upload_svn <0|1>
Whether to upload RC artifacts to the ASF dev dist SVN repository.
Default: 0
--svn_dist_url <url>
SVN directory URL where the RC artifact directory will be uploaded.
Default: https://dist.apache.org/repos/dist/dev/iceberg
-h, --help
Show this help message.
Examples:
$0 0.9.1 2
$0 0.9.1 2 --create_rc_tag 0 --sign 0
$0 0.9.1 2 --upload_svn 1
$0 0.9.1 2 --release_ref abc123 --dist_dir /tmp/iceberg-rust-dist
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_cargo_deny() {
require_command cargo
if ! cargo deny --version >/dev/null 2>&1; then
echo "This step requires 'cargo-deny' for dependency license checks." >&2
echo "Install it with: cargo install --locked cargo-deny" >&2
echo "To skip this step locally, pass: --check_deps 0" >&2
return 1
fi
}
require_gpg_secret_key() {
require_command gpg
if ! gpg --list-secret-keys --with-colons 2>/dev/null | grep -q '^sec'; then
echo "This step requires a GPG secret key for signing release artifacts or tags." >&2
echo "Set up a release signing key first: https://rust.iceberg.apache.org/reference/setup_gpg.html" >&2
echo "To skip signing locally, pass: --sign 0 --create_rc_tag 0" >&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
;;
--release_ref | --release-ref)
require_option_value "$1" "${2:-}"
RELEASE_REF="$2"
shift 2
;;
--dist_dir | --dist-dir)
require_option_value "$1" "${2:-}"
RELEASE_DIST_DIR="$2"
shift 2
;;
--create_rc_tag | --create-rc-tag)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
CREATE_RC_TAG="$2"
shift 2
;;
--check_headers | --check-headers)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
CHECK_HEADERS="$2"
shift 2
;;
--check_deps | --check-deps)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
CHECK_DEPS="$2"
shift 2
;;
--sign)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
SIGN_ARCHIVE="$2"
shift 2
;;
--upload_svn | --upload-svn)
require_option_value "$1" "${2:-}"
validate_bool_option "$1" "$2"
UPLOAD_SVN="$2"
shift 2
;;
--svn_dist_url | --svn-dist-url)
require_option_value "$1" "${2:-}"
SVN_DIST_URL="$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"
if [[ "${RELEASE_DIST_DIR}" = /* ]]; then
DIST_ROOT="${RELEASE_DIST_DIR}"
else
DIST_ROOT="${REPO_ROOT}/${RELEASE_DIST_DIR}"
fi
RC_DIR="${DIST_ROOT}/${RC_ID}"
SVN_DIST_URL="${SVN_DIST_URL%/}"
SVN_RC_URL="${SVN_DIST_URL}/${RC_ID}"
}
check_release_ref() {
require_command git
git -C "${REPO_ROOT}" rev-parse --verify "${RELEASE_REF}^{commit}" >/dev/null
}
check_rc_tag_available() {
require_command git
if git -C "${REPO_ROOT}" rev-parse --verify --quiet "refs/tags/${RC_TAG}" >/dev/null; then
echo "Tag '${RC_TAG}' already exists." >&2
return 1
fi
}
check_dependency_licenses() {
require_cargo_deny
(
trap - ERR
cd "${REPO_ROOT}"
cargo deny check license
)
}
prepare_output_directory() {
rm -rf "${RC_DIR}"
mkdir -p "${RC_DIR}"
}
create_source_archive() {
require_command git
git -C "${REPO_ROOT}" archive \
--format=tar.gz \
--output="${RC_DIR}/${ARCHIVE_FILE_NAME}" \
--prefix="${ARCHIVE_BASE_NAME}/" \
"${RELEASE_REF}"
}
check_license_headers() {
require_command tar
require_command docker
require_command mktemp
(
trap - ERR
local header_tmp_dir
header_tmp_dir="$(mktemp -d -t "iceberg-rust-headers.XXXXXX")"
cleanup() {
rm -rf "${header_tmp_dir}"
}
trap cleanup EXIT
tar -xzf "${RC_DIR}/${ARCHIVE_FILE_NAME}" -C "${header_tmp_dir}"
docker run --rm -v "${header_tmp_dir}/${ARCHIVE_BASE_NAME}:/github/workspace" apache/skywalking-eyes header check
)
}
sign_source_archive() {
require_gpg_secret_key
(
trap - ERR
cd "${RC_DIR}"
gpg --armor --output "${ARCHIVE_FILE_NAME}.asc" --detach-sig "${ARCHIVE_FILE_NAME}"
)
}
verify_source_signature() {
require_command gpg
(
trap - ERR
cd "${RC_DIR}"
gpg --verify "${ARCHIVE_FILE_NAME}.asc" "${ARCHIVE_FILE_NAME}"
)
}
generate_checksum() {
require_checksum_command
(
trap - ERR
cd "${RC_DIR}"
if command -v shasum >/dev/null 2>&1; then
shasum -a 512 "${ARCHIVE_FILE_NAME}" > "${ARCHIVE_FILE_NAME}.sha512"
else
sha512sum "${ARCHIVE_FILE_NAME}" > "${ARCHIVE_FILE_NAME}.sha512"
fi
)
}
verify_checksum() {
require_checksum_command
(
trap - ERR
cd "${RC_DIR}"
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
)
}
require_release_artifact() {
local artifact_path="$1"
if [ ! -f "${artifact_path}" ]; then
echo "Required release artifact is missing: ${artifact_path}" >&2
return 1
fi
}
upload_to_svn() {
require_command svn
require_release_artifact "${RC_DIR}/${ARCHIVE_FILE_NAME}"
require_release_artifact "${RC_DIR}/${ARCHIVE_FILE_NAME}.asc"
require_release_artifact "${RC_DIR}/${ARCHIVE_FILE_NAME}.sha512"
if svn info "${SVN_RC_URL}" >/dev/null 2>&1; then
echo "SVN target already exists: ${SVN_RC_URL}" >&2
return 1
fi
svn import "${RC_DIR}" "${SVN_RC_URL}" -m "Prepare Apache Iceberg Rust ${VERSION} RC${RC}"
}
create_rc_tag() {
require_command git
require_gpg_secret_key
git -C "${REPO_ROOT}" tag -s "${RC_TAG}" "${RELEASE_REF}" -m "Apache Iceberg Rust ${VERSION} RC${RC}"
}
print_summary() {
echo "Created Apache Iceberg Rust RC artifacts:"
echo " Tag: ${RC_TAG}"
echo " Directory: ${RC_DIR}"
echo " Archive: ${RC_DIR}/${ARCHIVE_FILE_NAME}"
if enabled "${SIGN_ARCHIVE}"; then
echo " Signature: ${RC_DIR}/${ARCHIVE_FILE_NAME}.asc"
fi
echo " Checksum: ${RC_DIR}/${ARCHIVE_FILE_NAME}.sha512"
if enabled "${UPLOAD_SVN}"; then
echo " SVN URL: ${SVN_RC_URL}"
fi
}
print_vote_email() {
cat <<MAIL
Draft vote email for dev@iceberg.apache.org:
---------------------------------------------------------
To: dev@iceberg.apache.org
Subject: [VOTE] Release Apache Iceberg Rust ${VERSION} RC${RC}
Hello Apache Iceberg Rust Community,
This is a call for a vote to release Apache Iceberg Rust version ${VERSION}.
The tag to be voted on is: ${RC_TAG}.
The release candidate:
https://dist.apache.org/repos/dist/dev/iceberg/${RC_ID}/
Keys to verify the release candidate:
https://downloads.apache.org/iceberg/KEYS
Git tag for the release:
https://github.com/apache/iceberg-rust/releases/tag/${RC_TAG}
Please download, verify, and test the release candidate.
This vote will be open for at least 72 hours and will remain open until the required number of votes is reached.
Please vote accordingly:
[ ] +1 Approve
[ ] +0 No opinion
[ ] -1 Disapprove (please provide a reason)
To learn more about Apache Iceberg, please visit:
https://rust.iceberg.apache.org/
Checklist for reference:
[ ] Download links are valid
[ ] Checksums and signatures are correct
[ ] LICENSE and NOTICE files are present
[ ] No unexpected binary files are included
[ ] All source files have ASF headers
[ ] The project builds successfully from source
[ ] pyiceberg-core builds and tests successfully
For more details, please refer to:
https://rust.iceberg.apache.org/release.html#how-to-verify-a-release
Thanks,
<your name>
---------------------------------------------------------
MAIL
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
VERSION=""
RC=""
RELEASE_REF="HEAD"
RELEASE_DIST_DIR="dist"
CREATE_RC_TAG="1"
CHECK_HEADERS="1"
CHECK_DEPS="1"
SIGN_ARCHIVE="1"
UPLOAD_SVN="0"
SVN_DIST_URL="https://dist.apache.org/repos/dist/dev/iceberg"
show_help_if_requested "$@"
run_step "Parse command arguments" parse_args "$@"
run_step "Validate release arguments" validate_args
derive_release_names
run_step "Check release reference ${RELEASE_REF}" check_release_ref
if enabled "${CREATE_RC_TAG}"; then
run_step "Check RC tag ${RC_TAG} is available" check_rc_tag_available
else
skip_step "Check RC tag ${RC_TAG} is available"
fi
if enabled "${CHECK_DEPS}"; then
run_step "Check dependency licenses" check_dependency_licenses
else
skip_step "Check dependency licenses"
fi
run_step "Prepare output directory ${RC_DIR}" prepare_output_directory
run_step "Create source archive ${ARCHIVE_FILE_NAME}" create_source_archive
if enabled "${CHECK_HEADERS}"; then
run_step "Check source archive license headers" check_license_headers
else
skip_step "Check source archive license headers"
fi
if enabled "${SIGN_ARCHIVE}"; then
run_step "Sign source archive ${ARCHIVE_FILE_NAME}" sign_source_archive
run_step "Verify source archive signature" verify_source_signature
else
skip_step "Sign source archive ${ARCHIVE_FILE_NAME}"
skip_step "Verify source archive signature"
fi
run_step "Generate SHA-512 checksum" generate_checksum
run_step "Verify SHA-512 checksum" verify_checksum
if enabled "${UPLOAD_SVN}"; then
run_step "Upload artifacts to SVN dist ${SVN_RC_URL}" upload_to_svn
else
skip_step "Upload artifacts to SVN dist ${SVN_RC_URL}"
fi
if enabled "${CREATE_RC_TAG}"; then
run_step "Create signed RC tag ${RC_TAG}" create_rc_tag
else
skip_step "Create signed RC tag ${RC_TAG}"
fi
run_step "Print artifact summary" print_summary
run_step "Print draft vote email" print_vote_email