| #!/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. |
| |
| # ============================================================================= |
| # Apache Hamilton Release Candidate Validation Script |
| # |
| # Validates an Apache Hamilton release candidate by: |
| # 1. Downloading artifacts from Apache SVN |
| # 2. Importing signing keys and verifying GPG signatures |
| # 3. Verifying SHA512 checksums |
| # 4. Checking Apache license headers with Apache RAT |
| # 5. Building from source |
| # 6. Running unit tests and plugin tests |
| # 7. Running representative examples |
| # |
| # Prerequisites: svn, gpg, java (for RAT), uv, curl |
| # |
| # Usage: |
| # ./verification-script.sh # defaults: version=1.90.0 rc=0 |
| # ./verification-script.sh 1.90.0 0 # explicit version and RC |
| # ./verification-script.sh 1.90.0 1 # validate RC1 |
| # ============================================================================= |
| |
| set -euo pipefail |
| |
| VERSION="${1:-1.90.0}" |
| RC="${2:-0}" |
| PACKAGE="apache-hamilton" |
| PYTHON_VERSION="${PYTHON_VERSION:-3.11}" |
| |
| # Derived names |
| SRC_TAR="${PACKAGE}-${VERSION}-incubating-src.tar.gz" |
| WHEEL_NAME="$(echo "${PACKAGE}" | tr '-' '_')-${VERSION}-py3-none-any.whl" |
| EXTRACTED_DIR="$(echo "${PACKAGE}" | tr '-' '_')-${VERSION}" |
| WORK_DIR="hamilton-rc${RC}-validation" |
| RAT_VERSION="0.15" |
| RAT_JAR="apache-rat-${RAT_VERSION}.jar" |
| |
| # Updated .rat-excludes content (from commit 518a516) |
| RAT_EXCLUDES_CONTENT='# Requirements/data files (not source code) |
| .*\.txt |
| .*\.jsonl |
| |
| # SPDX short license template (is itself a license reference) |
| SHORT_LICENSE\.md |
| |
| # Data files (not source code) |
| .*\.json |
| .*\.csv |
| .*\.fwf |
| .*\.xml |
| |
| # Git and version control config |
| \.gitignore |
| \.rat-excludes |
| |
| # Third-party vendored code with its own license (MIT) |
| .*databackend\.py |
| |
| # PKG-INFO (generated metadata) |
| PKG-INFO' |
| |
| # Colors for output |
| RED='\033[0;31m' |
| GREEN='\033[0;32m' |
| YELLOW='\033[1;33m' |
| BLUE='\033[0;34m' |
| NC='\033[0m' # No Color |
| |
| PASS_COUNT=0 |
| FAIL_COUNT=0 |
| WARN_COUNT=0 |
| |
| pass_check() { |
| echo -e " ${GREEN}PASS${NC}: $1" |
| PASS_COUNT=$((PASS_COUNT + 1)) |
| } |
| |
| fail_check() { |
| echo -e " ${RED}FAIL${NC}: $1" |
| FAIL_COUNT=$((FAIL_COUNT + 1)) |
| } |
| |
| warn_check() { |
| echo -e " ${YELLOW}WARN${NC}: $1" |
| WARN_COUNT=$((WARN_COUNT + 1)) |
| } |
| |
| section() { |
| echo "" |
| echo -e "${BLUE}================================================================${NC}" |
| echo -e "${BLUE} $1${NC}" |
| echo -e "${BLUE}================================================================${NC}" |
| echo "" |
| } |
| |
| # ============================================================================= |
| # Check prerequisites |
| # ============================================================================= |
| section "Checking Prerequisites" |
| |
| for cmd in svn gpg java curl uv; do |
| if command -v "$cmd" &>/dev/null; then |
| pass_check "$cmd found: $(command -v "$cmd")" |
| else |
| fail_check "$cmd not found - please install it" |
| fi |
| done |
| |
| if [ "$FAIL_COUNT" -gt 0 ]; then |
| echo -e "\n${RED}Missing prerequisites. Please install them and retry.${NC}" |
| exit 1 |
| fi |
| |
| # Ensure GPG can prompt for passphrase |
| export GPG_TTY=$(tty) |
| |
| # ============================================================================= |
| # Step 1: Download artifacts |
| # ============================================================================= |
| section "Step 1: Downloading Artifacts from SVN" |
| |
| if [ -d "$WORK_DIR" ]; then |
| echo "Removing existing work directory: $WORK_DIR" |
| rm -rf "$WORK_DIR" |
| fi |
| |
| SVN_URL="https://dist.apache.org/repos/dist/dev/incubator/hamilton/${PACKAGE}/${VERSION}-RC${RC}/" |
| echo "SVN URL: $SVN_URL" |
| |
| if svn export "$SVN_URL" "$WORK_DIR" 2>&1; then |
| pass_check "Artifacts downloaded from SVN" |
| else |
| fail_check "Failed to download artifacts from SVN" |
| exit 1 |
| fi |
| |
| cd "$WORK_DIR" |
| echo "" |
| echo "Downloaded artifacts:" |
| ls -la |
| |
| # ============================================================================= |
| # Step 2: Import KEYS |
| # ============================================================================= |
| section "Step 2: Importing Signing Keys" |
| |
| curl -sO https://downloads.apache.org/incubator/hamilton/KEYS |
| if gpg --import KEYS 2>&1; then |
| pass_check "KEYS imported into GPG keyring" |
| else |
| fail_check "Failed to import KEYS" |
| fi |
| |
| # ============================================================================= |
| # Step 3: Verify GPG Signatures |
| # ============================================================================= |
| section "Step 3: Verifying GPG Signatures" |
| |
| for artifact in "$SRC_TAR" "$WHEEL_NAME"; do |
| if [ -f "$artifact" ] && [ -f "${artifact}.asc" ]; then |
| if gpg --verify "${artifact}.asc" "$artifact" 2>&1; then |
| pass_check "GPG signature valid: $artifact" |
| else |
| fail_check "GPG signature INVALID: $artifact" |
| fi |
| else |
| fail_check "Missing artifact or signature: $artifact" |
| fi |
| done |
| |
| # ============================================================================= |
| # Step 4: Verify SHA512 Checksums |
| # ============================================================================= |
| section "Step 4: Verifying SHA512 Checksums" |
| |
| for artifact in "$SRC_TAR" "$WHEEL_NAME"; do |
| if [ -f "$artifact" ] && [ -f "${artifact}.sha512" ]; then |
| expected=$(tr -d '[:space:]' < "${artifact}.sha512") |
| actual=$(shasum -a 512 "$artifact" | awk '{print $1}') |
| if [ "$expected" = "$actual" ]; then |
| pass_check "SHA512 checksum matches: $artifact" |
| else |
| fail_check "SHA512 checksum MISMATCH: $artifact" |
| echo " Expected: $expected" |
| echo " Actual: $actual" |
| fi |
| else |
| fail_check "Missing artifact or checksum file: $artifact" |
| fi |
| done |
| |
| # ============================================================================= |
| # Step 5: Extract and Set Up Environment |
| # ============================================================================= |
| section "Step 5: Extracting Source and Setting Up Environment" |
| |
| tar -xzf "$SRC_TAR" |
| if [ -d "$EXTRACTED_DIR" ]; then |
| pass_check "Source archive extracted: $EXTRACTED_DIR" |
| else |
| fail_check "Extraction failed - directory not found: $EXTRACTED_DIR" |
| exit 1 |
| fi |
| |
| cd "$EXTRACTED_DIR" |
| |
| # Check required files |
| for f in LICENSE NOTICE DISCLAIMER README.md pyproject.toml; do |
| if [ -f "$f" ]; then |
| pass_check "Required file present: $f" |
| else |
| fail_check "Required file missing: $f" |
| fi |
| done |
| |
| # Set up Python environment |
| echo "" |
| echo "Setting up Python $PYTHON_VERSION environment..." |
| uv venv --python "$PYTHON_VERSION" --quiet 2>&1 |
| uv sync --group release 2>&1 |
| pass_check "Python environment created and release deps installed" |
| |
| # ============================================================================= |
| # Step 6: Apache RAT License Check |
| # ============================================================================= |
| section "Step 6: Checking License Headers with Apache RAT" |
| |
| curl -sO "https://repo1.maven.org/maven2/org/apache/rat/apache-rat/${RAT_VERSION}/apache-rat-${RAT_VERSION}.jar" |
| |
| if [ -f "$RAT_JAR" ]; then |
| pass_check "Apache RAT downloaded" |
| else |
| fail_check "Failed to download Apache RAT" |
| fi |
| |
| # Write the updated .rat-excludes |
| echo "$RAT_EXCLUDES_CONTENT" > .rat-excludes |
| |
| # Copy artifacts into dist/ for the verification script |
| mkdir -p dist |
| cp ../"$SRC_TAR"* dist/ |
| cp ../"$WHEEL_NAME"* dist/ |
| |
| # Run verification script |
| echo "" |
| echo "Running automated verification script..." |
| if uv run python scripts/verify_apache_artifacts.py all --rat-jar "$RAT_JAR" 2>&1; then |
| pass_check "Automated verification (signatures + checksums + licenses)" |
| else |
| fail_check "Automated verification failed" |
| fi |
| |
| # Twine check |
| echo "" |
| if uv run python scripts/verify_apache_artifacts.py twine-check 2>&1; then |
| pass_check "Twine wheel metadata validation" |
| else |
| fail_check "Twine check failed" |
| fi |
| |
| # ============================================================================= |
| # Step 7: Build from Source |
| # ============================================================================= |
| section "Step 7: Building from Source" |
| |
| if uv run flit build --no-use-vcs 2>&1; then |
| pass_check "Built sdist and wheel from source" |
| else |
| fail_check "Build from source failed" |
| fi |
| |
| # Install the freshly built wheel |
| if uv pip install "dist/${WHEEL_NAME}" --force-reinstall 2>&1; then |
| pass_check "Installed built wheel" |
| else |
| fail_check "Failed to install built wheel" |
| fi |
| |
| # Verify version |
| INSTALLED_VERSION=$(uv run python -c "import hamilton; print(hamilton.version.VERSION)" 2>&1) |
| echo "Installed version: $INSTALLED_VERSION" |
| if echo "$INSTALLED_VERSION" | grep -q "${VERSION%%.*}"; then |
| pass_check "Version check: $INSTALLED_VERSION" |
| else |
| warn_check "Version string may not match expected: $INSTALLED_VERSION" |
| fi |
| |
| # ============================================================================= |
| # Step 8: Run Unit Tests |
| # ============================================================================= |
| section "Step 8: Running Unit Tests" |
| |
| uv sync --group test 2>&1 |
| |
| echo "" |
| echo "Running core unit tests..." |
| # The plotly static writer test requires a working chromium/kaleido setup and |
| # can leave zombie processes that hang the script, so we skip it here. |
| if uv run pytest tests/ -q \ |
| --deselect tests/plugins/test_plotly_extensions.py::test_plotly_static_writer \ |
| --timeout=120 2>&1; then |
| pass_check "Core unit tests" |
| else |
| fail_check "Core unit tests (check output above)" |
| fi |
| |
| echo "" |
| echo "Running plugin tests (skipping ray/spark/vaex - optional deps)..." |
| if uv run pytest plugin_tests/ -q \ |
| --ignore=plugin_tests/h_ray \ |
| --ignore=plugin_tests/h_spark \ |
| --ignore=plugin_tests/h_vaex \ |
| --timeout=120 2>&1; then |
| pass_check "Plugin tests (polars, pandas, etc.)" |
| else |
| warn_check "Some plugin tests failed (check output above)" |
| fi |
| |
| # ============================================================================= |
| # Step 9: Run Examples |
| # ============================================================================= |
| section "Step 9: Running Representative Examples" |
| |
| # Hello World |
| echo "--- Hello World ---" |
| if uv run python examples/hello_world/my_script.py 2>&1; then |
| pass_check "Example: hello_world" |
| else |
| fail_check "Example: hello_world" |
| fi |
| |
| # Reusing Functions |
| echo "" |
| echo "--- Reusing Functions ---" |
| if uv run python examples/reusing_functions/main.py 2>&1; then |
| pass_check "Example: reusing_functions" |
| else |
| fail_check "Example: reusing_functions" |
| fi |
| |
| # Schema |
| echo "" |
| echo "--- Schema ---" |
| if uv run python examples/schema/dataflow.py 2>&1; then |
| pass_check "Example: schema" |
| else |
| fail_check "Example: schema" |
| fi |
| |
| # Data Quality (needs to run from its directory for CSV path) |
| echo "" |
| echo "--- Data Quality ---" |
| ORIG_DIR=$(pwd) |
| cd examples/data_quality/simple |
| if uv run python run.py 2>&1; then |
| pass_check "Example: data_quality" |
| else |
| fail_check "Example: data_quality" |
| fi |
| cd "$ORIG_DIR" |
| |
| # Pandas Materialization |
| echo "" |
| echo "--- Pandas Materialization ---" |
| if uv run python examples/pandas/materialization/my_script.py 2>&1; then |
| pass_check "Example: pandas/materialization" |
| else |
| fail_check "Example: pandas/materialization" |
| fi |
| |
| # ============================================================================= |
| # Summary |
| # ============================================================================= |
| section "Validation Summary for ${PACKAGE} ${VERSION}-incubating RC${RC}" |
| |
| echo -e " ${GREEN}Passed: ${PASS_COUNT}${NC}" |
| echo -e " ${RED}Failed: ${FAIL_COUNT}${NC}" |
| echo -e " ${YELLOW}Warnings: ${WARN_COUNT}${NC}" |
| echo "" |
| |
| if [ "$FAIL_COUNT" -eq 0 ]; then |
| echo -e "${GREEN}All checks passed! This RC looks good for a +1 (binding/non-binding) vote.${NC}" |
| echo "" |
| echo "Sample vote email:" |
| echo "---" |
| echo "+1 (non-binding)" |
| echo "" |
| echo "I verified the following:" |
| echo "- GPG signatures and SHA512 checksums: PASS" |
| echo "- Apache RAT license headers: PASS (0 issues)" |
| echo "- Build from source with flit: PASS" |
| echo "- Unit tests: PASS" |
| echo "- Plugin tests (polars, pandas): PASS" |
| echo "- Representative examples: PASS" |
| echo "- LICENSE, NOTICE, DISCLAIMER present and correct" |
| echo "---" |
| exit 0 |
| else |
| echo -e "${RED}Some checks failed. Review the output above before voting.${NC}" |
| exit 1 |
| fi |