Files
roa2web-service-auto/deployment/linux/deploy.sh
Claude Agent 6718c956f7 feat: [US-004] Add SSH tunnel auto-start for Windows services
- Add ssh-tunnel.ps1: Windows SSH tunnel manager (equivalent to ssh-tunnel.sh)
  - Supports password auth via plink.exe (PuTTY)
  - Supports ssh_hostkey for non-interactive batch mode
  - Commands: start, stop, restart, status

- Add start-backend-service.ps1: NSSM service wrapper
  - Starts SSH tunnels before uvicorn
  - Waits for tunnel ports to be accessible (30s timeout)
  - Configured by Install-ROA2WEB.ps1

- Add start.ps1: Windows equivalent of start.sh
  - Orchestrates SSH tunnel + backend + frontend startup

- Add backend/shared/ssh_tunnel_manager.py: Python monitoring
  - Background asyncio task monitors tunnel health every 30s
  - Auto-restarts tunnels after 2 consecutive failures
  - Exposes status to /health endpoint

- Update ROA2WEB-Console.ps1:
  - Add Deploy-Scripts function
  - Update Update-ServiceToUseVenv to use wrapper script

- Fix PowerShell reserved variable ($PID -> $tunnelPid)
- Fix script path detection (scripts/ vs deployment/windows/scripts/)
- Update README.md with ssh_hostkey documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 19:04:26 +00:00

384 lines
11 KiB
Bash
Executable File

