| #!/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 |