Restore to commit 74e578279624c6045ca440a3459ebfa1f8d54191
This commit is contained in:
381
scripts/entrypoint.sh
Executable file
381
scripts/entrypoint.sh
Executable file
@@ -0,0 +1,381 @@
|
||||
#!/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 and ttyd..."
|
||||
|
||||
# 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:-4000}"
|
||||
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
|
||||
|
||||
# Start ttyd proxy instead of ttyd directly (on-demand activation)
|
||||
log "Starting ttyd proxy (on-demand mode)"
|
||||
log "ttyd will only run when port 4001 is accessed"
|
||||
log "Idle timeout: 5 minutes of inactivity"
|
||||
exec node /usr/local/bin/ttyd-proxy.js
|
||||
Reference in New Issue
Block a user