| | #!/bin/bash
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | set -e
|
| |
|
| |
|
| | RED='\033[0;31m'
|
| | GREEN='\033[0;32m'
|
| | YELLOW='\033[1;33m'
|
| | BLUE='\033[0;34m'
|
| | NC='\033[0m'
|
| |
|
| |
|
| | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| | PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
| | CACHE_DIR="${SCRIPT_DIR}/migration-test/cache"
|
| | WORK_DIR="${SCRIPT_DIR}/migration-test/work"
|
| | DB_FILE="${WORK_DIR}/migration-test.db"
|
| | LOG_FILE="${WORK_DIR}/migration-test.log"
|
| | PLAN_FILE="${WORK_DIR}/migration-plan.json"
|
| |
|
| |
|
| | E2E_PORT=8099
|
| |
|
| |
|
| | DB_TYPE="sqlite"
|
| | MYSQL_CONTAINER="axonhub-migration-mysql"
|
| | MYSQL_PORT=13306
|
| | MYSQL_ROOT_PASSWORD="axonhub_test_root"
|
| | MYSQL_DATABASE="axonhub_e2e"
|
| | MYSQL_USER="axonhub"
|
| | MYSQL_PASSWORD="axonhub_test"
|
| |
|
| | POSTGRES_CONTAINER="axonhub-migration-postgres"
|
| | POSTGRES_PORT=15432
|
| | POSTGRES_DATABASE="axonhub_e2e"
|
| | POSTGRES_USER="axonhub"
|
| | POSTGRES_PASSWORD="axonhub_test"
|
| |
|
| |
|
| | INIT_OWNER_EMAIL="${AXONHUB_INIT_OWNER_EMAIL:-owner@example.com}"
|
| | INIT_OWNER_PASSWORD="${AXONHUB_INIT_OWNER_PASSWORD:-InitPassword123!}"
|
| | INIT_OWNER_FIRST_NAME="${AXONHUB_INIT_OWNER_FIRST_NAME:-System}"
|
| | INIT_OWNER_LAST_NAME="${AXONHUB_INIT_OWNER_LAST_NAME:-Owner}"
|
| | INIT_BRAND_NAME="${AXONHUB_INIT_BRAND_NAME:-AxonHub Migration Test}"
|
| |
|
| |
|
| | REPO="looplj/axonhub"
|
| | GITHUB_API="https://api.github.com/repos/${REPO}"
|
| |
|
| | print_info() {
|
| | echo -e "${BLUE}[INFO]${NC} $1"
|
| | }
|
| |
|
| | print_success() {
|
| | echo -e "${GREEN}[SUCCESS]${NC} $1"
|
| | }
|
| |
|
| | print_warning() {
|
| | echo -e "${YELLOW}[WARNING]${NC} $1"
|
| | }
|
| |
|
| | print_error() {
|
| | echo -e "${RED}[ERROR]${NC} $1"
|
| | }
|
| |
|
| | print_step() {
|
| | echo ""
|
| | echo -e "${GREEN}==>${NC} $1"
|
| | }
|
| |
|
| | usage() {
|
| | cat <<EOF
|
| | AxonHub Migration Test Script
|
| |
|
| | Usage:
|
| | ./migration-test.sh <from-tag> [options]
|
| |
|
| | Arguments:
|
| | from-tag Git tag to test migration from (e.g., v0.1.0)
|
| |
|
| | Options:
|
| | --db-type TYPE Database type: sqlite, mysql, postgres (default: sqlite)
|
| | --skip-download Skip downloading binary if cached version exists
|
| | --skip-e2e Skip running e2e tests after migration
|
| | --skip-init-system
|
| | Skip system initialization step (reuse existing database state)
|
| | --keep-artifacts Keep work directory after test completion
|
| | --keep-db Keep database container after test completion
|
| | -h, --help Show this help and exit
|
| |
|
| | Examples:
|
| | ./migration-test.sh v0.1.0
|
| | ./migration-test.sh v0.1.0 --db-type mysql
|
| | ./migration-test.sh v0.1.0 --db-type postgres --skip-e2e
|
| | ./migration-test.sh v0.2.0 --keep-artifacts
|
| |
|
| | Description:
|
| | This script tests database migration by:
|
| | 1. Setting up database (SQLite file or Docker container for MySQL/PostgreSQL)
|
| | 2. Downloading the binary for the specified tag from GitHub releases
|
| | 3. Initializing a database with the old version
|
| | 4. Running migration to the current branch version
|
| | 5. Executing e2e tests to verify the migration
|
| |
|
| | Supported databases:
|
| | - SQLite (default, no Docker required)
|
| | - MySQL (requires Docker, creates temporary container)
|
| | - PostgreSQL (requires Docker, creates temporary container)
|
| |
|
| | Binaries are cached in: ${CACHE_DIR}
|
| | Test artifacts are in: ${WORK_DIR}
|
| | EOF
|
| | }
|
| |
|
| | check_docker() {
|
| | if ! command -v docker >/dev/null 2>&1; then
|
| | print_error "Docker is not installed. Please install Docker to use MySQL or PostgreSQL."
|
| | exit 1
|
| | fi
|
| |
|
| | if ! docker info >/dev/null 2>&1; then
|
| | print_error "Docker daemon is not running. Please start Docker."
|
| | exit 1
|
| | fi
|
| | }
|
| |
|
| | setup_mysql() {
|
| | print_step "Setting up MySQL database" >&2
|
| |
|
| | check_docker
|
| |
|
| |
|
| | if docker ps -a --format '{{.Names}}' | grep -q "^${MYSQL_CONTAINER}$"; then
|
| | print_info "Removing existing MySQL container..." >&2
|
| | docker rm -f "$MYSQL_CONTAINER" >/dev/null 2>&1 || true
|
| | fi
|
| |
|
| |
|
| | print_info "Starting MySQL container..." >&2
|
| | docker run -d \
|
| | --name "$MYSQL_CONTAINER" \
|
| | -e MYSQL_ROOT_PASSWORD="$MYSQL_ROOT_PASSWORD" \
|
| | -e MYSQL_DATABASE="$MYSQL_DATABASE" \
|
| | -e MYSQL_USER="$MYSQL_USER" \
|
| | -e MYSQL_PASSWORD="$MYSQL_PASSWORD" \
|
| | -p "${MYSQL_PORT}:3306" \
|
| | mysql:8.0 \
|
| | --character-set-server=utf8mb4 \
|
| | --collation-server=utf8mb4_unicode_ci \
|
| | >/dev/null
|
| |
|
| |
|
| | print_info "Waiting for MySQL to be ready..." >&2
|
| | for i in {1..30}; do
|
| | if docker exec "$MYSQL_CONTAINER" mysqladmin ping -h localhost -u root -p"$MYSQL_ROOT_PASSWORD" >/dev/null 2>&1; then
|
| | print_success "MySQL is ready" >&2
|
| | return 0
|
| | fi
|
| | sleep 1
|
| | done
|
| |
|
| | print_error "MySQL failed to start" >&2
|
| | docker logs "$MYSQL_CONTAINER" >&2
|
| | exit 1
|
| | }
|
| |
|
| | setup_postgres() {
|
| | print_step "Setting up PostgreSQL database" >&2
|
| |
|
| | check_docker
|
| |
|
| |
|
| | if docker ps -a --format '{{.Names}}' | grep -q "^${POSTGRES_CONTAINER}$"; then
|
| | print_info "Removing existing PostgreSQL container..." >&2
|
| | docker rm -f "$POSTGRES_CONTAINER" >/dev/null 2>&1 || true
|
| | fi
|
| |
|
| |
|
| | print_info "Starting PostgreSQL container..." >&2
|
| | docker run -d \
|
| | --name "$POSTGRES_CONTAINER" \
|
| | -e POSTGRES_DB="$POSTGRES_DATABASE" \
|
| | -e POSTGRES_USER="$POSTGRES_USER" \
|
| | -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
|
| | -p "${POSTGRES_PORT}:5432" \
|
| | postgres:15-alpine \
|
| | >/dev/null
|
| |
|
| |
|
| | print_info "Waiting for PostgreSQL to be ready..." >&2
|
| | for i in {1..30}; do
|
| | if docker exec "$POSTGRES_CONTAINER" pg_isready -U "$POSTGRES_USER" >/dev/null 2>&1; then
|
| | print_success "PostgreSQL is ready" >&2
|
| | return 0
|
| | fi
|
| | sleep 1
|
| | done
|
| |
|
| | print_error "PostgreSQL failed to start" >&2
|
| | docker logs "$POSTGRES_CONTAINER" >&2
|
| | exit 1
|
| | }
|
| |
|
| | cleanup_database() {
|
| | if [[ "$KEEP_DB" == "true" ]]; then
|
| | print_info "Keeping database container (--keep-db specified)" >&2
|
| | return
|
| | fi
|
| |
|
| | case "$DB_TYPE" in
|
| | mysql)
|
| | if docker ps -a --format '{{.Names}}' | grep -q "^${MYSQL_CONTAINER}$"; then
|
| | print_info "Removing MySQL container..." >&2
|
| | docker rm -f "$MYSQL_CONTAINER" >/dev/null 2>&1 || true
|
| | fi
|
| | ;;
|
| | postgres)
|
| | if docker ps -a --format '{{.Names}}' | grep -q "^${POSTGRES_CONTAINER}$"; then
|
| | print_info "Removing PostgreSQL container..." >&2
|
| | docker rm -f "$POSTGRES_CONTAINER" >/dev/null 2>&1 || true
|
| | fi
|
| | ;;
|
| | sqlite)
|
| |
|
| | ;;
|
| | esac
|
| | }
|
| |
|
| | get_db_dsn() {
|
| | case "$DB_TYPE" in
|
| | sqlite)
|
| | echo "file:${DB_FILE}?cache=shared&_fk=1"
|
| | ;;
|
| | mysql)
|
| | echo "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(localhost:${MYSQL_PORT})/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=True&loc=Local"
|
| | ;;
|
| | postgres)
|
| | echo "host=localhost port=${POSTGRES_PORT} user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DATABASE} sslmode=disable"
|
| | ;;
|
| | *)
|
| | print_error "Unknown database type: $DB_TYPE" >&2
|
| | exit 1
|
| | ;;
|
| | esac
|
| | }
|
| |
|
| | get_db_dialect() {
|
| | case "$DB_TYPE" in
|
| | sqlite)
|
| | echo "sqlite3"
|
| | ;;
|
| | mysql)
|
| | echo "mysql"
|
| | ;;
|
| | postgres)
|
| | echo "postgres"
|
| | ;;
|
| | *)
|
| | print_error "Unknown database type: $DB_TYPE" >&2
|
| | exit 1
|
| | ;;
|
| | esac
|
| | }
|
| |
|
| | detect_architecture() {
|
| | local arch=$(uname -m)
|
| | local os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
| |
|
| | case $arch in
|
| | x86_64|amd64)
|
| | arch="amd64"
|
| | ;;
|
| | aarch64|arm64)
|
| | arch="arm64"
|
| | ;;
|
| | *)
|
| | print_error "Unsupported architecture: $arch"
|
| | exit 1
|
| | ;;
|
| | esac
|
| |
|
| | case $os in
|
| | linux)
|
| | os="linux"
|
| | ;;
|
| | darwin)
|
| | os="darwin"
|
| | ;;
|
| | *)
|
| | print_error "Unsupported operating system: $os"
|
| | exit 1
|
| | ;;
|
| | esac
|
| |
|
| | echo "${os}_${arch}"
|
| | }
|
| |
|
| | curl_gh() {
|
| | local url="$1"
|
| | local headers=(
|
| | -H "Accept: application/vnd.github+json"
|
| | -H "X-GitHub-Api-Version: 2022-11-28"
|
| | -H "User-Agent: axonhub-migration-test"
|
| | )
|
| | if [[ -n "$GITHUB_TOKEN" ]]; then
|
| | headers+=( -H "Authorization: Bearer $GITHUB_TOKEN" )
|
| | fi
|
| | curl -fsSL "${headers[@]}" "$url"
|
| | }
|
| |
|
| | get_asset_download_url() {
|
| | local version=$1
|
| | local platform=$2
|
| | local url=""
|
| |
|
| | print_info "Resolving asset download URL for ${version} (${platform})..." >&2
|
| |
|
| | if json=$(curl_gh "${GITHUB_API}/releases/tags/${version}" 2>/dev/null); then
|
| | if command -v jq >/dev/null 2>&1; then
|
| | url=$(echo "$json" | jq -r --arg platform "$platform" \
|
| | '.assets[]?.browser_download_url | select(test($platform)) | select(endswith(".zip"))' | head -n1)
|
| | else
|
| | url=$(echo "$json" \
|
| | | tr -d '\n\r\t' \
|
| | | sed -nE 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' \
|
| | | grep "$platform" \
|
| | | grep '\.zip$' -m 1)
|
| | fi
|
| | fi
|
| |
|
| |
|
| | if [[ -z "$url" ]]; then
|
| | print_warning "API failed, trying patterned URL..." >&2
|
| | local clean_version="${version#v}"
|
| | local filename="axonhub_${clean_version}_${platform}.zip"
|
| | local candidate="https://github.com/${REPO}/releases/download/${version}/${filename}"
|
| | if curl -fsI "$candidate" >/dev/null 2>&1; then
|
| | url="$candidate"
|
| | fi
|
| | fi
|
| |
|
| | if [[ -z "$url" ]]; then
|
| | print_error "Could not find asset for platform ${platform} in release ${version}" >&2
|
| | exit 1
|
| | fi
|
| |
|
| | echo "$url"
|
| | }
|
| |
|
| | download_binary() {
|
| | local version=$1
|
| | local platform=$2
|
| | local cache_path="${CACHE_DIR}/${version}/axonhub"
|
| |
|
| |
|
| | if [[ -f "$cache_path" && "$SKIP_DOWNLOAD" == "true" ]]; then
|
| | print_info "Using cached binary: $cache_path" >&2
|
| | echo "$cache_path"
|
| | return
|
| | fi
|
| |
|
| |
|
| | mkdir -p "${CACHE_DIR}/${version}"
|
| |
|
| |
|
| | if [[ ! -f "$cache_path" ]]; then
|
| | print_info "Downloading AxonHub ${version} for ${platform}..." >&2
|
| |
|
| | local download_url
|
| | download_url=$(get_asset_download_url "$version" "$platform")
|
| | local filename=$(basename "$download_url")
|
| | local temp_dir=$(mktemp -d)
|
| |
|
| | if ! curl -fSL -o "${temp_dir}/${filename}" "$download_url"; then
|
| | print_error "Failed to download AxonHub asset" >&2
|
| | rm -rf "$temp_dir"
|
| | exit 1
|
| | fi
|
| |
|
| | print_info "Extracting archive..." >&2
|
| |
|
| | if ! command -v unzip >/dev/null 2>&1; then
|
| | print_error "unzip command not found. Please install unzip." >&2
|
| | rm -rf "$temp_dir"
|
| | exit 1
|
| | fi
|
| |
|
| | if ! unzip -q "${temp_dir}/${filename}" -d "$temp_dir"; then
|
| | print_error "Failed to extract archive" >&2
|
| | rm -rf "$temp_dir"
|
| | exit 1
|
| | fi
|
| |
|
| |
|
| | local binary_path
|
| | binary_path=$(find "$temp_dir" -name "axonhub" -type f | head -1)
|
| |
|
| | if [[ -z "$binary_path" ]]; then
|
| | print_error "Could not find axonhub binary in archive" >&2
|
| | rm -rf "$temp_dir"
|
| | exit 1
|
| | fi
|
| |
|
| | cp "$binary_path" "$cache_path"
|
| | chmod +x "$cache_path"
|
| | rm -rf "$temp_dir"
|
| |
|
| | print_success "Binary cached: $cache_path" >&2
|
| | else
|
| | print_info "Using cached binary: $cache_path" >&2
|
| | fi
|
| |
|
| | echo "$cache_path"
|
| | }
|
| |
|
| | build_current_binary() {
|
| | local binary_path="${WORK_DIR}/axonhub-current"
|
| |
|
| | print_info "Building current branch binary..." >&2
|
| | cd "$PROJECT_ROOT"
|
| |
|
| | if ! go build -o "$binary_path" ./cmd/axonhub; then
|
| | print_error "Failed to build current branch binary" >&2
|
| | exit 1
|
| | fi
|
| |
|
| | chmod +x "$binary_path"
|
| | print_success "Current binary built: $binary_path" >&2
|
| |
|
| | echo "$binary_path"
|
| | }
|
| |
|
| | get_binary_version() {
|
| | local binary_path=$1
|
| | local version
|
| |
|
| | if version=$("$binary_path" version 2>/dev/null | head -n1 | tr -d '\r'); then
|
| | echo "$version"
|
| | else
|
| | echo "unknown"
|
| | fi
|
| | }
|
| |
|
| | initialize_database() {
|
| | local binary_path=$1
|
| | local version=$2
|
| |
|
| | print_info "Initializing database with version ${version}..." >&2
|
| | print_info "Binary path: $binary_path" >&2
|
| | print_info "Database type: $DB_TYPE" >&2
|
| |
|
| |
|
| | if [[ "$DB_TYPE" == "sqlite" ]]; then
|
| | print_info "Removing old SQLite database file: $DB_FILE" >&2
|
| | rm -f "$DB_FILE"
|
| | fi
|
| |
|
| | local db_dsn
|
| | db_dsn=$(get_db_dsn)
|
| | print_info "Database DSN: $db_dsn" >&2
|
| |
|
| | local db_dialect
|
| | db_dialect=$(get_db_dialect)
|
| |
|
| |
|
| | print_info "Starting server for initialization (PID will be captured)..." >&2
|
| | AXONHUB_SERVER_PORT=$E2E_PORT \
|
| | AXONHUB_DB_DIALECT="$db_dialect" \
|
| | AXONHUB_DB_DSN="$db_dsn" \
|
| | AXONHUB_LOG_OUTPUT="file" \
|
| | AXONHUB_LOG_FILE_PATH="$LOG_FILE" \
|
| | AXONHUB_LOG_LEVEL="info" \
|
| | "$binary_path" > /dev/null 2>&1 &
|
| |
|
| | local pid=$!
|
| | print_info "Server started with PID: $pid" >&2
|
| |
|
| | local init_status=0
|
| |
|
| | if ! wait_for_server_ready "$E2E_PORT" 60; then
|
| | print_error "Server failed to become ready" >&2
|
| | kill "$pid" 2>/dev/null || true
|
| | wait "$pid" 2>/dev/null || true
|
| | print_error "Failed to start server for database initialization" >&2
|
| | exit 1
|
| | fi
|
| |
|
| | if [[ "$SKIP_INIT_SYSTEM" == "true" ]]; then
|
| | print_warning "Skipping system initialization (--skip-init-system specified)" >&2
|
| | else
|
| | if ! initialize_system_via_api; then
|
| | init_status=1
|
| | fi
|
| | fi
|
| |
|
| | print_info "Stopping server (PID: $pid)..." >&2
|
| | kill "$pid" 2>/dev/null || true
|
| | wait "$pid" 2>/dev/null || true
|
| |
|
| | if [[ $init_status -ne 0 ]]; then
|
| | print_error "Database initialization failed" >&2
|
| | exit 1
|
| | fi
|
| |
|
| | if [[ "$SKIP_INIT_SYSTEM" == "true" ]]; then
|
| | print_success "Database prepared with existing state (initialization skipped)" >&2
|
| | else
|
| | print_success "Database initialized with version ${version}" >&2
|
| | fi
|
| | }
|
| |
|
| | run_migration() {
|
| | local binary_path=$1
|
| | local version=$2
|
| |
|
| | print_info "Running migration with version ${version}..." >&2
|
| | print_info "Binary path: $binary_path" >&2
|
| | print_info "Database type: $DB_TYPE" >&2
|
| |
|
| | local db_dsn
|
| | db_dsn=$(get_db_dsn)
|
| | print_info "Database DSN: $db_dsn" >&2
|
| |
|
| | local db_dialect
|
| | db_dialect=$(get_db_dialect)
|
| |
|
| |
|
| | print_info "Starting server for migration (PID will be captured)..." >&2
|
| | AXONHUB_SERVER_PORT=$E2E_PORT \
|
| | AXONHUB_DB_DIALECT="$db_dialect" \
|
| | AXONHUB_DB_DSN="$db_dsn" \
|
| | AXONHUB_LOG_OUTPUT="file" \
|
| | AXONHUB_LOG_FILE_PATH="$LOG_FILE" \
|
| | AXONHUB_LOG_LEVEL="debug" \
|
| | "$binary_path" > /dev/null 2>&1 &
|
| |
|
| | local pid=$!
|
| | print_info "Server started with PID: $pid" >&2
|
| |
|
| |
|
| | print_info "Waiting for migration to complete..." >&2
|
| | if wait_for_server_ready "$E2E_PORT" 60; then
|
| |
|
| | if ! kill -0 "$pid" 2>/dev/null; then
|
| |
|
| | local exit_status=0
|
| | wait "$pid" 2>/dev/null || exit_status=$?
|
| | print_error "Server process exited unexpectedly with status: $exit_status" >&2
|
| | print_error "Check log file for details: $LOG_FILE" >&2
|
| | exit 1
|
| | fi
|
| |
|
| |
|
| | print_info "Server is ready, verifying stability..." >&2
|
| | sleep 3
|
| |
|
| |
|
| | if ! kill -0 "$pid" 2>/dev/null; then
|
| | local exit_status=0
|
| | wait "$pid" 2>/dev/null || exit_status=$?
|
| | print_error "Server process crashed after startup with status: $exit_status" >&2
|
| | print_error "Check log file for details: $LOG_FILE" >&2
|
| | exit 1
|
| | fi
|
| |
|
| | print_success "Migration completed successfully" >&2
|
| | print_info "Stopping server (PID: $pid)..." >&2
|
| | kill "$pid" 2>/dev/null || true
|
| | wait "$pid" 2>/dev/null || true
|
| | return 0
|
| | fi
|
| |
|
| | print_error "Server failed to become ready, stopping..." >&2
|
| | kill "$pid" 2>/dev/null || true
|
| | wait "$pid" 2>/dev/null || true
|
| | print_error "Migration failed or timed out" >&2
|
| | exit 1
|
| | }
|
| |
|
| | wait_for_server_ready() {
|
| | local port=$1
|
| | local max_attempts=${2:-60}
|
| |
|
| | print_info "Waiting for server to be ready on port ${port}..." >&2
|
| |
|
| | for ((attempt = 1; attempt <= max_attempts; attempt++)); do
|
| | if curl -s "http://localhost:${port}/health" > /dev/null 2>&1 || \
|
| | curl -s "http://localhost:${port}/" > /dev/null 2>&1; then
|
| | return 0
|
| | fi
|
| |
|
| | sleep 1
|
| | done
|
| |
|
| | return 1
|
| | }
|
| |
|
| | initialize_system_via_api() {
|
| | local url="http://localhost:${E2E_PORT}/admin/system/initialize"
|
| | local response_file="${WORK_DIR}/initialize-response.json"
|
| | local payload
|
| |
|
| | payload=$(cat <<EOF
|
| | {
|
| | "ownerEmail": "${INIT_OWNER_EMAIL}",
|
| | "ownerPassword": "${INIT_OWNER_PASSWORD}",
|
| | "ownerFirstName": "${INIT_OWNER_FIRST_NAME}",
|
| | "ownerLastName": "${INIT_OWNER_LAST_NAME}",
|
| | "brandName": "${INIT_BRAND_NAME}"
|
| | }
|
| | EOF
|
| | )
|
| |
|
| | print_info "Initializing system via API (${url})..." >&2
|
| |
|
| | for attempt in {1..5}; do
|
| | local tmp_file="${response_file}.tmp"
|
| | local status
|
| |
|
| | status=$(curl -sS -o "$tmp_file" -w "%{http_code}" \
|
| | -H "Content-Type: application/json" \
|
| | -X POST \
|
| | -d "$payload" \
|
| | "$url" || echo "000")
|
| | status=$(echo "$status" | tr -d '\n\r')
|
| |
|
| | if [[ -f "$tmp_file" ]]; then
|
| | mv "$tmp_file" "$response_file"
|
| | fi
|
| |
|
| | case "$status" in
|
| | 200)
|
| | print_success "System initialized successfully via API" >&2
|
| | return 0
|
| | ;;
|
| | 400)
|
| | if [[ -f "$response_file" ]] && grep -qi "already initialized" "$response_file"; then
|
| | print_warning "System initialization API returned already initialized" >&2
|
| | return 0
|
| | fi
|
| | ;;
|
| | esac
|
| |
|
| | if [[ "$status" == "000" ]]; then
|
| | print_warning "Initialization attempt ${attempt} failed: unable to reach server" >&2
|
| | else
|
| | local body=""
|
| | if [[ -f "$response_file" ]]; then
|
| | body=$(cat "$response_file")
|
| | fi
|
| | print_warning "Initialization attempt ${attempt} failed (status: ${status}): ${body}" >&2
|
| | fi
|
| |
|
| | sleep 2
|
| | done
|
| |
|
| | print_error "System initialization via API failed after multiple attempts" >&2
|
| | if [[ -f "$response_file" ]]; then
|
| | print_error "Last response: $(cat "$response_file")" >&2
|
| | fi
|
| |
|
| | return 1
|
| | }
|
| |
|
| | generate_migration_plan() {
|
| | local from_tag=$1
|
| | local platform=$2
|
| |
|
| | print_info "Generating migration plan..." >&2
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | local old_binary
|
| | old_binary=$(download_binary "$from_tag" "$platform")
|
| |
|
| | local current_binary
|
| | current_binary=$(build_current_binary)
|
| |
|
| | local old_version
|
| | old_version=$(get_binary_version "$old_binary")
|
| |
|
| | local current_version
|
| | current_version=$(get_binary_version "$current_binary")
|
| |
|
| |
|
| | cat > "$PLAN_FILE" <<EOF
|
| | {
|
| | "from_tag": "$from_tag",
|
| | "from_version": "$old_version",
|
| | "to_version": "$current_version",
|
| | "platform": "$platform",
|
| | "steps": [
|
| | {
|
| | "step": 1,
|
| | "action": "initialize",
|
| | "version": "$from_tag",
|
| | "binary": "$old_binary",
|
| | "description": "Initialize database with version $old_version"
|
| | },
|
| | {
|
| | "step": 2,
|
| | "action": "migrate",
|
| | "version": "current",
|
| | "binary": "$current_binary",
|
| | "description": "Migrate database to version $current_version"
|
| | }
|
| | ]
|
| | }
|
| | EOF
|
| |
|
| | print_success "Migration plan generated: $PLAN_FILE" >&2
|
| |
|
| |
|
| | echo "" >&2
|
| | echo "Migration Plan:" >&2
|
| | echo " From: $from_tag ($old_version)" >&2
|
| | echo " To: current ($current_version)" >&2
|
| | echo " Steps:" >&2
|
| | echo " 1. Initialize database with $from_tag" >&2
|
| | echo " 2. Migrate to current branch" >&2
|
| | echo "" >&2
|
| | }
|
| |
|
| | execute_migration_plan() {
|
| | print_step "Executing migration plan" >&2
|
| |
|
| | if [[ ! -f "$PLAN_FILE" ]]; then
|
| | print_error "Migration plan not found: $PLAN_FILE" >&2
|
| | exit 1
|
| | fi
|
| |
|
| |
|
| | local from_tag from_version to_version
|
| |
|
| | if command -v jq >/dev/null 2>&1; then
|
| | from_tag=$(jq -r '.from_tag' "$PLAN_FILE")
|
| | from_version=$(jq -r '.from_version' "$PLAN_FILE")
|
| | to_version=$(jq -r '.to_version' "$PLAN_FILE")
|
| |
|
| |
|
| | local step1_binary
|
| | step1_binary=$(jq -r '.steps[0].binary' "$PLAN_FILE")
|
| | if [[ "$SKIP_INIT_SYSTEM" == "true" ]]; then
|
| | print_step "Step 2.1: Initialize database with $from_tag ($from_version) [skipped]" >&2
|
| | print_warning "System initialization skipped by option" >&2
|
| | else
|
| | print_step "Step 2.1: Initialize database with $from_tag ($from_version)" >&2
|
| | print_info "Using binary: $step1_binary" >&2
|
| | initialize_database "$step1_binary" "$from_version"
|
| | print_success "Step 2.1 completed" >&2
|
| | fi
|
| |
|
| |
|
| | local step2_binary
|
| | step2_binary=$(jq -r '.steps[1].binary' "$PLAN_FILE")
|
| | print_step "Step 2.2: Migrate to current ($to_version)" >&2
|
| | print_info "Using binary: $step2_binary" >&2
|
| | run_migration "$step2_binary" "$to_version"
|
| | print_success "Step 2.2 completed" >&2
|
| | else
|
| |
|
| | from_tag=$(grep '"from_tag"' "$PLAN_FILE" | sed -E 's/.*"from_tag"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
| | from_version=$(grep '"from_version"' "$PLAN_FILE" | sed -E 's/.*"from_version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
| | to_version=$(grep '"to_version"' "$PLAN_FILE" | sed -E 's/.*"to_version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
| |
|
| |
|
| | local step1_binary=$(grep -A 5 '"step": 1' "$PLAN_FILE" | grep '"binary"' | sed -E 's/.*"binary"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
| | local step2_binary=$(grep -A 5 '"step": 2' "$PLAN_FILE" | grep '"binary"' | sed -E 's/.*"binary"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
| |
|
| | if [[ "$SKIP_INIT_SYSTEM" == "true" ]]; then
|
| | print_step "Step 2.1: Initialize database with $from_tag ($from_version) [skipped]" >&2
|
| | print_warning "System initialization skipped by option" >&2
|
| | else
|
| | print_step "Step 2.1: Initialize database with $from_tag ($from_version)" >&2
|
| | print_info "Using binary: $step1_binary" >&2
|
| | initialize_database "$step1_binary" "$from_version"
|
| | print_success "Step 2.1 completed" >&2
|
| | fi
|
| |
|
| | print_step "Step 2.2: Migrate to current ($to_version)" >&2
|
| | print_info "Using binary: $step2_binary" >&2
|
| | run_migration "$step2_binary" "$to_version"
|
| | print_success "Step 2.2 completed" >&2
|
| | fi
|
| |
|
| | print_success "Migration plan executed successfully" >&2
|
| | }
|
| |
|
| | run_e2e_tests() {
|
| | print_step "Running e2e tests to verify migration" >&2
|
| |
|
| | local db_dsn
|
| | db_dsn=$(get_db_dsn)
|
| | local db_dialect
|
| | db_dialect=$(get_db_dialect)
|
| |
|
| | if [[ "$DB_TYPE" == "sqlite" ]]; then
|
| | local e2e_db="${SCRIPT_DIR}/../e2e/axonhub-e2e.db"
|
| | cp "$DB_FILE" "$e2e_db"
|
| | print_info "Database copied to e2e location: $e2e_db" >&2
|
| | cd "$PROJECT_ROOT"
|
| | if env \
|
| | AXONHUB_E2E_DB_TYPE="$DB_TYPE" \
|
| | AXONHUB_E2E_DB_DIALECT="$db_dialect" \
|
| | ./scripts/e2e/e2e-test.sh; then
|
| | print_success "E2E tests passed!" >&2
|
| | return 0
|
| | else
|
| | print_error "E2E tests failed" >&2
|
| | return 1
|
| | fi
|
| | fi
|
| |
|
| | print_info "Reusing migrated $DB_TYPE database for e2e tests" >&2
|
| |
|
| | cd "$PROJECT_ROOT"
|
| | if env \
|
| | AXONHUB_E2E_DB_TYPE="$DB_TYPE" \
|
| | AXONHUB_E2E_DB_DIALECT="$db_dialect" \
|
| | AXONHUB_E2E_DB_DSN="$db_dsn" \
|
| | AXONHUB_E2E_USE_EXISTING_DB="true" \
|
| | ./scripts/e2e/e2e-test.sh; then
|
| | print_success "E2E tests passed!" >&2
|
| | return 0
|
| | else
|
| | print_error "E2E tests failed" >&2
|
| | return 1
|
| | fi
|
| | }
|
| |
|
| | cleanup() {
|
| |
|
| | cleanup_database
|
| |
|
| |
|
| | if [[ "$KEEP_ARTIFACTS" != "true" ]]; then
|
| | print_info "Cleaning up work directory..." >&2
|
| | rm -rf "$WORK_DIR"
|
| | else
|
| | print_info "Keeping artifacts in: $WORK_DIR" >&2
|
| | fi
|
| | }
|
| |
|
| | main() {
|
| | print_info "AxonHub Migration Test Script" >&2
|
| | echo "" >&2
|
| |
|
| |
|
| | local from_tag=""
|
| | SKIP_DOWNLOAD="false"
|
| | SKIP_E2E="false"
|
| | SKIP_INIT_SYSTEM="false"
|
| | KEEP_ARTIFACTS="false"
|
| | KEEP_DB="false"
|
| |
|
| | while [[ $# -gt 0 ]]; do
|
| | case "$1" in
|
| | --db-type)
|
| | if [[ -z "$2" || "$2" == -* ]]; then
|
| | print_error "--db-type requires an argument" >&2
|
| | usage
|
| | exit 1
|
| | fi
|
| | DB_TYPE="$2"
|
| | shift 2
|
| | ;;
|
| | --skip-download)
|
| | SKIP_DOWNLOAD="true"
|
| | shift
|
| | ;;
|
| | --skip-e2e)
|
| | SKIP_E2E="true"
|
| | shift
|
| | ;;
|
| | --skip-init-system)
|
| | SKIP_INIT_SYSTEM="true"
|
| | shift
|
| | ;;
|
| | --keep-artifacts)
|
| | KEEP_ARTIFACTS="true"
|
| | shift
|
| | ;;
|
| | --keep-db)
|
| | KEEP_DB="true"
|
| | shift
|
| | ;;
|
| | -h|--help)
|
| | usage
|
| | exit 0
|
| | ;;
|
| | -*)
|
| | print_error "Unknown option: $1" >&2
|
| | usage
|
| | exit 1
|
| | ;;
|
| | *)
|
| | if [[ -z "$from_tag" ]]; then
|
| | from_tag="$1"
|
| | shift
|
| | else
|
| | print_error "Too many arguments" >&2
|
| | usage
|
| | exit 1
|
| | fi
|
| | ;;
|
| | esac
|
| | done
|
| |
|
| | if [[ -z "$from_tag" ]]; then
|
| | print_error "Missing required argument: from-tag" >&2
|
| | usage
|
| | exit 1
|
| | fi
|
| |
|
| |
|
| | case "$DB_TYPE" in
|
| | sqlite|mysql|postgres)
|
| | ;;
|
| | *)
|
| | print_error "Invalid database type: $DB_TYPE (must be sqlite, mysql, or postgres)" >&2
|
| | exit 1
|
| | ;;
|
| | esac
|
| |
|
| | print_info "Testing migration from $from_tag to current branch" >&2
|
| | print_info "Database type: $DB_TYPE" >&2
|
| | echo "" >&2
|
| |
|
| |
|
| | local platform
|
| | platform=$(detect_architecture)
|
| | print_info "Detected platform: $platform" >&2
|
| |
|
| |
|
| | mkdir -p "$CACHE_DIR" "$WORK_DIR"
|
| |
|
| |
|
| | case "$DB_TYPE" in
|
| | mysql)
|
| | setup_mysql
|
| | ;;
|
| | postgres)
|
| | setup_postgres
|
| | ;;
|
| | sqlite)
|
| | print_info "Using SQLite database: $DB_FILE" >&2
|
| | ;;
|
| | esac
|
| |
|
| |
|
| | print_step "Step 1: Generate migration plan" >&2
|
| | generate_migration_plan "$from_tag" "$platform"
|
| |
|
| |
|
| | print_step "Step 2: Execute migration plan" >&2
|
| | execute_migration_plan
|
| |
|
| |
|
| | if [[ "$SKIP_E2E" != "true" ]]; then
|
| | print_step "Step 3: Run e2e tests" >&2
|
| | if ! run_e2e_tests; then
|
| | cleanup
|
| | exit 1
|
| | fi
|
| | else
|
| | print_warning "Skipping e2e tests (--skip-e2e specified)" >&2
|
| | fi
|
| |
|
| |
|
| | cleanup
|
| |
|
| | echo "" >&2
|
| | print_success "Migration test completed successfully!" >&2
|
| | echo "" >&2
|
| | print_info "Summary:" >&2
|
| | echo " From: $from_tag" >&2
|
| | echo " To: current branch" >&2
|
| | echo " Database Type: $DB_TYPE" >&2
|
| | case "$DB_TYPE" in
|
| | sqlite)
|
| | echo " Database File: $DB_FILE" >&2
|
| | ;;
|
| | mysql)
|
| | echo " MySQL Container: $MYSQL_CONTAINER" >&2
|
| | echo " MySQL Port: $MYSQL_PORT" >&2
|
| | echo " MySQL Database: $MYSQL_DATABASE" >&2
|
| | if [[ "$KEEP_DB" == "true" ]]; then
|
| | echo " MySQL DSN: ${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(localhost:${MYSQL_PORT})/${MYSQL_DATABASE}" >&2
|
| | fi
|
| | ;;
|
| | postgres)
|
| | echo " PostgreSQL Container: $POSTGRES_CONTAINER" >&2
|
| | echo " PostgreSQL Port: $POSTGRES_PORT" >&2
|
| | echo " PostgreSQL Database: $POSTGRES_DATABASE" >&2
|
| | if [[ "$KEEP_DB" == "true" ]]; then
|
| | echo " PostgreSQL DSN: host=localhost port=${POSTGRES_PORT} user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DATABASE}" >&2
|
| | fi
|
| | ;;
|
| | esac
|
| | echo " Log: $LOG_FILE" >&2
|
| | echo " Cache: $CACHE_DIR" >&2
|
| | echo "" >&2
|
| | }
|
| |
|
| |
|
| | main "$@"
|
| |
|