Files
shopify-ai-backup/scripts/entrypoint.sh

380 lines
14 KiB
Bash
Executable File

#!/bin/bash
set -e
# Source diagnostic logger if available
if [ -f "/usr/local/bin/diagnostic-logger.sh" ]; then
. /usr/local/bin/diagnostic-logger.sh
fi
# Helper function to log messages
log() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[${timestamp}] $*" >&2
}
# Sanitize environment variables by removing invisible Unicode characters
# This fixes the Portainer deployment issue where U+200E and similar characters
# cause "unexpected character" errors in variable names
sanitize_env_vars() {
log "Sanitizing environment variables..."
# Create a secure temporary file
local temp_env
temp_env=$(mktemp /tmp/sanitized_env.XXXXXX)
# Export current environment to a file, then clean it
export -p > "$temp_env"
# Remove common invisible Unicode characters in a single sed command:
# - U+200E (Left-to-Right Mark) - E2 80 8E
# - U+200F (Right-to-Left Mark) - E2 80 8F
# - U+200B (Zero Width Space) - E2 80 8B
# - U+FEFF (Zero Width No-Break Space / BOM) - EF BB BF
# - U+202A-202E (Directional formatting characters)
sed -i \
-e 's/\xE2\x80\x8E//g' \
-e 's/\xE2\x80\x8F//g' \
-e 's/\xE2\x80\x8B//g' \
-e 's/\xEF\xBB\xBF//g' \
-e 's/\xE2\x80\xAA//g' \
-e 's/\xE2\x80\xAB//g' \
-e 's/\xE2\x80\xAC//g' \
-e 's/\xE2\x80\xAD//g' \
-e 's/\xE2\x80\xAE//g' \
"$temp_env" 2>/dev/null
# Source the sanitized environment
# If sourcing fails, log a warning but continue (environment may be partially set)
if ! source "$temp_env" 2>/dev/null; then
log "WARNING: Failed to source sanitized environment. Some variables may not be set correctly."
fi
# Clean up temporary file
rm -f "$temp_env"
log "Environment variables sanitized successfully"
}
# Sanitize environment variables on startup
log "=== STARTUP INITIALIZATION ==="
log "Container ID: $(hostname 2>/dev/null || 'unknown')"
log "Timestamp: $(date '+%Y-%m-%d %H:%M:%S %Z')"
# Initialize diagnostic logging only if file exists and we're in debug mode
if [ -f "/usr/local/bin/diagnostic-logger.sh" ] && [ "${DEBUG_LOGGING:-false}" = "true" ]; then
. /usr/local/bin/diagnostic-logger.sh
log_system_info
validate_environment
rotate_logs
fi
sanitize_env_vars
log "=== ENVIRONMENT SANITIZATION COMPLETE ==="
# Repository configuration
REPO_URL="${REPO_URL:-}"
REPO_DIR="/home/web/data"
REPO_BRANCH="${REPO_BRANCH:-main}"
GITHUB_USERNAME="${GITHUB_USERNAME:-}"
GITHUB_PAT="${GITHUB_PAT:-}"
# Helper function to get authenticated repository URL
get_auth_url() {
local url="$1"
if [ -n "$GITHUB_USERNAME" ] && [ -n "$GITHUB_PAT" ]; then
# Extract the repository path from the URL
local repo_path=$(echo "$url" | sed 's|https://github.com/||')
echo "https://${GITHUB_USERNAME}:${GITHUB_PAT}@github.com/${repo_path}"
else
echo "$url"
fi
}
log "Initializing Shopify AI App Builder..."
# Log repository configuration details
log "Repository Configuration:"
log " URL: ${REPO_URL:-'NOT SET'}"
log " Branch: ${REPO_BRANCH:-'NOT SET'}"
log " Directory: ${REPO_DIR}"
log " GitHub Username: ${GITHUB_USERNAME:-'NOT SET'}"
log " GitHub PAT: ${GITHUB_PAT:+SET (hidden)}${GITHUB_PAT:-NOT SET}"
# Only clone/pull if REPO_URL is set
if [ -n "$REPO_URL" ]; then
log "Repository URL: $REPO_URL"
log "Repository directory: $REPO_DIR"
log "Default branch: $REPO_BRANCH"
# Check if authentication credentials are available
if [ -n "$GITHUB_USERNAME" ] && [ -n "$GITHUB_PAT" ]; then
log "GitHub authentication credentials found for user: $GITHUB_USERNAME"
else
log "WARNING: No GitHub authentication credentials found. Private repository access may fail."
fi
# Check if git is available
if ! command -v git &> /dev/null; then
log "ERROR: git is not available in the container"
exit 1
fi
# Check if the repository directory is empty or doesn't have .git
if [ ! -d "$REPO_DIR/.git" ]; then
# Directory doesn't exist or .git is missing - need to clone
if [ -d "$REPO_DIR" ] && [ "$(ls -A "$REPO_DIR")" ]; then
# Directory exists but is not a git repo - back it up
log "WARNING: $REPO_DIR exists but is not a git repository. Backing up to ${REPO_DIR}.backup"
mv "$REPO_DIR" "${REPO_DIR}.backup"
fi
log "Repository not found. Cloning $REPO_URL into $REPO_DIR..."
auth_url=$(get_auth_url "$REPO_URL")
git clone "$auth_url" "$REPO_DIR"
cd "$REPO_DIR"
log "Successfully cloned repository"
else
# Repository exists, pull latest changes
cd "$REPO_DIR"
log "Repository found at $REPO_DIR. Pulling latest changes from $REPO_BRANCH..."
# Update remote URL to use authentication if credentials are available
if [ -n "$GITHUB_USERNAME" ] && [ -n "$GITHUB_PAT" ]; then
auth_url=$(get_auth_url "$REPO_URL")
log "Updating remote URL with authentication credentials"
git remote set-url origin "$auth_url"
fi
# Check if we're on a detached HEAD or have uncommitted changes
if git diff --quiet && git diff --cached --quiet; then
git fetch origin
# Use plain git pull per policy (do not force origin/branch explicitly here)
git pull || {
log "WARNING: Failed to pull from $REPO_BRANCH, attempting to pull from any available branch"
git pull || log "WARNING: Pull operation failed, continuing anyway"
}
log "Successfully pulled latest changes"
else
log "WARNING: Repository has uncommitted changes. Skipping pull to avoid conflicts."
log "Run 'git status' to see changes, then 'git pull' manually if desired"
fi
fi
log "Repository is ready at $REPO_DIR"
else
log "No REPO_URL set - starting with empty workspace"
mkdir -p "$REPO_DIR"
fi
log "Starting Shopify AI App Builder service..."
# Use /opt/webchat directly as it contains the actual server.js with node_modules
# /opt/webchat_v2 is just a wrapper and causes module loading failures
CHAT_APP_DIR="${CHAT_APP_DIR:-/opt/webchat}"
CHAT_APP_FALLBACK="/opt/webchat_v2"
CHAT_PORT="${CHAT_PORT:-4500}"
CHAT_HOST="${CHAT_HOST:-0.0.0.0}"
ACCESS_PASSWORD="${ACCESS_PASSWORD:-}"
# Persist opencode installation & user config
# Move/Copy /root/.opencode to $REPO_DIR/.opencode if not already present
# Then symlink /root/.opencode -> $REPO_DIR/.opencode so opencode state (connections, providers)
# are persisted on container restarts while keeping the install accessible for runtime.
PERSISTED_OPENCODE_DIR="$REPO_DIR/.opencode"
OPENCODE_INSTALL_DIR="/root/.opencode"
if [ -d "$OPENCODE_INSTALL_DIR" ]; then
# If persisted dir does not exist, copy initial files so the app continues to work
if [ ! -d "$PERSISTED_OPENCODE_DIR" ] || [ -z "$(ls -A $PERSISTED_OPENCODE_DIR 2>/dev/null)" ]; then
log "Persisting opencode into $PERSISTED_OPENCODE_DIR"
mkdir -p "$PERSISTED_OPENCODE_DIR"
# Copy installed files to the persisted folder to preserve both the binary and state
cp -a "$OPENCODE_INSTALL_DIR/." "$PERSISTED_OPENCODE_DIR/" || true
chown -R root:root "$PERSISTED_OPENCODE_DIR" || true
fi
# Replace the install dir with a symlink to the persisted directory (only if not a symlink already)
if [ -e "$OPENCODE_INSTALL_DIR" ] && [ ! -L "$OPENCODE_INSTALL_DIR" ]; then
log "Symlinking $OPENCODE_INSTALL_DIR -> $PERSISTED_OPENCODE_DIR"
rm -rf "$OPENCODE_INSTALL_DIR"
ln -s "$PERSISTED_OPENCODE_DIR" "$OPENCODE_INSTALL_DIR"
elif [ -L "$OPENCODE_INSTALL_DIR" ]; then
log "$OPENCODE_INSTALL_DIR already symlinked; skipping"
fi
fi
# Only ensure opencode command exists - qwen and gemini commands should not be aliased to opencode
ensure_cli_wrappers() {
local bin_dir="$PERSISTED_OPENCODE_DIR/bin"
mkdir -p "$bin_dir"
# Only create symlink for opencode command itself
if [ ! -L "/usr/local/bin/opencode" ]; then
ln -sf "$bin_dir/opencode" "/usr/local/bin/opencode"
fi
}
ensure_cli_wrappers
# Ensure a root-level opencode.json exists so OpenCode can discover the configured
# Ollama/OpenAI-compatible model. This file lives in the persisted storage root
# (/home/web/data) and is overwritten on every container start.
ensure_root_opencode_config() {
local config_path="$REPO_DIR/opencode.json"
# Allow overrides while defaulting to the PluginCompass Ollama gateway + qwen3 model
export OPENCODE_OLLAMA_BASE_URL="${OPENCODE_OLLAMA_BASE_URL:-https://ollama.plugincompass.com}"
export OPENCODE_OLLAMA_MODEL="${OPENCODE_OLLAMA_MODEL:-qwen3:0.6b}"
# Prefer an explicit Ollama key, fall back to OPENCODE_API_KEY (existing env var)
export OPENCODE_OLLAMA_API_KEY="${OPENCODE_OLLAMA_API_KEY:-${OPENCODE_API_KEY:-}}"
mkdir -p "$(dirname "$config_path")"
log "Writing OpenCode config: ${config_path} (baseURL=${OPENCODE_OLLAMA_BASE_URL}, model=${OPENCODE_OLLAMA_MODEL})"
python3 - <<'PY' > "$config_path"
import json, os
base_url = (os.environ.get("OPENCODE_OLLAMA_BASE_URL") or "https://ollama.plugincompass.com").rstrip("/")
model_id = os.environ.get("OPENCODE_OLLAMA_MODEL") or "qwen3:0.6b"
api_key = (os.environ.get("OPENCODE_OLLAMA_API_KEY") or "").strip()
provider_name = os.environ.get("OPENCODE_OLLAMA_PROVIDER") or "ollama"
provider_cfg = {
"options": {
"baseURL": base_url,
},
"models": {
model_id: {
"id": model_id,
"name": model_id,
"tool_call": True,
"temperature": True,
}
},
}
if api_key:
provider_cfg["options"]["apiKey"] = api_key
cfg = {
"$schema": "https://opencode.ai/config.json",
"model": f"{provider_name}/{model_id}",
"small_model": f"{provider_name}/{model_id}",
"provider": {
provider_name: provider_cfg,
},
}
print(json.dumps(cfg, indent=2))
PY
chmod 600 "$config_path" 2>/dev/null || true
}
ensure_root_opencode_config
# Set up signal handlers to properly clean up background processes
cleanup() {
local signal="$1"
log "=== SIGNAL RECEIVED: ${signal} ==="
log "Initiating graceful shutdown..."
# Log final resource snapshot before shutdown
if type monitor_resources &>/dev/null; then
log "Final resource snapshot:"
monitor_resources
fi
if [ -n "$MONITOR_PID" ] && kill -0 "$MONITOR_PID" 2>/dev/null; then
log "Terminating monitor process (PID: $MONITOR_PID)"
kill "$MONITOR_PID" 2>/dev/null || true
wait "$MONITOR_PID" 2>/dev/null || true
log "Monitor process terminated"
fi
if [ -n "$CHAT_PID" ] && kill -0 "$CHAT_PID" 2>/dev/null; then
log "Terminating chat service (PID: $CHAT_PID) - giving it time for graceful shutdown"
kill "$CHAT_PID" 2>/dev/null || true
# Wait up to 25 seconds for graceful shutdown (Docker stop_grace_period is 30s)
for i in $(seq 1 25); do
if ! kill -0 "$CHAT_PID" 2>/dev/null; then
log "Chat service terminated gracefully (${i} seconds)"
if type diag_log &>/dev/null; then
diag_log "INFO" "Chat service shutdown complete"
fi
exit 0
fi
sleep 1
done
log "WARNING: Chat service did not terminate gracefully, forcing exit"
kill -9 "$CHAT_PID" 2>/dev/null || true
fi
log "=== SHUTDOWN COMPLETE ==="
exit 0
}
# Set up traps for common signals
trap cleanup SIGTERM SIGINT SIGQUIT SIGHUP
if [ -f "$CHAT_APP_DIR/server.js" ]; then
log "Launching chat service on ${CHAT_HOST}:${CHAT_PORT} from $CHAT_APP_DIR"
log "Environment: CHAT_PORT=${CHAT_PORT} CHAT_HOST=${CHAT_HOST} CHAT_DATA_ROOT=${REPO_DIR} CHAT_REPO_ROOT=${REPO_DIR}"
CHAT_PORT=$CHAT_PORT CHAT_HOST=$CHAT_HOST CHAT_DATA_ROOT=$REPO_DIR CHAT_REPO_ROOT=$REPO_DIR node "$CHAT_APP_DIR/server.js" 2>&1 &
CHAT_PID=$!
log "Chat service started with PID: $CHAT_PID"
elif [ -f "$CHAT_APP_FALLBACK/server.js" ]; then
log "Primary chat service not found at $CHAT_APP_DIR, trying fallback at $CHAT_APP_FALLBACK"
log "Launching chat service on ${CHAT_HOST}:${CHAT_PORT} from $CHAT_APP_FALLBACK"
log "Environment: CHAT_PORT=${CHAT_PORT} CHAT_HOST=${CHAT_HOST} CHAT_DATA_ROOT=${REPO_DIR} CHAT_REPO_ROOT=${REPO_DIR}"
CHAT_PORT=$CHAT_PORT CHAT_HOST=$CHAT_HOST CHAT_DATA_ROOT=$REPO_DIR CHAT_REPO_ROOT=$REPO_DIR node "$CHAT_APP_FALLBACK/server.js" 2>&1 &
CHAT_PID=$!
log "Chat service started with PID: $CHAT_PID"
else
log "ERROR: Chat service not found at $CHAT_APP_DIR or $CHAT_APP_FALLBACK; skipping startup"
exit 1
fi
# Log initial service status after startup
sleep 2
if type check_service_status &>/dev/null; then
check_service_status "chat" "$CHAT_PORT" "$CHAT_PID"
fi
# Monitor chat service health
if [ -n "$CHAT_PID" ]; then
(
# Initial check
sleep 5
if type check_service_status &>/dev/null; then
check_service_status "chat" "$CHAT_PORT" "$CHAT_PID"
fi
# Periodic monitoring
check_count=0
while kill -0 "$CHAT_PID" 2>/dev/null; do
sleep 30
check_count=$((check_count + 1))
# Every 2 minutes (4 checks), do resource monitoring
if [ $((check_count % 4)) -eq 0 ] && type monitor_resources &>/dev/null; then
monitor_resources
fi
# Every 5 minutes (10 checks), do service status check
if [ $((check_count % 10)) -eq 0 ] && type check_service_status &>/dev/null; then
check_service_status "chat" "$CHAT_PORT" "$CHAT_PID"
fi
done
log "ERROR: Chat service (PID: $CHAT_PID) has exited unexpectedly"
if type diag_log &>/dev/null; then
diag_log "ERROR" "Chat service exited unexpectedly after ${check_count} checks"
fi
) &
MONITOR_PID=$!
log "Health monitor started with PID: $MONITOR_PID"
fi
# Keep the container running
log "Container started successfully. Waiting for chat service..."
wait "$CHAT_PID" || true