#!/bin/bash
#
# ROA2WEB Linux Deployment Script
# Builds and deploys to Windows IIS server via SSH/SCP
#
# Usage:
# ./deploy.sh # Full deploy (frontend + backend)
# ./deploy.sh frontend # Frontend only
# ./deploy.sh backend # Backend only
# ./deploy.sh test # Test SSH connection only
#
set -e
# =============================================================================
# CONFIGURATION
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# SSH Configuration (matches deploy-config.json)
SSH_HOST="roa2web-prod" # Uses ~/.ssh/config
REMOTE_PATH="C:/Temp"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
log_step() {
echo -e "\n${CYAN}[*] $1${NC}" >&2
}
log_success() {
echo -e "${GREEN} [OK] $1${NC}" >&2
}
log_error() {
echo -e "${RED} [ERROR] $1${NC}" >&2
}
log_warning() {
echo -e "${YELLOW} [WARN] $1${NC}" >&2
}
log_info() {
echo -e " [INFO] $1" >&2
}
# =============================================================================
# SSH CONNECTION TEST
# =============================================================================
test_ssh_connection() {
log_step "Testing SSH connection to $SSH_HOST..."
if ssh -o ConnectTimeout=10 "$SSH_HOST" "echo 'Connection successful'" 2>/dev/null; then
log_success "SSH connection working"
return 0
else
log_error "SSH connection failed"
echo ""
echo "Please ensure:"
echo " 1. SSH key is configured in ~/.ssh/config"
echo " 2. Public key is added to server's authorized_keys"
echo " 3. Server is reachable on port 22122"
echo ""
echo "SSH config should contain:"
echo " Host roa2web-prod"
echo " HostName 10.0.20.36"
echo " Port 22122"
echo " User Administrator"
echo " IdentityFile ~/.ssh/roa2web_deploy"
return 1
fi
}
# =============================================================================
# BUILD FRONTEND
# =============================================================================
build_frontend() {
log_step "Building frontend..."
cd "$PROJECT_ROOT"
# Check Node.js
if ! command -v node &> /dev/null; then
log_error "Node.js not found. Please install Node.js 16+"
exit 1
fi
NODE_VERSION=$(node --version)
log_info "Node.js: $NODE_VERSION"
# Check if node_modules exists
if [ ! -d "node_modules" ]; then
log_step "Installing npm dependencies..."
npm install
fi
# Build production
log_step "Running production build..."
NODE_ENV=production npm run build
if [ ! -d "dist" ]; then
log_error "Build failed: dist/ directory not created"
exit 1
fi
# Count files
FILE_COUNT=$(find dist -type f | wc -l)
TOTAL_SIZE=$(du -sh dist | cut -f1)
log_success "Build completed: $FILE_COUNT files ($TOTAL_SIZE)"
# Verify web.config
if [ -f "dist/web.config" ]; then
log_success "web.config present in build"
else
log_warning "web.config NOT found in build"
fi
}
# =============================================================================
# CREATE DEPLOYMENT PACKAGE
# =============================================================================
create_package() {
local COMPONENT=$1
local TIMESTAMP=$(date +%Y%m%d-%H%M%S)
local PACKAGE_DIR="$PROJECT_ROOT/deploy-package-$TIMESTAMP"
log_step "Creating deployment package..."
log_info "Package: $PACKAGE_DIR"
mkdir -p "$PACKAGE_DIR"
# Frontend
if [ "$COMPONENT" = "all" ] || [ "$COMPONENT" = "frontend" ]; then
log_step "Packaging frontend..."
mkdir -p "$PACKAGE_DIR/frontend"
cp -r "$PROJECT_ROOT/dist/"* "$PACKAGE_DIR/frontend/"
log_success "Frontend packaged"
fi
# Backend
if [ "$COMPONENT" = "all" ] || [ "$COMPONENT" = "backend" ]; then
log_step "Packaging backend..."
mkdir -p "$PACKAGE_DIR/backend"
# Copy backend files (excluding venv, __pycache__, logs, .env)
rsync -av --progress \
--exclude 'venv' \
--exclude '__pycache__' \
--exclude '*.pyc' \
--exclude '*.pyo' \
--exclude '.pytest_cache' \
--exclude 'logs' \
--exclude '.env' \
--exclude '.env.local' \
--exclude '*.log' \
"$PROJECT_ROOT/backend/" "$PACKAGE_DIR/backend/"
# Copy .env.example if exists
if [ -f "$PROJECT_ROOT/backend/.env.example" ]; then
cp "$PROJECT_ROOT/backend/.env.example" "$PACKAGE_DIR/backend/"
fi
log_success "Backend packaged"
fi
# Shared modules
if [ "$COMPONENT" = "all" ] || [ "$COMPONENT" = "backend" ]; then
log_step "Packaging shared modules..."
mkdir -p "$PACKAGE_DIR/shared"
rsync -av --progress \
--exclude '__pycache__' \
--exclude '*.pyc' \
--exclude 'tests' \
"$PROJECT_ROOT/shared/" "$PACKAGE_DIR/shared/"
log_success "Shared modules packaged"
fi
# Config templates
log_step "Packaging config templates..."
if [ -d "$PROJECT_ROOT/deployment/config" ]; then
mkdir -p "$PACKAGE_DIR/config"
cp -r "$PROJECT_ROOT/deployment/config/"* "$PACKAGE_DIR/config/" 2>/dev/null || true
fi
# Deployment scripts
log_step "Packaging deployment scripts..."
mkdir -p "$PACKAGE_DIR/scripts"
SCRIPTS=(
"ROA2WEB-Console.ps1"
"Install-ROA2WEB.ps1"
"Check-And-Deploy.ps1"
"ssh-tunnel.ps1"
"start-backend-service.ps1"
)
for script in "${SCRIPTS[@]}"; do
if [ -f "$PROJECT_ROOT/deployment/windows/scripts/$script" ]; then
cp "$PROJECT_ROOT/deployment/windows/scripts/$script" "$PACKAGE_DIR/scripts/"
fi
done
log_success "Deployment scripts packaged"
# Create README
cat > "$PACKAGE_DIR/README.txt" << EOF
================================================================================
ROA2WEB DEPLOYMENT PACKAGE
Generated: $(date '+%Y-%m-%d %H:%M:%S')
From: Linux/LXC deployment script
================================================================================
CONTENTS:
---------
backend/ Unified FastAPI backend
frontend/ Vue.js SPA (production build)
shared/ Shared Python modules
config/ Configuration templates
scripts/ PowerShell deployment scripts
DEPLOYMENT:
-----------
Server will auto-deploy within 5 minutes (Check-And-Deploy.ps1 scheduled task)
Or manually:
cd scripts
.\ROA2WEB-Console.ps1 -NonInteractive -Action DeployAll
================================================================================
EOF
# Calculate package size
PACKAGE_SIZE=$(du -sh "$PACKAGE_DIR" | cut -f1)
FILE_COUNT=$(find "$PACKAGE_DIR" -type f | wc -l)
log_success "Package created: $FILE_COUNT files ($PACKAGE_SIZE)"
# Return path via global variable (avoid stdout pollution from rsync)
CREATED_PACKAGE_DIR="$PACKAGE_DIR"
}
# =============================================================================
# TRANSFER TO SERVER
# =============================================================================
transfer_to_server() {
local PACKAGE_DIR=$1
local TIMESTAMP=$(date +%Y%m%d-%H%M%S)
local REMOTE_DEPLOY_DIR="deploy-$TIMESTAMP"
local REMOTE_FULL_PATH="C:\\Temp\\$REMOTE_DEPLOY_DIR"
log_step "Transferring to server..."
log_info "Remote path: C:\\Temp\\$REMOTE_DEPLOY_DIR"
# Create remote directory
log_step "Creating remote directory..."
ssh "$SSH_HOST" "New-Item -ItemType Directory -Path 'C:\\Temp\\$REMOTE_DEPLOY_DIR' -Force"
log_success "Remote directory created"
# Transfer files one directory at a time (Bitvise SCP compatibility)
log_step "Uploading files via SCP..."
for dir in "$PACKAGE_DIR"/*/; do
if [ -d "$dir" ]; then
local dirname=$(basename "$dir")
log_info "Uploading $dirname/..."
scp -r "$dir" "roa2web-prod:C:\\Temp\\$REMOTE_DEPLOY_DIR\\$dirname"
fi
done
# Upload root files (README, etc.)
for file in "$PACKAGE_DIR"/*; do
if [ -f "$file" ]; then
local filename=$(basename "$file")
scp "$file" "roa2web-prod:C:\\Temp\\$REMOTE_DEPLOY_DIR\\$filename"
fi
done
log_success "Transfer completed"
# Verify transfer
log_step "Verifying transfer..."
REMOTE_FILE_COUNT=$(ssh "$SSH_HOST" "(Get-ChildItem -Path 'C:\\Temp\\$REMOTE_DEPLOY_DIR' -Recurse -File).Count")
log_success "Remote files: $REMOTE_FILE_COUNT"
echo ""
echo "=========================================="
echo -e "${GREEN} DEPLOYMENT PACKAGE UPLOADED${NC}"
echo "=========================================="
echo ""
echo " Remote path: C:\\Temp\\$REMOTE_DEPLOY_DIR"
echo ""
echo " Server will auto-deploy within 5 minutes."
echo " Or manually deploy:"
echo " ssh $SSH_HOST"
echo " cd C:\\Temp\\$REMOTE_DEPLOY_DIR\\scripts"
echo " .\\ROA2WEB-Console.ps1"
echo ""
}
# =============================================================================
# CLEANUP
# =============================================================================
cleanup_local_package() {
local PACKAGE_DIR=$1
log_step "Cleaning up local package..."
rm -rf "$PACKAGE_DIR"
log_success "Local package removed"
}
# =============================================================================
# MAIN
# =============================================================================
main() {
local COMPONENT=${1:-all}
echo ""
echo "========================================"
echo " ROA2WEB Linux Deployment Script"
echo " Component: $COMPONENT"
echo "========================================"
echo ""
# Handle test command
if [ "$COMPONENT" = "test" ]; then
test_ssh_connection
exit $?
fi
# Validate component
case $COMPONENT in
all|frontend|backend)
;;
*)
echo "Usage: $0 [all|frontend|backend|test]"
exit 1
;;
esac
# Test SSH connection first
if ! test_ssh_connection; then
exit 1
fi
# Build frontend if needed
if [ "$COMPONENT" = "all" ] || [ "$COMPONENT" = "frontend" ]; then
build_frontend
fi
# Create package (sets CREATED_PACKAGE_DIR global variable)
create_package "$COMPONENT"
# Transfer to server
transfer_to_server "$CREATED_PACKAGE_DIR"
# Cleanup
cleanup_local_package "$CREATED_PACKAGE_DIR"
echo ""
echo -e "${GREEN}Deployment completed successfully!${NC}"
echo ""
}
# Run main with all arguments
main "$@"