Files
ngit-grasp/docs/how-to/migration-scripts/run-migration-analysis.sh
T
DanConwayDev 2b21b807bd Prevent Phase 4 from using wrong service (ngit-relay vs ngit-grasp)
Add validation to ensure Phase 4 scripts use ngit-grasp service
(with structured logging) instead of ngit-relay service.

Changes:
- Add validate-service.sh helper for reusable service validation
- Add validation to run-migration-analysis.sh before Phase 4
- Add validation to 30-extract-parse-failures.sh
- Add validation to 31-extract-purgatory-expiry.sh
- Update migration guide with clear warnings about service selection
- Expand troubleshooting for 'Phase 4 finds no logs' issue
- Emphasize lesson learned in relay.ngit.dev notes

This prevents the issue where Phase 4 was run against ngit-relay.service
and found no parse failures because structured logging only exists in
ngit-grasp services.
2026-01-27 20:38:04 +00:00

745 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# run-migration-analysis.sh - Orchestrate the complete GRASP relay to ngit-grasp migration analysis
#
# This script runs all 5 phases of the migration analysis pipeline in sequence,
# with proper error handling, progress reporting, and timing information.
#
# QUICK START:
# # Basic usage (local analysis only - Phases 1, 3, 5)
# ./run-migration-analysis.sh --prod-relay wss://relay.ngit.dev --archive-relay wss://archive.relay.ngit.dev
#
# # Full analysis including git sync check (requires VPS access)
# ./run-migration-analysis.sh \
# --prod-relay wss://relay.ngit.dev \
# --archive-relay wss://archive.relay.ngit.dev \
# --prod-git /var/lib/grasp-relay/git \
# --archive-git /var/lib/ngit-grasp/git
#
# USAGE:
# ./run-migration-analysis.sh [options]
#
# REQUIRED OPTIONS:
# --prod-relay <url> Production relay WebSocket URL (e.g., wss://relay.ngit.dev)
# --archive-relay <url> Archive relay WebSocket URL (e.g., wss://archive.relay.ngit.dev)
#
# OPTIONAL OPTIONS:
# --prod-git <path> Git base directory for prod (enables Phase 2)
# --archive-git <path> Git base directory for archive (enables Phase 2)
# --service <name> Systemd service name for log extraction (enables Phase 4)
# --output <dir> Output directory (default: work/migration-analysis-YYYYMMDD-HHMM)
#
# PHASE CONTROL:
# --skip-phase-1 Skip event fetching (use existing data)
# --skip-phase-2 Skip git sync check (use existing data)
# --skip-phase-3 Skip categorization (use existing data)
# --skip-phase-4 Skip log extraction (use existing data)
# --skip-phase-5 Skip final classification
# --only-phase-N Run only phase N (1-5)
# --from-phase-N Start from phase N (skip earlier phases)
#
# OTHER OPTIONS:
# --dry-run Show what would be executed without running
# --continue-on-error Continue to next phase even if current phase fails
# --help Show this help message
#
# PHASES:
# Phase 1: Fetch events from both relays (~30s each, local)
# Phase 2: Check git sync status (~20 min each, requires VPS)
# Phase 3: Categorize and compare results (fast, local)
# Phase 4: Extract logs from systemd (requires VPS)
# Phase 5: Final classification (fast, local)
#
# EXAMPLES:
# # Dry run to see what would happen
# ./run-migration-analysis.sh --prod-relay wss://relay.ngit.dev --archive-relay wss://archive.relay.ngit.dev --dry-run
#
# # Run only Phase 1 (fetch events)
# ./run-migration-analysis.sh --prod-relay wss://relay.ngit.dev --archive-relay wss://archive.relay.ngit.dev --only-phase-1
#
# # Resume from Phase 3 using existing Phase 1-2 data
# ./run-migration-analysis.sh --prod-relay wss://relay.ngit.dev --archive-relay wss://archive.relay.ngit.dev --from-phase-3 --output work/migration-analysis-20260122-1430
#
# # Full analysis on VPS with all features
# ./run-migration-analysis.sh \
# --prod-relay wss://relay.ngit.dev \
# --archive-relay wss://archive.relay.ngit.dev \
# --prod-git /var/lib/grasp-relay/git \
# --archive-git /var/lib/ngit-grasp/git \
# --service ngit-grasp.service
#
# SEE ALSO:
# docs/how-to/migrate-to-ngit-grasp.md - Full migration guide
#
set -euo pipefail
# Get script directory for finding other scripts
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Colors for output (disabled if not a terminal)
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
CYAN=''
BOLD=''
NC=''
fi
# Logging functions
log_header() {
echo ""
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD}${CYAN} $*${NC}"
echo -e "${BOLD}${CYAN}════════════════════════════════════════════════════════════════${NC}"
echo ""
}
log_phase() {
echo ""
echo -e "${BOLD}${BLUE}┌──────────────────────────────────────────────────────────────┐${NC}"
echo -e "${BOLD}${BLUE}│ $*${NC}"
echo -e "${BOLD}${BLUE}└──────────────────────────────────────────────────────────────┘${NC}"
}
log_info() {
echo -e "${BLUE}[INFO]${NC} $*" >&2
}
log_success() {
echo -e "${GREEN}[OK]${NC} $*" >&2
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
log_step() {
echo -e "${CYAN} →${NC} $*" >&2
}
# Default values
PROD_RELAY=""
ARCHIVE_RELAY=""
PROD_GIT=""
ARCHIVE_GIT=""
SERVICE_NAME=""
OUTPUT_DIR=""
DRY_RUN=false
CONTINUE_ON_ERROR=false
# Phase control
SKIP_PHASE_1=false
SKIP_PHASE_2=false
SKIP_PHASE_3=false
SKIP_PHASE_4=false
SKIP_PHASE_5=false
ONLY_PHASE=""
FROM_PHASE=""
# Timing
declare -A PHASE_TIMES
usage() {
head -73 "$0" | tail -n +3 | sed 's/^# //' | sed 's/^#//'
exit 0
}
# Parse command line arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--prod-relay)
PROD_RELAY="$2"
shift 2
;;
--archive-relay)
ARCHIVE_RELAY="$2"
shift 2
;;
--prod-git)
PROD_GIT="$2"
shift 2
;;
--archive-git)
ARCHIVE_GIT="$2"
shift 2
;;
--service)
SERVICE_NAME="$2"
shift 2
;;
--output)
OUTPUT_DIR="$2"
shift 2
;;
--skip-phase-1)
SKIP_PHASE_1=true
shift
;;
--skip-phase-2)
SKIP_PHASE_2=true
shift
;;
--skip-phase-3)
SKIP_PHASE_3=true
shift
;;
--skip-phase-4)
SKIP_PHASE_4=true
shift
;;
--skip-phase-5)
SKIP_PHASE_5=true
shift
;;
--only-phase-1|--only-phase-2|--only-phase-3|--only-phase-4|--only-phase-5)
ONLY_PHASE="${1#--only-phase-}"
shift
;;
--from-phase-1|--from-phase-2|--from-phase-3|--from-phase-4|--from-phase-5)
FROM_PHASE="${1#--from-phase-}"
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--continue-on-error)
CONTINUE_ON_ERROR=true
shift
;;
--help|-h)
usage
;;
*)
log_error "Unknown option: $1"
echo "Use --help for usage information."
exit 1
;;
esac
done
}
# Validate required arguments
validate_args() {
local errors=0
if [[ -z "$PROD_RELAY" ]]; then
log_error "Missing required option: --prod-relay"
errors=1
fi
if [[ -z "$ARCHIVE_RELAY" ]]; then
log_error "Missing required option: --archive-relay"
errors=1
fi
# Validate relay URLs
if [[ -n "$PROD_RELAY" && ! "$PROD_RELAY" =~ ^wss?:// ]]; then
log_error "Invalid prod relay URL: $PROD_RELAY (must start with ws:// or wss://)"
errors=1
fi
if [[ -n "$ARCHIVE_RELAY" && ! "$ARCHIVE_RELAY" =~ ^wss?:// ]]; then
log_error "Invalid archive relay URL: $ARCHIVE_RELAY (must start with ws:// or wss://)"
errors=1
fi
# Validate git paths if provided
if [[ -n "$PROD_GIT" && ! -d "$PROD_GIT" ]]; then
log_warn "Prod git directory not found: $PROD_GIT"
log_warn "Phase 2 will fail unless running on VPS with access to this path."
fi
if [[ -n "$ARCHIVE_GIT" && ! -d "$ARCHIVE_GIT" ]]; then
log_warn "Archive git directory not found: $ARCHIVE_GIT"
log_warn "Phase 2 will fail unless running on VPS with access to this path."
fi
if [[ $errors -eq 1 ]]; then
echo ""
echo "Use --help for usage information."
exit 1
fi
}
# Check prerequisites
check_prerequisites() {
local missing=0
log_info "Checking prerequisites..."
# Required tools
for tool in git nak jq awk sort; do
if command -v "$tool" &> /dev/null; then
log_step "$tool: found"
else
log_error "$tool: NOT FOUND"
missing=1
fi
done
# Optional tools
if command -v journalctl &> /dev/null; then
log_step "journalctl: found (Phase 4 available)"
else
log_step "journalctl: not found (Phase 4 will be skipped)"
SKIP_PHASE_4=true
fi
if [[ $missing -eq 1 ]]; then
log_error "Missing required tools. Install them and try again."
exit 1
fi
# Check scripts exist
for script in 01-fetch-events.sh 10-check-git-sync.sh 20-categorize.sh 21-compare-relays.sh 30-extract-parse-failures.sh 31-extract-purgatory-expiry.sh 40-classify-actions.sh; do
if [[ ! -x "$SCRIPT_DIR/$script" ]]; then
log_error "Script not found or not executable: $SCRIPT_DIR/$script"
missing=1
fi
done
if [[ $missing -eq 1 ]]; then
exit 1
fi
log_success "All prerequisites satisfied"
}
# Determine which phases to run
determine_phases() {
# Handle --only-phase-N
if [[ -n "$ONLY_PHASE" ]]; then
for i in 1 2 3 4 5; do
if [[ "$i" != "$ONLY_PHASE" ]]; then
eval "SKIP_PHASE_$i=true"
fi
done
fi
# Handle --from-phase-N
if [[ -n "$FROM_PHASE" ]]; then
for i in 1 2 3 4 5; do
if [[ "$i" -lt "$FROM_PHASE" ]]; then
eval "SKIP_PHASE_$i=true"
fi
done
fi
# Auto-skip Phase 2 if git paths not provided
if [[ -z "$PROD_GIT" && -z "$ARCHIVE_GIT" ]]; then
if [[ "$SKIP_PHASE_2" != "true" ]]; then
log_warn "No git paths provided. Phase 2 (git sync check) will be skipped."
log_warn "Use --prod-git and --archive-git to enable Phase 2."
SKIP_PHASE_2=true
fi
fi
# Auto-skip Phase 4 if service not provided
if [[ -z "$SERVICE_NAME" ]]; then
if [[ "$SKIP_PHASE_4" != "true" ]]; then
log_warn "No service name provided. Phase 4 (log extraction) will be skipped."
log_warn "Use --service to enable Phase 4."
SKIP_PHASE_4=true
fi
fi
}
# Setup output directory
setup_output_dir() {
if [[ -z "$OUTPUT_DIR" ]]; then
OUTPUT_DIR="work/migration-analysis-$(date +%Y%m%d-%H%M)"
fi
log_info "Output directory: $OUTPUT_DIR"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY RUN] Would create directory structure"
return
fi
mkdir -p "$OUTPUT_DIR"/{prod/raw,archive/raw,comparison,logs,results}
# Save configuration
cat > "$OUTPUT_DIR/config.txt" << EOF
# Migration Analysis Configuration
# Generated: $(date -Iseconds)
PROD_RELAY=$PROD_RELAY
ARCHIVE_RELAY=$ARCHIVE_RELAY
PROD_GIT=$PROD_GIT
ARCHIVE_GIT=$ARCHIVE_GIT
SERVICE_NAME=$SERVICE_NAME
OUTPUT_DIR=$OUTPUT_DIR
EOF
log_success "Created output directory structure"
}
# Run a phase with timing and error handling
run_phase() {
local phase_num="$1"
local phase_name="$2"
shift 2
local cmd=("$@")
local skip_var="SKIP_PHASE_$phase_num"
if [[ "${!skip_var}" == "true" ]]; then
log_phase "Phase $phase_num: $phase_name [SKIPPED]"
return 0
fi
log_phase "Phase $phase_num: $phase_name"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY RUN] Would execute:"
for c in "${cmd[@]}"; do
echo " $c"
done
return 0
fi
local start_time
start_time=$(date +%s)
local exit_code=0
# Execute the command(s)
for c in "${cmd[@]}"; do
log_step "Running: $c"
if ! eval "$c"; then
exit_code=1
if [[ "$CONTINUE_ON_ERROR" == "true" ]]; then
log_warn "Command failed, continuing due to --continue-on-error"
else
log_error "Command failed"
break
fi
fi
done
local end_time
end_time=$(date +%s)
local duration=$((end_time - start_time))
PHASE_TIMES[$phase_num]=$duration
if [[ $exit_code -eq 0 ]]; then
log_success "Phase $phase_num completed in ${duration}s"
else
log_error "Phase $phase_num failed after ${duration}s"
if [[ "$CONTINUE_ON_ERROR" != "true" ]]; then
return 1
fi
fi
return $exit_code
}
# Phase 1: Fetch events
run_phase_1() {
local cmds=()
# Fetch from prod relay
cmds+=("'$SCRIPT_DIR/01-fetch-events.sh' '$PROD_RELAY' '$OUTPUT_DIR/prod'")
# Fetch from archive relay
cmds+=("'$SCRIPT_DIR/01-fetch-events.sh' '$ARCHIVE_RELAY' '$OUTPUT_DIR/archive'")
run_phase 1 "Fetch Events (~30s each)" "${cmds[@]}"
}
# Phase 2: Git sync check
run_phase_2() {
local cmds=()
if [[ -n "$PROD_GIT" ]]; then
cmds+=("'$SCRIPT_DIR/10-check-git-sync.sh' '$OUTPUT_DIR/prod/raw/state-events.json' '$PROD_GIT' '$OUTPUT_DIR/prod' --categorize")
else
log_warn "Skipping prod git sync check (no --prod-git provided)"
fi
if [[ -n "$ARCHIVE_GIT" ]]; then
cmds+=("'$SCRIPT_DIR/10-check-git-sync.sh' '$OUTPUT_DIR/archive/raw/state-events.json' '$ARCHIVE_GIT' '$OUTPUT_DIR/archive' --categorize")
else
log_warn "Skipping archive git sync check (no --archive-git provided)"
fi
if [[ ${#cmds[@]} -eq 0 ]]; then
log_warn "No git paths provided, skipping Phase 2"
return 0
fi
run_phase 2 "Git Sync Check (~20 min each)" "${cmds[@]}"
}
# Phase 3: Categorize and compare
run_phase_3() {
local cmds=()
# Check if we have git-sync-status.tsv files (from Phase 2)
# If not, we can't run categorization
local has_prod_sync=false
local has_archive_sync=false
if [[ -f "$OUTPUT_DIR/prod/git-sync-status.tsv" ]]; then
has_prod_sync=true
fi
if [[ -f "$OUTPUT_DIR/archive/git-sync-status.tsv" ]]; then
has_archive_sync=true
fi
# Run categorization if we have sync data but no category files
if [[ "$has_prod_sync" == "true" && ! -f "$OUTPUT_DIR/prod/category1-complete-match.txt" ]]; then
cmds+=("'$SCRIPT_DIR/20-categorize.sh' '$OUTPUT_DIR/prod/git-sync-status.tsv' '$OUTPUT_DIR/prod'")
fi
if [[ "$has_archive_sync" == "true" && ! -f "$OUTPUT_DIR/archive/category1-complete-match.txt" ]]; then
cmds+=("'$SCRIPT_DIR/20-categorize.sh' '$OUTPUT_DIR/archive/git-sync-status.tsv' '$OUTPUT_DIR/archive'")
fi
# Run comparison if we have category files
if [[ -f "$OUTPUT_DIR/prod/category1-complete-match.txt" && -f "$OUTPUT_DIR/archive/category1-complete-match.txt" ]]; then
cmds+=("'$SCRIPT_DIR/21-compare-relays.sh' '$OUTPUT_DIR/prod' '$OUTPUT_DIR/archive' '$OUTPUT_DIR/comparison'")
else
log_warn "Missing category files for comparison."
log_warn "Phase 2 must complete successfully before Phase 3 can compare relays."
# Create placeholder comparison files if they don't exist
if [[ "$DRY_RUN" != "true" ]]; then
mkdir -p "$OUTPUT_DIR/comparison"
for f in complete-in-both.txt complete-prod-missing-archive.txt complete-prod-incomplete-archive.txt incomplete-in-both.txt in-archive-not-prod.txt; do
if [[ ! -f "$OUTPUT_DIR/comparison/$f" ]]; then
echo "# Placeholder - Phase 2 data not available" > "$OUTPUT_DIR/comparison/$f"
fi
done
echo "# Comparison not available - Phase 2 data missing" > "$OUTPUT_DIR/comparison/summary.txt"
fi
fi
if [[ ${#cmds[@]} -eq 0 ]]; then
log_warn "No categorization or comparison needed (already done or missing input)"
return 0
fi
run_phase 3 "Categorize & Compare (fast)" "${cmds[@]}"
}
# Phase 4: Extract logs
run_phase_4() {
if [[ -z "$SERVICE_NAME" ]]; then
log_warn "No service name provided, skipping Phase 4"
return 0
fi
# Validate service name before running Phase 4
# Structured logging only exists in ngit-grasp, not ngit-relay
if [[ "$SERVICE_NAME" == *"ngit-relay"* ]]; then
log_error "SERVICE_NAME appears to be ngit-relay: $SERVICE_NAME"
log_error ""
log_error "Phase 4 requires an ngit-grasp service with structured logging."
log_error "Structured logging ([PARSE_FAIL], [PURGATORY_EXPIRED]) only exists"
log_error "in ngit-grasp services, NOT in ngit-relay services."
log_error ""
log_error "Please update --service to use the ngit-grasp archive service."
log_error ""
log_error "To find the correct service name:"
log_error " systemctl list-units 'ngit-grasp*' --all"
log_error ""
log_error "Common ngit-grasp service names:"
log_error " - ngit-grasp.service"
log_error " - ngit-grasp-relay-ngit-dev.service (NixOS multi-instance)"
log_error " - ngit-grasp-archive.service"
return 1
fi
# Warn if service name doesn't look like ngit-grasp
if [[ "$SERVICE_NAME" != *"ngit-grasp"* && "$SERVICE_NAME" != *"grasp"* ]]; then
log_warn "SERVICE_NAME doesn't contain 'ngit-grasp': $SERVICE_NAME"
log_warn "Structured logging only exists in ngit-grasp services."
log_warn "If this is not an ngit-grasp service, Phase 4 will find no logs."
fi
local cmds=()
cmds+=("'$SCRIPT_DIR/30-extract-parse-failures.sh' '$SERVICE_NAME' '$OUTPUT_DIR/logs'")
cmds+=("'$SCRIPT_DIR/31-extract-purgatory-expiry.sh' '$SERVICE_NAME' '$OUTPUT_DIR/logs'")
run_phase 4 "Extract Logs (VPS required)" "${cmds[@]}"
}
# Phase 5: Final classification
run_phase_5() {
# Check if we have the minimum required files
local can_run=true
if [[ ! -d "$OUTPUT_DIR/prod" ]]; then
log_warn "Missing prod directory"
can_run=false
fi
if [[ ! -d "$OUTPUT_DIR/archive" ]]; then
log_warn "Missing archive directory"
can_run=false
fi
if [[ ! -d "$OUTPUT_DIR/comparison" ]]; then
log_warn "Missing comparison directory"
can_run=false
fi
# Create logs directory with empty files if missing
if [[ "$DRY_RUN" != "true" ]]; then
mkdir -p "$OUTPUT_DIR/logs"
for f in parse-failures.txt purgatory-expired.txt; do
if [[ ! -f "$OUTPUT_DIR/logs/$f" ]]; then
echo "# No data - Phase 4 not run" > "$OUTPUT_DIR/logs/$f"
fi
done
fi
if [[ "$can_run" == "false" ]]; then
log_error "Cannot run Phase 5 - missing required input directories"
return 1
fi
run_phase 5 "Final Classification (fast)" "'$SCRIPT_DIR/40-classify-actions.sh' '$OUTPUT_DIR'"
}
# Display summary
display_summary() {
log_header "Migration Analysis Complete"
echo "Output Directory: $OUTPUT_DIR"
echo ""
# Phase timing summary
echo "Phase Timing:"
local total_time=0
for phase in 1 2 3 4 5; do
local skip_var="SKIP_PHASE_$phase"
if [[ "${!skip_var}" == "true" ]]; then
echo " Phase $phase: SKIPPED"
elif [[ -n "${PHASE_TIMES[$phase]:-}" ]]; then
local t="${PHASE_TIMES[$phase]}"
echo " Phase $phase: ${t}s"
total_time=$((total_time + t))
else
echo " Phase $phase: N/A"
fi
done
echo " ─────────────"
echo " Total: ${total_time}s"
echo ""
# Results summary
if [[ -f "$OUTPUT_DIR/results/summary.txt" ]]; then
echo "Results Summary:"
echo ""
# Extract key metrics from summary
if grep -q "No Action Required" "$OUTPUT_DIR/results/summary.txt"; then
grep -A1 "No Action Required" "$OUTPUT_DIR/results/summary.txt" | head -2
fi
if grep -q "Action Required" "$OUTPUT_DIR/results/summary.txt"; then
grep -A1 "Action Required" "$OUTPUT_DIR/results/summary.txt" | head -2
fi
if grep -q "Manual Investigation" "$OUTPUT_DIR/results/summary.txt"; then
grep -A1 "Manual Investigation" "$OUTPUT_DIR/results/summary.txt" | head -2
fi
echo ""
fi
# Output files
echo "Output Files:"
echo " $OUTPUT_DIR/results/no-action-required.txt"
echo " $OUTPUT_DIR/results/action-required.txt"
echo " $OUTPUT_DIR/results/manual-investigation.txt"
echo " $OUTPUT_DIR/results/summary.txt"
echo ""
# Next steps
echo "Next Steps:"
echo " 1. Review results/summary.txt for overview"
echo " 2. Address items in results/action-required.txt"
echo " 3. Investigate items in results/manual-investigation.txt"
echo " 4. Plan migration window when action items are resolved"
echo ""
}
# Main
main() {
parse_args "$@"
log_header "GRASP Relay to ngit-grasp Migration Analysis"
validate_args
check_prerequisites
determine_phases
setup_output_dir
# Show configuration
log_info "Configuration:"
log_step "Prod relay: $PROD_RELAY"
log_step "Archive relay: $ARCHIVE_RELAY"
[[ -n "$PROD_GIT" ]] && log_step "Prod git: $PROD_GIT"
[[ -n "$ARCHIVE_GIT" ]] && log_step "Archive git: $ARCHIVE_GIT"
[[ -n "$SERVICE_NAME" ]] && log_step "Service: $SERVICE_NAME"
log_step "Output: $OUTPUT_DIR"
echo ""
# Show phase plan
log_info "Phase Plan:"
for phase in 1 2 3 4 5; do
local skip_var="SKIP_PHASE_$phase"
if [[ "${!skip_var}" == "true" ]]; then
log_step "Phase $phase: SKIP"
else
log_step "Phase $phase: RUN"
fi
done
echo ""
if [[ "$DRY_RUN" == "true" ]]; then
log_warn "DRY RUN MODE - No changes will be made"
echo ""
fi
# Run phases
local overall_exit=0
run_phase_1 || overall_exit=1
run_phase_2 || overall_exit=1
run_phase_3 || overall_exit=1
run_phase_4 || overall_exit=1
run_phase_5 || overall_exit=1
# Display summary
if [[ "$DRY_RUN" != "true" ]]; then
display_summary
fi
if [[ $overall_exit -ne 0 ]]; then
log_warn "Some phases failed. Review output for details."
fi
exit $overall_exit
}
main "$@"