You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by sc...@apache.org on 2026/05/28 20:31:24 UTC
(iceberg-rust) branch main updated: Add some scripts to help release (#2522)
This is an automated email from the ASF dual-hosted git repository.
CTTY pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 28c4800f1 Add some scripts to help release (#2522)
28c4800f1 is described below
commit 28c4800f198686f306bd80d11d03f232670feec3
Author: blackmwk <li...@outlook.com>
AuthorDate: Fri May 29 04:31:18 2026 +0800
Add some scripts to help release (#2522)
## Which issue does this PR close?
- Closes #2512
## What changes are included in this PR?
- Add create_rc.sh to create RC source artifacts, signatures, checksums,
optional SVN upload, RC tag creation, and draft
VOTE email output.
- Add verify_rc.sh to verify RC artifacts locally or from ASF dev dist,
with optional GPG key import disabled by
default.
- Add release.sh to promote an approved RC by creating the final release
tag and moving SVN artifacts from dev dist to
release dist.
- Add dependencies.sh for dependency license checks and dependency list
generation using cargo metadata.
- Move release documentation to the new dev/release workflow and update
website release instructions.
- Remove all files under the old scripts directory.
- Update CI path filters to ignore dev/release/**.
- Update deny.toml license exceptions for current dependencies.
## Are these changes tested?
Yes, locally without committing.
---
.github/workflows/bindings_python_ci.yml | 2 +-
.github/workflows/ci.yml | 2 +-
deny.toml | 2 +-
dev/release/README.md | 138 ++++++++
dev/release/create_rc.sh | 559 +++++++++++++++++++++++++++++++
dev/release/dependencies.sh | 187 +++++++++++
dev/release/release.sh | 335 ++++++++++++++++++
dev/release/verify_rc.sh | 456 +++++++++++++++++++++++++
scripts/dependencies.py | 94 ------
scripts/release.sh | 79 -----
scripts/verify.py | 52 ---
website/src/release.md | 291 ++++++++++------
12 files changed, 1860 insertions(+), 337 deletions(-)
diff --git a/.github/workflows/bindings_python_ci.yml b/.github/workflows/bindings_python_ci.yml
index 91283c66f..1f193e1ee 100644
--- a/.github/workflows/bindings_python_ci.yml
+++ b/.github/workflows/bindings_python_ci.yml
@@ -25,7 +25,7 @@ on:
paths:
- '**' # Include all files and directories in the repository by default.
- '!.github/ISSUE_TEMPLATE/**' # Exclude files and directories that don't impact tests or code like templates, metadata, and documentation.
- - '!scripts/**'
+ - '!dev/release/**'
- '!website/**'
- '!.asf.yml'
- '!.gitattributes'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 182a28b07..823e5ca5e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,7 +25,7 @@ on:
paths:
- '**' # Include all files and directories in the repository by default.
- '!.github/ISSUE_TEMPLATE/**' # Exclude files and directories that don't impact tests or code like templates, metadata, and documentation.
- - '!scripts/**'
+ - '!dev/release/**'
- '!website/**'
- '!.asf.yml'
- '!.gitattributes'
diff --git a/deny.toml b/deny.toml
index 847e002bd..11c6dd5dc 100644
--- a/deny.toml
+++ b/deny.toml
@@ -37,7 +37,7 @@ allow = [
exceptions = [
# The MPL license is allowed (binary-only):
# https://www.apache.org/legal/resolved.html#category-b
- { allow = ["MPL-2.0"], crate = "webpki-roots" },
+ { allow = ["MPL-2.0"], crate = "colored" },
{ allow = ["MPL-2.0"], crate = "generational-arena" },
{ allow = ["MPL-2.0"], crate = "option-ext" },
]
diff --git a/dev/release/README.md b/dev/release/README.md
new file mode 100644
index 000000000..e02edab80
--- /dev/null
+++ b/dev/release/README.md
@@ -0,0 +1,138 @@
+<!--
+ ~ 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.
+-->
+
+# Release Helpers
+
+These scripts support local Apache Iceberg Rust release candidate creation and verification.
+
+## Create an RC
+
+```shell
+dev/release/create_rc.sh 0.9.1 2
+```
+
+This creates:
+
+- source archive `dist/apache-iceberg-rust-0.9.1-rc2/apache-iceberg-rust-0.9.1.tar.gz`
+- detached signature `.asc`
+- SHA-512 checksum `.sha512`
+- signed annotated tag `v0.9.1-rc.2`
+
+The license header check runs against the generated source archive, not the live Git worktree. The optional SVN upload runs after local artifact verification and before RC tag creation. The signed RC tag is created as the final release step, then the script prints a draft VOTE email for `dev@iceberg.apache.org`. The script logs every step before it runs and after it succeeds. If a step fails, it prints the failed step and stops.
+
+Common options:
+
+```shell
+dev/release/create_rc.sh 0.9.1 2 --release_ref <commit-ish>
+dev/release/create_rc.sh 0.9.1 2 --create_rc_tag 0 --sign 0
+dev/release/create_rc.sh 0.9.1 2 --upload_svn 1
+dev/release/create_rc.sh 0.9.1 2 --check_headers 0 --check_deps 0
+```
+
+Defaults:
+
+- `--release_ref HEAD`: git commit-ish to archive and tag.
+- `--dist_dir dist`: artifact output root.
+- `--create_rc_tag 1`: create the signed annotated RC tag as the final release step.
+- `--check_headers 1`: check Apache license headers against the source archive.
+- `--check_deps 1`: run dependency license checks before artifact creation.
+- `--sign 1`: create and verify the detached GPG signature.
+- `--upload_svn 0`: upload RC artifacts to the ASF dev dist SVN repository.
+- `--svn_dist_url https://dist.apache.org/repos/dist/dev/iceberg`: SVN directory URL where the RC artifact directory will be uploaded.
+
+`--sign 1` or `--create_rc_tag 1` requires a local GPG secret key. See the website's GPG setup guide before creating a real release candidate.
+
+`--check_deps 1` requires `cargo-deny`. Install it with:
+
+```shell
+cargo install cargo-deny
+```
+
+## Verify an RC
+
+```shell
+dev/release/verify_rc.sh 0.9.1 2
+```
+
+By default this downloads from `https://dist.apache.org/repos/dist/dev/iceberg/apache-iceberg-rust-0.9.1-rc2/`, verifies checksum and signature with the local GPG keyring, checks source headers, and runs Rust and Python build/tests.
+
+To verify artifacts already created under local `dist/`:
+
+```shell
+dev/release/verify_rc.sh 0.9.1 2 --download 0
+```
+
+Common options:
+
+```shell
+dev/release/verify_rc.sh 0.9.1 2 --verify_signature 0
+dev/release/verify_rc.sh 0.9.1 2 --import_gpg_keys 1
+dev/release/verify_rc.sh 0.9.1 2 --build 0 --python 0 --check_headers 0
+```
+
+Defaults:
+
+- `--dist_dir dist`: local artifact root used when `--download 0`.
+- `--download 1`: download artifacts from ASF dev dist.
+- `--verify_signature 1`: verify the `.asc` signature with the local GPG keyring.
+- `--import_gpg_keys 0`: download and import Apache Iceberg release keys before signature verification.
+- `--check_headers 1`: check Apache license headers against the extracted source archive.
+- `--build 1`: build and test the Rust source distribution.
+- `--python 1`: build and test pyiceberg-core.
+- `--tmp_dir <auto>`: verification sandbox; auto-created and deleted on success when omitted.
+
+## Promote an RC to a Release
+
+After the VOTE passes, convert the approved RC to the official release:
+
+```shell
+dev/release/release.sh 0.9.1 2
+```
+
+This creates the signed annotated final release tag `v0.9.1` from the RC tag `v0.9.1-rc.2`, then moves the ASF SVN artifacts from `dev/iceberg/apache-iceberg-rust-0.9.1-rc2` to `release/iceberg/apache-iceberg-rust-0.9.1`.
+
+Common options:
+
+```shell
+dev/release/release.sh 0.9.1 2 --create_release_tag 0
+dev/release/release.sh 0.9.1 2 --move_svn 0
+```
+
+Defaults:
+
+- `--create_release_tag 1`: create the signed annotated final release git tag.
+- `--move_svn 1`: move the RC artifacts from ASF dev dist to ASF release dist.
+- `--tag_ref <rc tag commit>`: git commit-ish to tag as the final release.
+- `--dev_dist_url https://dist.apache.org/repos/dist/dev/iceberg`: SVN directory URL containing RC artifact directories.
+- `--release_dist_url https://dist.apache.org/repos/dist/release/iceberg`: SVN directory URL where final release artifact directories are published.
+
+The script does not push the final release tag. Push it manually after reviewing the output:
+
+```shell
+git push origin "v0.9.1"
+```
+
+## Dependencies
+
+Use the dependency helper to update or verify dependency license lists:
+
+```shell
+dev/release/dependencies.sh generate
+dev/release/dependencies.sh check
+```
diff --git a/dev/release/create_rc.sh b/dev/release/create_rc.sh
new file mode 100755
index 000000000..10beae44a
--- /dev/null
+++ b/dev/release/create_rc.sh
@@ -0,0 +1,559 @@
+#!/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 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
diff --git a/dev/release/dependencies.sh b/dev/release/dependencies.sh
new file mode 100755
index 000000000..0fd79598b
--- /dev/null
+++ b/dev/release/dependencies.sh
@@ -0,0 +1,187 @@
+#!/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 <check|generate>
+
+Commands:
+ check
+ Run cargo-deny license validation once at the root workspace.
+ Default command: none; this argument is required.
+
+ generate
+ Regenerate each workspace package's DEPENDENCIES.rust.tsv file.
+ Package directories are discovered from cargo metadata.
+ Default command: none; this argument is required.
+
+Options:
+ -h, --help
+ Show this help message.
+
+Examples:
+ $0 check
+ $0 generate
+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
+}
+
+require_command() {
+ local command_name="$1"
+ if ! command -v "${command_name}" >/dev/null 2>&1; then
+ echo "This script requires '${command_name}', but it is not installed." >&2
+ return 1
+ fi
+}
+
+require_cargo_deny() {
+ require_command cargo
+ if ! cargo deny --version >/dev/null 2>&1; then
+ echo "This script requires 'cargo-deny' for dependency license checks." >&2
+ echo "Install it with: cargo install cargo-deny" >&2
+ return 1
+ fi
+}
+
+validate_args() {
+ if [ "$#" -ne 1 ]; then
+ usage
+ return 1
+ fi
+
+ case "$1" in
+ check | generate) ;;
+ *)
+ usage
+ return 1
+ ;;
+ esac
+}
+
+discover_cargo_dirs() {
+ require_command cargo
+ require_command jq
+ CARGO_DIRS="$(cargo metadata \
+ --format-version=1 \
+ --no-deps \
+ --manifest-path "${REPO_ROOT}/Cargo.toml" |
+ jq -r '
+ .workspace_members as $workspace_members
+ | .packages[]
+ | select(.id as $id | $workspace_members | index($id))
+ | .manifest_path
+ | sub("/Cargo.toml$"; "")
+ ' |
+ sort)"
+}
+
+check_deps_for_dir() {
+ local cargo_dir="$1"
+ require_cargo_deny
+ (
+ trap - ERR
+ cd "${cargo_dir}"
+ cargo deny check license
+ )
+}
+
+generate_deps_for_dir() {
+ local cargo_dir="$1"
+ require_cargo_deny
+ (
+ trap - ERR
+ cd "${cargo_dir}"
+ cargo deny list -f tsv -t 0.6 > DEPENDENCIES.rust.tsv
+ )
+}
+
+check_deps() {
+ run_step "Check dependency licenses in ${REPO_ROOT}" check_deps_for_dir "${REPO_ROOT}"
+}
+
+generate_deps() {
+ run_step "Discover Cargo workspace package directories" discover_cargo_dirs
+ while IFS= read -r cargo_dir; do
+ [ -n "${cargo_dir}" ] || continue
+ run_step "Generate dependency list in ${cargo_dir}" generate_deps_for_dir "${cargo_dir}"
+ done <<< "${CARGO_DIRS}"
+}
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+COMMAND="${1:-}"
+CARGO_DIRS=""
+
+show_help_if_requested "$@"
+run_step "Validate dependency command arguments" validate_args "$@"
+
+case "${COMMAND}" in
+ check)
+ check_deps
+ ;;
+ generate)
+ generate_deps
+ ;;
+esac
diff --git a/dev/release/release.sh b/dev/release/release.sh
new file mode 100755
index 000000000..0e5479683
--- /dev/null
+++ b/dev/release/release.sh
@@ -0,0 +1,335 @@
+#!/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 that passed the vote.
+ Example: 2 promotes v0.9.1-rc.2 and apache-iceberg-rust-0.9.1-rc2.
+
+Options:
+ --create_release_tag <0|1>
+ Whether to create the signed annotated final release git tag.
+ Default: 1
+
+ --move_svn <0|1>
+ Whether to move the RC artifacts from ASF dev dist to ASF release dist.
+ Default: 1
+
+ --tag_ref <ref>
+ Git commit-ish to tag as the final release.
+ Default: the commit pointed to by the RC tag
+
+ --dev_dist_url <url>
+ SVN directory URL containing RC artifact directories.
+ Default: https://dist.apache.org/repos/dist/dev/iceberg
+
+ --release_dist_url <url>
+ SVN directory URL where final release artifact directories are published.
+ Default: https://dist.apache.org/repos/dist/release/iceberg
+
+ -h, --help
+ Show this help message.
+
+Examples:
+ $0 0.9.1 2
+ $0 0.9.1 2 --create_release_tag 0
+ $0 0.9.1 2 --move_svn 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_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 the release tag." >&2
+ echo "Set up a release signing key first: https://rust.iceberg.apache.org/reference/setup_gpg.html" >&2
+ echo "To skip tag creation locally, pass: --create_release_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
+ ;;
+ --create_release_tag | --create-release-tag)
+ require_option_value "$1" "${2:-}"
+ validate_bool_option "$1" "$2"
+ CREATE_RELEASE_TAG="$2"
+ shift 2
+ ;;
+ --move_svn | --move-svn)
+ require_option_value "$1" "${2:-}"
+ validate_bool_option "$1" "$2"
+ MOVE_SVN="$2"
+ shift 2
+ ;;
+ --tag_ref | --tag-ref)
+ require_option_value "$1" "${2:-}"
+ TAG_REF="$2"
+ shift 2
+ ;;
+ --dev_dist_url | --dev-dist-url)
+ require_option_value "$1" "${2:-}"
+ DEV_DIST_URL="$2"
+ shift 2
+ ;;
+ --release_dist_url | --release-dist-url)
+ require_option_value "$1" "${2:-}"
+ RELEASE_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}"
+ RELEASE_TAG="v${VERSION}"
+ RC_ID="apache-iceberg-rust-${VERSION}-rc${RC}"
+ RELEASE_ID="apache-iceberg-rust-${VERSION}"
+
+ if [ -z "${TAG_REF}" ]; then
+ TAG_REF="${RC_TAG}^{commit}"
+ fi
+
+ DEV_DIST_URL="${DEV_DIST_URL%/}"
+ RELEASE_DIST_URL="${RELEASE_DIST_URL%/}"
+ SVN_RC_URL="${DEV_DIST_URL}/${RC_ID}"
+ SVN_RELEASE_URL="${RELEASE_DIST_URL}/${RELEASE_ID}"
+}
+
+check_tag_ref() {
+ require_command git
+ git -C "${REPO_ROOT}" rev-parse --verify "${TAG_REF}" >/dev/null
+}
+
+check_release_tag_available() {
+ require_command git
+ if git -C "${REPO_ROOT}" rev-parse --verify --quiet "refs/tags/${RELEASE_TAG}" >/dev/null; then
+ echo "Tag '${RELEASE_TAG}' already exists." >&2
+ return 1
+ fi
+}
+
+create_release_tag() {
+ require_command git
+ require_gpg_secret_key
+ git -C "${REPO_ROOT}" tag -s "${RELEASE_TAG}" "${TAG_REF}" -m "Apache Iceberg Rust ${VERSION}"
+}
+
+check_svn_rc_available() {
+ require_command svn
+ if ! svn info "${SVN_RC_URL}" >/dev/null; then
+ echo "SVN RC source does not exist: ${SVN_RC_URL}" >&2
+ return 1
+ fi
+}
+
+check_svn_release_available() {
+ require_command svn
+ if svn info "${SVN_RELEASE_URL}" >/dev/null 2>&1; then
+ echo "SVN release target already exists: ${SVN_RELEASE_URL}" >&2
+ return 1
+ fi
+}
+
+move_svn_release() {
+ require_command svn
+ svn mv "${SVN_RC_URL}" "${SVN_RELEASE_URL}" -m "Release Apache Iceberg Rust ${VERSION}"
+}
+
+print_summary() {
+ echo "Promoted Apache Iceberg Rust RC to release:"
+ echo " RC tag: ${RC_TAG}"
+ echo " Release tag: ${RELEASE_TAG}"
+ echo " Release tag ref: ${TAG_REF}"
+ echo " SVN RC URL: ${SVN_RC_URL}"
+ echo " SVN release URL: ${SVN_RELEASE_URL}"
+ echo
+ echo "Next step:"
+ echo " git push origin \"${RELEASE_TAG}\""
+}
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+
+VERSION=""
+RC=""
+CREATE_RELEASE_TAG="1"
+MOVE_SVN="1"
+TAG_REF=""
+DEV_DIST_URL="https://dist.apache.org/repos/dist/dev/iceberg"
+RELEASE_DIST_URL="https://dist.apache.org/repos/dist/release/iceberg"
+
+show_help_if_requested "$@"
+run_step "Parse command arguments" parse_args "$@"
+run_step "Validate release arguments" validate_args
+derive_release_names
+
+if enabled "${CREATE_RELEASE_TAG}"; then
+ run_step "Check release tag ref ${TAG_REF}" check_tag_ref
+ run_step "Check release tag ${RELEASE_TAG} is available" check_release_tag_available
+ run_step "Create signed release tag ${RELEASE_TAG}" create_release_tag
+else
+ skip_step "Check release tag ref ${TAG_REF}"
+ skip_step "Check release tag ${RELEASE_TAG} is available"
+ skip_step "Create signed release tag ${RELEASE_TAG}"
+fi
+
+if enabled "${MOVE_SVN}"; then
+ run_step "Check SVN RC source ${SVN_RC_URL}" check_svn_rc_available
+ run_step "Check SVN release target ${SVN_RELEASE_URL} is available" check_svn_release_available
+ run_step "Move SVN RC artifacts to release dist" move_svn_release
+else
+ skip_step "Check SVN RC source ${SVN_RC_URL}"
+ skip_step "Check SVN release target ${SVN_RELEASE_URL} is available"
+ skip_step "Move SVN RC artifacts to release dist"
+fi
+
+run_step "Print release summary" print_summary
diff --git a/dev/release/verify_rc.sh b/dev/release/verify_rc.sh
new file mode 100755
index 000000000..5248da053
--- /dev/null
+++ b/dev/release/verify_rc.sh
@@ -0,0 +1,456 @@
+#!/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
+
+ --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 --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
+ ;;
+ --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_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}"
+}
+
+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"
+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 "${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
diff --git a/scripts/dependencies.py b/scripts/dependencies.py
deleted file mode 100755
index 56c83a179..000000000
--- a/scripts/dependencies.py
+++ /dev/null
@@ -1,94 +0,0 @@
-#!/usr/bin/env python3
-# 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.
-
-from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, REMAINDER
-import subprocess
-import os
-
-
-def get_git_root():
- try:
- return (
- subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
- .decode()
- .strip()
- )
- except subprocess.CalledProcessError:
- raise RuntimeError("Not a git repository (or git not installed)")
-
-
-def find_cargo_dirs():
- repo_root = get_git_root()
- cargo_dirs = []
- for root, dirs, files in os.walk(repo_root):
- # Skip the current directory
- if os.path.abspath(root) == os.path.abspath(repo_root):
- continue
- if "Cargo.toml" in files:
- cargo_dirs.append(root)
- return cargo_dirs
-
-
-def check_deps():
- cargo_dirs = find_cargo_dirs()
- for root in cargo_dirs:
- print(f"Checking dependencies of {root}")
- subprocess.run(["cargo", "deny", "check", "license"], cwd=root, check=True)
-
-
-def generate_deps():
- cargo_dirs = find_cargo_dirs()
- for root in cargo_dirs:
- print(f"Generating dependencies {root}")
- try:
- result = subprocess.run(
- ["cargo", "deny", "list", "-f", "tsv", "-t", "0.6"],
- cwd=root,
- capture_output=True,
- text=True,
- check=True,
- )
- except subprocess.CalledProcessError as e:
- raise RuntimeError(
- f"Failed to run 'cargo deny' in '{root}'. "
- f"Is it installed?\n\nSTDERR:\n{e.stderr}"
- ) from e
-
- with open(f"{root}/DEPENDENCIES.rust.tsv", "w") as f:
- f.write(result.stdout)
-
-
-if __name__ == "__main__":
- parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
- parser.set_defaults(func=parser.print_help)
- subparsers = parser.add_subparsers()
-
- parser_check = subparsers.add_parser(
- "check", description="Check dependencies", help="Check dependencies"
- )
- parser_check.set_defaults(func=check_deps)
-
- parser_generate = subparsers.add_parser(
- "generate", description="Generate dependencies", help="Generate dependencies"
- )
- parser_generate.set_defaults(func=generate_deps)
-
- args = parser.parse_args()
- arg_dict = dict(vars(args))
- del arg_dict["func"]
- args.func(**arg_dict)
diff --git a/scripts/release.sh b/scripts/release.sh
deleted file mode 100755
index e4cee342d..000000000
--- a/scripts/release.sh
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/bin/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 -e
-
-if [ -z "${ICEBERG_VERSION}" ]; then
- echo "ICEBERG_VERSION is unset"
- exit 1
-else
- echo "var is set to '$ICEBERG_VERSION'"
-fi
-
-# Validate version format (e.g., 1.0.0)
-if [[ ! "$ICEBERG_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
- echo "Error: ICEBERG_VERSION ($ICEBERG_VERSION) must be in the format: <number>.<number>.<number>"
- exit 1
-fi
-
-# tar source code
-release_version=${ICEBERG_VERSION}
-# rc versions
-rc_version="${ICEBERG_VERSION_RC:-rc.1}"
-# Corresponding git repository branch
-git_branch=release-${release_version}-${rc_version}
-
-rm -rf dist
-mkdir -p dist/
-
-echo "> Checkout version branch"
-git checkout -B "${git_branch}"
-
-# Run a few checks
-echo "> Check license"
-docker run -it --rm -v $(pwd):/github/workspace apache/skywalking-eyes header check
-
-echo "> Run dependency license check using cargo-deny"
-python3 ./scripts/dependencies.py check
-
-# Generate and verify artifacts
-echo "> Start package"
-git archive --format=tar.gz --output="dist/apache-iceberg-rust-$release_version.tar.gz" --prefix="apache-iceberg-rust-$release_version/" --add-file=Cargo.toml "$git_branch"
-
-cd dist
-
-echo "> Generate signature"
-for i in *.tar.gz; do
- echo "$i"
- gpg --armor --output "$i.asc" --detach-sig "$i"
-done
-echo "> Check signature"
-for i in *.tar.gz; do
- echo "$i"
- gpg --verify "$i.asc" "$i"
-done
-echo "> Generate sha512sum"
-for i in *.tar.gz; do
- echo "$i"
- sha512sum "$i" >"$i.sha512"
-done
-echo "> Check sha512sum"
-for i in *.tar.gz; do
- echo "$i"
- sha512sum --check "$i.sha512"
-done
diff --git a/scripts/verify.py b/scripts/verify.py
deleted file mode 100755
index 415bb243a..000000000
--- a/scripts/verify.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env python3
-# 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.
-
-
-import subprocess
-import sys
-import os
-
-BASE_DIR = os.getcwd()
-
-
-def check_rust():
- try:
- subprocess.run(["cargo", "--version"], check=True)
- return True
- except FileNotFoundError:
- return False
- except Exception as e:
- raise Exception("Check rust met unexpected error", e)
-
-def build_core():
- print("Start building iceberg rust")
-
- subprocess.run(["cargo", "build", "--release"], check=True)
-
-def main():
- if not check_rust():
- print(
- "Cargo is not found, please check if rust development has been setup correctly"
- )
- print("Visit https://www.rust-lang.org/tools/install for more information")
- sys.exit(1)
-
- build_core()
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/website/src/release.md b/website/src/release.md
index 1c062e983..f51162133 100644
--- a/website/src/release.md
+++ b/website/src/release.md
@@ -17,7 +17,7 @@
~ under the License.
-->
-This document mainly introduces how the release manager releases a new version in accordance with the Apache requirements.
+This document explains how the release manager releases Apache Iceberg Rust in accordance with Apache requirements.
## Introduction
@@ -31,15 +31,17 @@ This guide complements the foundation-wide policies and guides:
- [Release Distribution Policy](https://infra.apache.org/release-distribution)
- [Release Creation Process](https://infra.apache.org/release-publishing.html)
-## Some Terminology of release
+## Terminology
-In the context of our release, we use several terms to describe different stages of the release process.
+In this guide:
-Here's an explanation of these terms:
+- `iceberg_version`: the final Iceberg Rust version, like `0.9.1`.
+- `rc`: the numeric release candidate voting round, like `2`.
+- `rc_tag`: the git tag for a release candidate, like `v0.9.1-rc.2`.
+- `rc_dist_dir`: the ASF dev distribution directory, like `apache-iceberg-rust-0.9.1-rc2`.
+- `source_archive`: the source tarball, like `apache-iceberg-rust-0.9.1.tar.gz`.
-- `iceberg_version`: the version of Iceberg to be released, like `0.2.0`.
-- `rc_version`: the minor version for voting round, like `rc.1`.
-- `release_version`: the version of release candidate, like `0.2.0-rc.1`.
+The RC tag includes `rc.<number>`. The ASF dev distribution directory uses `rc<number>`. The source archive name uses only the final version.
## Preparation
@@ -50,6 +52,16 @@ This section is the requirements for individuals who are new to the role of rele
</div>
Refer to [Setup GPG Key](reference/setup_gpg.md) to make sure the GPG key has been set up.
+The RC creation script requires a local GPG secret key when artifact signing or tag creation is enabled.
+
+Install the release tooling used by the local scripts:
+
+- `cargo-deny`
+- `docker`
+- `gpg`
+- `svn`
+
+The local release helpers are under `dev/release/`. They log every step before it runs and after it succeeds. If a step fails, the script prints the failed step and stops.
## Start a tracking issue about the next release
@@ -79,11 +91,11 @@ This issue is used to track tasks of the iceberg rust ${iceberg_version} release
- [ ] Bump version in project
- [ ] Update docs
- [ ] Generate dependencies list
-- [ ] Push release candidate tag to GitHub
+- [ ] Create and push release candidate tag
#### ASF Side
-- [ ] Create an ASF Release
+- [ ] Create ASF source release artifacts
- [ ] Upload artifacts to the SVN dist repo
### Voting
@@ -104,120 +116,153 @@ For details of each step, please refer to: https://rust.iceberg.apache.org/relea
### Bump version in project
-Bump all components' version in the project to the new iceberg version.
-Please note that this version is the exact version of the release, not the release candidate version.
+Bump all components' version in the project to the new Iceberg Rust version.
+This version is the final version, not the release candidate version.
-- rust core and python binding: bump version in root `Cargo.toml` under `[workspace.package]`
+- Rust core and Python binding: bump version in root `Cargo.toml` under `[workspace.package]`.
### Update docs
-- Update `CHANGELOG.md` by Drafting a new release [note on Github Releases](https://github.com/apache/iceberg-rust/releases/new)
+Update `CHANGELOG.md` by drafting a new release [note on GitHub Releases](https://github.com/apache/iceberg-rust/releases/new).
### Generate dependencies list
-Download and setup `cargo-deny`. You can refer to [cargo-deny](https://embarkstudios.github.io/cargo-deny/cli/index.html).
+Download and set up `cargo-deny`. You can refer to [cargo-deny](https://embarkstudios.github.io/cargo-deny/cli/index.html).
+For example:
+
+```shell
+cargo install cargo-deny
+```
+
+Run the following command to update the dependencies list of every package:
+
+```shell
+dev/release/dependencies.sh generate
+```
-Run `python3 ./scripts/dependencies.py generate` to update the dependencies list of every package.
+Run the following command to verify the updated dependencies' license:
-Run `python3 ./scripts/dependencies.py check` to verify the updated dependencies' license.
+```shell
+dev/release/dependencies.sh check
+```
-### Push release candidate tag
+### Create release candidate tag and artifacts
-After bump version PR gets merged, we can create a GitHub release for the release candidate:
+After the version bump PR gets merged, check out the exact commit to release and run:
-- Create a tag at `main` branch on the `Bump Version` / `Patch up version` commit: `git tag -s "v${release_version}"`, please correctly check out the corresponding commit instead of directly tagging on the main branch.
-- Push tags to GitHub: `git push origin "v${release_version}"`.
+```shell
+dev/release/create_rc.sh ${iceberg_version} ${rc}
+```
-## ASF Side
+For example:
-If any step in the ASF Release process fails and requires code changes,
-we will abandon that version and prepare for the next one.
-Our release page will only display ASF releases instead of GitHub Releases.
+```shell
+dev/release/create_rc.sh 0.9.1 2
+```
-### Create an ASF Release
+Useful options include:
-After GitHub Release has been created, we can start to create ASF Release.
+- `--release_ref HEAD`: git commit-ish to archive and tag.
+- `--dist_dir dist`: artifact output root.
+- `--create_rc_tag 1`: create the signed annotated RC tag as the final release step.
+- `--check_headers 1`: check Apache license headers against the source archive.
+- `--check_deps 1`: run dependency license checks before artifact creation.
+- `--sign 1`: create and verify the detached GPG signature.
+- `--upload_svn 0`: upload RC artifacts to the ASF dev dist SVN repository.
+- `--svn_dist_url https://dist.apache.org/repos/dist/dev/iceberg`: SVN directory URL where the RC artifact directory will be uploaded.
-- Checkout to released tag. (e.g. `git checkout v0.2.0-rc.1`, tag is created in the previous step)
-- Use the release script to create a new release: `ICEBERG_VERSION=<iceberg_version> ICEBERG_VERSION_RC=<rc_version> ./scripts/release.sh`(e.g. `ICEBERG_VERSION=0.2.0 ICEBERG_VERSION_RC=rc.1 ./scripts/release.sh`)
- - This script will do the following things:
- - Create a new branch named by `release-${release_version}` from the tag
- - Generate the release candidate artifacts under `dist`, including:
- - `apache-iceberg-rust-${release_version}.tar.gz`
- - `apache-iceberg-rust-${release_version}.tar.gz.asc`
- - `apache-iceberg-rust-${release_version}.tar.gz.sha512`
- - Check the header of the source code. This step needs docker to run.
+This script creates:
-This script will create a new release under `dist`.
+- Local artifact directory: `dist/apache-iceberg-rust-${iceberg_version}-rc${rc}/`
+- Source archive: `apache-iceberg-rust-${iceberg_version}.tar.gz`
+- Signature: `apache-iceberg-rust-${iceberg_version}.tar.gz.asc`
+- SHA-512 checksum: `apache-iceberg-rust-${iceberg_version}.tar.gz.sha512`
+- Signed annotated RC tag: `v${iceberg_version}-rc.${rc}`
-For example:
+The script checks license headers against the generated source archive, not the live Git worktree. If enabled, SVN upload runs after local artifact verification and before RC tag creation. The script creates the signed RC tag as the final release step, then prints a draft VOTE email for `dev@iceberg.apache.org`.
+
+To upload artifacts to ASF dev dist as part of RC creation, pass:
```shell
-> tree dist
-dist
-├── apache-iceberg-rust-0.2.0.tar.gz
-├── apache-iceberg-rust-0.2.0.tar.gz.asc
-└── apache-iceberg-rust-0.2.0.tar.gz.sha512
+dev/release/create_rc.sh ${iceberg_version} ${rc} --upload_svn 1
```
-It is recommended to verify the artifacts yourself before uploading them to the SVN dist repo, see [How to verify a release](#how-to-verify-a-release)
-### Upload artifacts to the SVN dist repo
+The script does not push the RC tag. Review the output, then push the tag manually:
-SVN is required for this step.
+```shell
+git push origin "v${iceberg_version}-rc.${rc}"
+```
-The svn repository of the dev branch is: <https://dist.apache.org/repos/dist/dev/iceberg/>
+If an RC has a problem, abandon that RC and increment the RC number.
-First, checkout Iceberg to local directory:
+## ASF Side
+
+If any step in the ASF release process fails and requires code changes, abandon that RC and prepare a new RC number.
+Our release page displays ASF releases instead of GitHub Releases.
+
+### Verify the release candidate locally
+
+Before uploading artifacts to ASF dev dist, verify the local artifacts:
```shell
-# As this step will copy all the versions, it will take some time. If the network is broken, please use svn cleanup to delete the lock before re-execute it.
-svn co https://dist.apache.org/repos/dist/dev/iceberg/ /tmp/iceberg-dist-dev
+dev/release/verify_rc.sh ${iceberg_version} ${rc} --download 0
```
-Then, upload the artifacts:
+To skip expensive build steps during a quick local check:
-> The `${release_version}` here should be like `0.2.0-rc1`
+```shell
+dev/release/verify_rc.sh ${iceberg_version} ${rc} --download 0 --build 0 --python 0
+```
-Example of uploaded artifacts can be found at: https://dist.apache.org/repos/dist/dev/iceberg/apache-iceberg-rust-0.9.1-rc3/
+### Upload artifacts to the SVN dist repo
-```shell
-# create a directory named by version
-mkdir /tmp/iceberg-dist-dev/apache-iceberg-rust-${release_version}/
+SVN is required for this step.
-# copy source code and signature package to the versioned directory
-cp ./dist/* /tmp/iceberg-dist-dev/apache-iceberg-rust-${release_version}/
+The SVN repository of the dev branch is: <https://dist.apache.org/repos/dist/dev/iceberg/>
-# change dir to the svn folder
-cd /tmp/iceberg-dist-dev/
+First, check out Iceberg to a local directory:
-# check svn status
-svn status
+```shell
+svn co https://dist.apache.org/repos/dist/dev/iceberg/ /tmp/iceberg-dist-dev
```
+
+If the artifacts were not uploaded by `dev/release/create_rc.sh --upload_svn 1`, upload them manually:
+
```shell
-# add to svn
-svn add apache-iceberg-rust-${release_version}
+rc_dist_dir="apache-iceberg-rust-${iceberg_version}-rc${rc}"
+
+mkdir "/tmp/iceberg-dist-dev/${rc_dist_dir}/"
+cp "./dist/${rc_dist_dir}/"* "/tmp/iceberg-dist-dev/${rc_dist_dir}/"
-# commit to SVN remote server
-svn commit -m "Prepare for iceberg-rust ${release_version}"
+cd /tmp/iceberg-dist-dev/
+svn status
+svn add "${rc_dist_dir}"
+svn commit -m "Prepare Apache Iceberg Rust ${iceberg_version} RC${rc}"
```
Visit <https://dist.apache.org/repos/dist/dev/iceberg/> to make sure the artifacts are uploaded correctly.
+### Verify the uploaded release candidate
+
+After uploading the artifacts, verify them from ASF dev dist:
+
+```shell
+dev/release/verify_rc.sh ${iceberg_version} ${rc}
+```
+
### Rescue
-If you accidentally published wrong or unexpected artifacts, like wrong signature files, wrong sha256 files,
-please cancel the release for the current `release_version`,
-_increase th RC counting_ and re-initiate a release with the new `release_version`.
-And remember to delete the wrong artifacts from the SVN dist repo.
+If you accidentally publish wrong or unexpected artifacts, like wrong signature files or checksum files, cancel the current RC, increment the RC number, and initiate a new release candidate.
+Remember to delete the wrong artifacts from the SVN dist repo.
## Voting
-Iceberg Community Vote should send email to: <de...@iceberg.apache.org>:
+Send the Iceberg community VOTE email to <de...@iceberg.apache.org>.
Title:
```
-[VOTE] Release Apache Iceberg Rust ${iceberg_version} ${rc_version}
+[VOTE] Release Apache Iceberg Rust ${iceberg_version} RC${rc}
```
Content:
@@ -227,11 +272,11 @@ Hello Apache Iceberg Rust Community,
This is a call for a vote to release Apache Iceberg Rust version ${iceberg_version}.
-The tag to be voted on is: v${release_version}.
+The tag to be voted on is: v${iceberg_version}-rc.${rc}.
The release candidate:
-https://dist.apache.org/repos/dist/dev/iceberg/apache-iceberg-rust-${release_version}/
+https://dist.apache.org/repos/dist/dev/iceberg/apache-iceberg-rust-${iceberg_version}-rc${rc}/
Keys to verify the release candidate:
@@ -239,7 +284,7 @@ https://downloads.apache.org/iceberg/KEYS
Git tag for the release:
-https://github.com/apache/iceberg-rust/releases/tag/v${release_version}
+https://github.com/apache/iceberg-rust/releases/tag/v${iceberg_version}-rc.${rc}
Please download, verify, and test the release candidate.
@@ -271,12 +316,12 @@ ${name}
Example: <https://lists.apache.org/thread/c211gqq2yl15jbxqk4rcnq1bdqltjm5l>
-After at least 3 `+1` binding vote (from Iceberg PMC member), claim the vote result:
+After at least 3 `+1` binding votes from Iceberg PMC members, claim the vote result.
Title:
```
-[RESULT][VOTE] Release Apache Iceberg Rust ${iceberg_version} ${rc_version}
+[RESULT][VOTE] Release Apache Iceberg Rust ${iceberg_version} RC${rc}
```
Content:
@@ -284,7 +329,7 @@ Content:
```
Hello Apache Iceberg Rust Community,
-The vote to release Apache Iceberg Rust ${release_version} has passed.
+The vote to release Apache Iceberg Rust ${iceberg_version} RC${rc} has passed.
The vote PASSED with 3 +1 binding and 1 +1 non-binding votes, no +0 or -1 votes:
@@ -308,45 +353,64 @@ Example: <https://lists.apache.org/thread/xk5myl10mztcfotn59oo59s4ckvojds6>
## How to verify a release
-### Validating a source release
+### Validate with the helper script
+
+Run:
-A release contains links to following things:
+```shell
+dev/release/verify_rc.sh ${iceberg_version} ${rc}
+```
+
+The helper downloads the source archive, signature, and checksum from ASF dev dist, verifies the signature with the local GPG keyring, verifies the checksum, extracts the archive, checks source headers, and runs Rust and Python build/tests.
+
+To import Apache Iceberg release keys before signature verification, run:
+
+```shell
+dev/release/verify_rc.sh ${iceberg_version} ${rc} --import_gpg_keys 1
+```
+
+### Validate manually
-* A source tarball
-* A signature(.asc)
-* A checksum(.sha512)
+A release candidate contains links to following things:
+
+- A source tarball
+- A signature (`.asc`)
+- A checksum (`.sha512`)
After downloading them, here are the instructions on how to verify them.
-* Import keys:
+- Import keys:
```bash
curl https://downloads.apache.org/iceberg/KEYS -o KEYS
gpg --import KEYS
```
-* Verify the `.asc` file:
-
+
+- Verify the `.asc` file:
+
```bash
gpg --verify apache-iceberg-rust-*.tar.gz.asc
```
+
Expects: `gpg: Good signature from ...`
-* Verify the checksums:
-
+
+- Verify the checksum:
+
```bash
shasum -a 512 -c apache-iceberg-rust-*.tar.gz.sha512
```
+
Expects: `"apache-iceberg-rust-...tar.gz: OK"`
-* Verify build and test:
-
+
+- Verify build and test:
+
```bash
tar -xzf apache-iceberg-rust-*.tar.gz
cd apache-iceberg-rust-*/
- ```
-
- ```bash
make build && make test
```
-* Verify pyiceberg-core build and tests:
+
+- Verify pyiceberg-core build and tests:
```bash
(
@@ -355,32 +419,41 @@ After downloading them, here are the instructions on how to verify them.
make test
)
```
-* Verify license headers:
-
+
+- Verify license headers:
+
```bash
- docker run -it --rm -v $(pwd):/github/workspace apache/skywalking-eyes header check
+ docker run --rm -v $(pwd):/github/workspace apache/skywalking-eyes header check
```
- Expects: `INFO Totally checked _ files, valid: _, invalid: 0, ignored: _, fixed: 0 `
+
+ Expects: `INFO Totally checked _ files, valid: _, invalid: 0, ignored: _, fixed: 0`
## Official Release
-### Push the release git tag
+### Promote the RC
+
+After the VOTE passes, create the final release tag and move the ASF artifacts from dev dist to release dist:
```shell
-# Checkout the tags that passed VOTE
-git checkout "v${release_version}"
-# Tag with the iceberg version
-git tag -s "v${iceberg_version}"
-# Push tags to github to trigger releases
-git push origin "v${iceberg_version}"
+dev/release/release.sh ${iceberg_version} ${rc}
```
-### Publish artifacts to SVN RELEASE branch
+Useful options include:
+
+- `--create_release_tag 1`: create the signed annotated final release git tag.
+- `--move_svn 1`: move the RC artifacts from ASF dev dist to ASF release dist.
+- `--tag_ref <rc tag commit>`: git commit-ish to tag as the final release.
+- `--dev_dist_url https://dist.apache.org/repos/dist/dev/iceberg`: SVN directory URL containing RC artifact directories.
+- `--release_dist_url https://dist.apache.org/repos/dist/release/iceberg`: SVN directory URL where final release artifact directories are published.
+
+The release script does not push the final release tag. Review the output, then push the tag manually:
```shell
-svn mv https://dist.apache.org/repos/dist/dev/iceberg/apache-iceberg-rust-${release_version} https://dist.apache.org/repos/dist/release/iceberg/apache-iceberg-rust-${iceberg_version} -m "Release Apache Iceberg Rust ${iceberg_version}"
+git push origin "v${iceberg_version}"
```
+Pushing the final release tag triggers the publish workflow for crates and pyiceberg-core.
+
### Create a GitHub Release
- Click [here](https://github.com/apache/iceberg-rust/releases/new) to create a new release.
@@ -394,7 +467,7 @@ svn mv https://dist.apache.org/repos/dist/dev/iceberg/apache-iceberg-rust-${rele
Send the release announcement to `dev@iceberg.apache.org` and CC `announce@apache.org`.
-Instead of adding breaking changes, let's include the new features as "notable changes" in the announcement.
+Instead of adding breaking changes, include the new features as "notable changes" in the announcement.
Title:
@@ -413,7 +486,7 @@ that Apache Iceberg Rust ${iceberg_version} has been released!
Iceberg is a data access layer that allows users to easily and efficiently
retrieve data from various storage services in a unified way.
-The notable changes since ${iceberg_version} include:
+The notable changes since the previous release include:
1. xxxxx
2. yyyyyy
3. zzzzzz
@@ -425,7 +498,7 @@ Apache Iceberg Rust website: https://rust.iceberg.apache.org/
Download Links: https://rust.iceberg.apache.org/download
-From official ASF distribution: https://dist.apache.org/repos/dist/release/iceberg/apache-iceberg-rust-${release_version}/
+From official ASF distribution: https://dist.apache.org/repos/dist/release/iceberg/apache-iceberg-rust-${iceberg_version}/
Iceberg Resources:
- Issue: https://github.com/apache/iceberg-rust/issues