From 18f86f4721b01debbf95ce8cba6a5454a31e41f0 Mon Sep 17 00:00:00 2001 From: GambleCodez Affiliates Date: Fri, 20 Feb 2026 18:36:39 -0600 Subject: [PATCH] Upgrade prod-run.sh with VPS diagnostics and self-healing checks --- .gitignore | 8 + README.md | 1 + prod-run.sh | 466 +++++++++++++++++++++++++++++++++++++++++++- runewager.logrotate | 7 +- 4 files changed, 468 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 3c7c6fb..98b390e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,11 @@ env.* # Legacy runtime state bot_state.json + +# Generated runtime backups +data/backups/* +!data/backups/.gitkeep + +# Service log artifacts +runewager-*.log +crash.log diff --git a/README.md b/README.md index 26b5f2d..6ce4000 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ sudo apt-get install -y nodejs sudo useradd --system --create-home --shell /usr/sbin/nologin runewager || true sudo mkdir -p /var/www/html/Runewager sudo chown -R "$USER":"$USER" /var/www/html/Runewager +# Keep directory casing exactly as "Runewager" to match systemd paths # 3) Clone or update repo cd /var/www/html/Runewager diff --git a/prod-run.sh b/prod-run.sh index 95aceda..9cd7f24 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -1,14 +1,462 @@ #!/usr/bin/env bash set -euo pipefail -if ! command -v node >/dev/null 2>&1; then - echo "ERROR: node is not available on PATH" >&2 - exit 1 -fi +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$SCRIPT_DIR" +INDEX_FILE="$PROJECT_DIR/index.js" +ENV_FILE="$PROJECT_DIR/.env" +LOG_DIR="$PROJECT_DIR/logs" +BOT_LOG="$LOG_DIR/runewager.log" +SERVICE_FILE="/etc/systemd/system/runewager.service" +LOGROTATE_FILE="/etc/logrotate.d/runewager" +CRON_LINE='0 3 * * * /usr/sbin/logrotate -f /etc/logrotate.d/runewager' -if [ ! -f "$(dirname "$0")/index.js" ]; then - echo "ERROR: index.js not found in $(dirname "$0")" >&2 - exit 1 -fi +say() { + printf '[runewager] %s\n' "$*" +} -exec node "$(dirname "$0")/index.js" +warn() { + printf '[runewager][warn] %s\n' "$*" >&2 +} + +err() { + printf '[runewager][error] %s\n' "$*" >&2 +} + +ensure_dir() { + [ -d "$1" ] || mkdir -p "$1" +} + +ensure_file() { + [ -f "$1" ] || : > "$1" +} + +fix_working_directory() { + if [ "$(pwd)" != "$PROJECT_DIR" ]; then + say "Switching working directory to $PROJECT_DIR" + cd "$PROJECT_DIR" + fi +} + +fix_merge_conflicts() { + if command -v rg >/dev/null 2>&1 && rg -n '^(<<<<<<<|=======|>>>>>>>)' "$PROJECT_DIR"/*.js "$PROJECT_DIR"/*.sh "$PROJECT_DIR"/*.md >/dev/null 2>&1; then + warn "Merge conflict markers detected. Auto-fix is unsafe; please resolve conflict markers manually." + fi +} + +fix_lockfiles() { + if [ -f "$PROJECT_DIR/package-lock.json" ] && [ -f "$PROJECT_DIR/yarn.lock" ]; then + warn "Both package-lock.json and yarn.lock detected. Removing yarn.lock to keep npm workflow deterministic." + rm -f "$PROJECT_DIR/yarn.lock" + fi + if [ -f "$PROJECT_DIR/package-lock.json" ] && [ -f "$PROJECT_DIR/pnpm-lock.yaml" ]; then + warn "Both package-lock.json and pnpm-lock.yaml detected. Removing pnpm-lock.yaml to keep npm workflow deterministic." + rm -f "$PROJECT_DIR/pnpm-lock.yaml" + fi +} + +fix_git_remote_and_branch() { + if command -v git >/dev/null 2>&1 && git -C "$PROJECT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + local expected='https://github.com/gamblecodezcom/Runewager.git' + local origin='' + origin="$(git -C "$PROJECT_DIR" remote get-url origin 2>/dev/null || true)" + if [ -n "$origin" ] && [ "$origin" != "$expected" ] && [ "$origin" != 'git@github.com:gamblecodezcom/Runewager.git' ]; then + warn "Fixing git origin URL" + git -C "$PROJECT_DIR" remote set-url origin "$expected" || warn "Unable to set git origin URL" + fi + + local current_branch='' + current_branch="$(git -C "$PROJECT_DIR" branch --show-current 2>/dev/null || true)" + if [ -n "$current_branch" ] && [ "$current_branch" != 'main' ]; then + warn "Switching to main branch" + git -C "$PROJECT_DIR" fetch origin main || warn "Could not fetch origin/main" + git -C "$PROJECT_DIR" checkout -f main || warn "Could not checkout main" + git -C "$PROJECT_DIR" reset --hard origin/main || warn "Could not hard reset to origin/main" + fi + fi +} + +fix_node_modules() { + if [ ! -d "$PROJECT_DIR/node_modules" ] || [ ! -f "$PROJECT_DIR/node_modules/.package-lock.json" ]; then + say "Installing dependencies with npm ci --omit=dev" + npm ci --omit=dev + fi +} + +fix_onboarding_assets() { + local missing=0 + for image in discord_code_generation.png discord_verify.png promo_entry.png; do + if [ ! -f "$PROJECT_DIR/images/$image" ]; then + warn "Missing onboarding asset: images/$image" + missing=1 + fi + done + if [ "$missing" -eq 1 ]; then + warn "Onboarding image assets are missing; restore files in images/." + fi +} + +normalize_env() { + if [ ! -f "$ENV_FILE" ]; then + if [ -f "$PROJECT_DIR/.env.example" ]; then + say "Creating .env from .env.example" + cp "$PROJECT_DIR/.env.example" "$ENV_FILE" + else + warn ".env and .env.example are missing; creating blank .env" + : > "$ENV_FILE" + fi + fi + + local keys=( + AFFILIATE_SOURCE + ADMIN_IDS + BOT_TOKEN + TELEGRAM_BOT_TOKEN + DISCORD_CODE_GENERATION_IMAGE_URL + DISCORD_VERIFY_IMAGE_URL + MAX_ANALYTICS_EVENTS + MAX_ONBOARDING_STEPS_HISTORY + MINI_APP_CLAIM_URL + MINI_APP_PLAY_URL + MINI_APP_PROFILE_URL + PORT + PROMO_AMOUNT_SC + PROMO_BONUS_RULE + PROMO_CLAIM_LIMIT + PROMO_CODE + PROMO_ENTRY_IMAGE_URL + RW_DISCORD_JOIN + RW_DISCORD_LINK + RW_DISCORD_SUPPORT + ) + + local k + for k in "${keys[@]}"; do + if ! grep -Eq "^${k}=" "$ENV_FILE"; then + warn "Adding missing .env key placeholder: $k" + printf '%s=\n' "$k" >> "$ENV_FILE" + fi + done + + if command -v awk >/dev/null 2>&1; then + awk 'NF && $0 !~ /^\s*#/{print}' "$ENV_FILE" | LC_ALL=C sort -u > "$ENV_FILE.tmp" || true + if [ -s "$ENV_FILE.tmp" ]; then + mv "$ENV_FILE.tmp" "$ENV_FILE" + say "Alphabetized .env keys" + else + rm -f "$ENV_FILE.tmp" + fi + fi +} + +validate_env_values() { + local required=( + BOT_TOKEN + TELEGRAM_BOT_TOKEN + RW_DISCORD_JOIN + RW_DISCORD_LINK + RW_DISCORD_SUPPORT + MINI_APP_CLAIM_URL + MINI_APP_PLAY_URL + MINI_APP_PROFILE_URL + PROMO_AMOUNT_SC + PROMO_BONUS_RULE + PROMO_CLAIM_LIMIT + PROMO_CODE + PORT + ) + local missing=0 + + local k + for k in "${required[@]}"; do + if ! grep -Eq "^${k}=[^[:space:]].*" "$ENV_FILE"; then + warn "Required .env value is empty: $k" + missing=1 + fi + done + + # Check Discord image URLs specifically. + for k in DISCORD_CODE_GENERATION_IMAGE_URL DISCORD_VERIFY_IMAGE_URL PROMO_ENTRY_IMAGE_URL; do + if ! grep -Eq "^${k}=https?://" "$ENV_FILE"; then + warn "Missing or invalid URL for $k" + missing=1 + fi + done + + if [ "$missing" -eq 1 ]; then + warn "One or more required env values are missing; update .env before production start." + fi +} + +fix_json_files() { + local json + shopt -s nullglob + for json in "$PROJECT_DIR"/*.json "$PROJECT_DIR"/data/*.json; do + if ! node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$json" >/dev/null 2>&1; then + warn "Invalid JSON detected: $json (auto-fix skipped to avoid data loss)" + fi + done + shopt -u nullglob +} + +fix_entrypoint_and_scripts() { + if [ ! -f "$INDEX_FILE" ]; then + err "index.js is missing and cannot be auto-generated safely" + exit 1 + fi + if [ ! -f "$PROJECT_DIR/package.json" ]; then + err "package.json is missing" + exit 1 + fi + + if ! node -e "const p=require('./package.json');if(!p.scripts||!p.scripts.start)process.exit(1)" >/dev/null 2>&1; then + warn "package.json scripts.start missing; adding scripts.start=node index.js" + node - <<'NODE' +const fs = require('fs'); +const p = JSON.parse(fs.readFileSync('package.json','utf8')); +p.scripts = p.scripts || {}; +p.scripts.start = p.scripts.start || 'node index.js'; +fs.writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n'); +NODE + fi +} + +fix_broken_symlinks() { + if command -v find >/dev/null 2>&1; then + local broken='' + broken="$(find "$PROJECT_DIR" -xtype l 2>/dev/null || true)" + if [ -n "$broken" ]; then + warn "Broken symlinks found; removing" + while IFS= read -r link; do + [ -n "$link" ] && rm -f "$link" + done <<< "$broken" + fi + fi +} + +fix_require_and_import_hints() { + if ! node --check "$INDEX_FILE" >/dev/null 2>&1; then + warn "Syntax issues detected in index.js; please fix manually." + fi + + if ! npm ls --omit=dev >/dev/null 2>&1; then + warn "Dependency graph has missing modules; running npm ci --omit=dev" + npm ci --omit=dev || true + fi +} + +ensure_logrotate_config() { + local conf + conf=$(cat <<'CONF' +/var/www/html/Runewager/logs/runewager*.log /var/www/html/Runewager/logs/crash.log { + daily + size 5M + rotate 7 + missingok + notifempty + compress + delaycompress + copytruncate + create 0640 runewager runewager +} +CONF +) + + if [ ! -f "$LOGROTATE_FILE" ]; then + warn "$LOGROTATE_FILE missing; creating" + if [ -w /etc/logrotate.d ]; then + printf '%s\n' "$conf" > "$LOGROTATE_FILE" + else + warn "No permission to write $LOGROTATE_FILE" + fi + fi + + if [ -f "$LOGROTATE_FILE" ] && command -v logrotate >/dev/null 2>&1; then + if ! logrotate -d "$LOGROTATE_FILE" >/dev/null 2>&1; then + warn "logrotate dry-run failed for $LOGROTATE_FILE" + else + say "logrotate config validation passed" + fi + fi +} + +ensure_cron_fallback() { + if ! command -v crontab >/dev/null 2>&1; then + warn "crontab is unavailable; skipping cron fallback" + return + fi + + local current + current="$(crontab -l 2>/dev/null || true)" + if ! printf '%s\n' "$current" | grep -Fqx "$CRON_LINE"; then + warn "Adding daily forced logrotate cron fallback" + (printf '%s\n' "$current"; printf '%s\n' "$CRON_LINE") | sed '/^$/N;/^\n$/D' | crontab - + fi +} + +ensure_systemd_service() { + local conf + conf=$(cat <<'CONF' +[Unit] +Description=Runewager bot +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=/var/www/html/Runewager +ExecStart=/var/www/html/Runewager/prod-run.sh +User=runewager +Group=runewager +EnvironmentFile=/var/www/html/Runewager/.env +Environment=NODE_ENV=production +Restart=always +RestartSec=5 +KillSignal=SIGTERM +TimeoutStopSec=20 +StandardOutput=append:/var/www/html/Runewager/logs/runewager-1.log +StandardError=inherit + +[Install] +WantedBy=multi-user.target +CONF +) + + if [ ! -f "$SERVICE_FILE" ]; then + warn "$SERVICE_FILE missing" + if [ -w /etc/systemd/system ]; then + printf '%s\n' "$conf" > "$SERVICE_FILE" + say "Created $SERVICE_FILE" + else + warn "No permission to create $SERVICE_FILE" + return + fi + fi + + local required=( + 'EnvironmentFile=/var/www/html/Runewager/.env' + 'Environment=NODE_ENV=production' + 'Restart=always' + 'RestartSec=5' + 'KillSignal=SIGTERM' + 'TimeoutStopSec=20' + ) + + local missing=0 + local line + for line in "${required[@]}"; do + if ! grep -Fqx "$line" "$SERVICE_FILE"; then + warn "Missing systemd directive: $line" + missing=1 + fi + done + + if [ "$missing" -eq 1 ] && [ -w "$SERVICE_FILE" ]; then + warn "Replacing $SERVICE_FILE with corrected content" + printf '%s\n' "$conf" > "$SERVICE_FILE" + fi + + if command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload >/dev/null 2>&1 || true + fi +} + +print_human_errors() { + say "Last 200 lines of $BOT_LOG" + tail -n 200 "$BOT_LOG" || true + + local blob + blob="$(tail -n 200 "$BOT_LOG" 2>/dev/null || true)" + + say "Human-readable diagnosis" + printf '%s\n' "$blob" | grep -qi 'Cannot find module' && say "Root cause: A Node module is missing or a require path is wrong. Run npm ci and verify relative import paths." + printf '%s\n' "$blob" | grep -qi 'SyntaxError' && say "Root cause: JavaScript syntax is invalid. Review the file/line listed in the log." + printf '%s\n' "$blob" | grep -qiE 'EACCES|permission denied' && say "Root cause: File permissions are too strict for the service user. Fix ownership/permissions for app, logs, and .env." + printf '%s\n' "$blob" | grep -qiE 'ENOENT|no such file or directory' && say "Root cause: A required file/path is missing (env, logs, index.js, or runtime dir)." + printf '%s\n' "$blob" | grep -qiE 'ECONNREFUSED|ETIMEDOUT|getaddrinfo' && say "Root cause: Network or upstream service is unavailable. Check DNS/firewall/connectivity." + printf '%s\n' "$blob" | grep -qiE 'BOT_TOKEN|TELEGRAM_BOT_TOKEN|RW_DISCORD_' && say "Root cause: Required env variables are missing or empty." + + if [ -z "$blob" ]; then + say "No recent logs found; app may not have started yet." + fi +} + +is_bot_running() { + pgrep -f "node .*index\.js" >/dev/null 2>&1 +} + +check_bot_status_twice() { + if is_bot_running; then + say "Bot running" + else + say "Bot NOT running" + fi + sleep 15 + if is_bot_running; then + say "Bot running" + else + say "Bot NOT running" + fi +} + +start_background_if_needed() { + if command -v systemctl >/dev/null 2>&1 && [ -n "${INVOCATION_ID:-}" ]; then + say "Detected systemd-managed execution; running in foreground for systemd supervision" + exec node "$INDEX_FILE" >> "$BOT_LOG" 2>&1 + fi + + if is_bot_running; then + say "Bot already running; skip new launch" + return + fi + + say "Launching bot in background with nohup" + nohup node "$INDEX_FILE" >> "$BOT_LOG" 2>&1 < /dev/null & + disown || true + sleep 1 + if is_bot_running; then + say "Background launch successful" + else + warn "Background launch failed; inspect $BOT_LOG" + fi +} + +main() { + fix_working_directory + + if ! command -v node >/dev/null 2>&1; then + err "node is not available on PATH" + exit 1 + fi + if ! command -v npm >/dev/null 2>&1; then + err "npm is not available on PATH" + exit 1 + fi + + ensure_dir "$LOG_DIR" + ensure_dir "$PROJECT_DIR/data" + ensure_dir "$PROJECT_DIR/data/backups" + ensure_file "$BOT_LOG" + + fix_merge_conflicts + fix_lockfiles + fix_git_remote_and_branch + fix_node_modules + fix_entrypoint_and_scripts + fix_broken_symlinks + normalize_env + validate_env_values + fix_json_files + fix_onboarding_assets + fix_require_and_import_hints + ensure_logrotate_config + ensure_cron_fallback + ensure_systemd_service + + print_human_errors + check_bot_status_twice + start_background_if_needed + check_bot_status_twice +} + +main "$@" diff --git a/runewager.logrotate b/runewager.logrotate index 1bc2d7a..c363d8d 100644 --- a/runewager.logrotate +++ b/runewager.logrotate @@ -1,4 +1,4 @@ -/var/www/runewager/logs/runewager-1.log /var/www/runewager/logs/crash.log { +/var/www/html/Runewager/logs/runewager*.log /var/www/html/Runewager/logs/crash.log /var/www/html/runewager/logs/runewager*.log /var/www/html/runewager/logs/crash.log { daily size 5M rotate 7 @@ -6,9 +6,6 @@ notifempty compress delaycompress + copytruncate create 0640 runewager runewager - sharedscripts - postrotate - /bin/systemctl try-restart runewager.service >/dev/null 2>&1 || true - endscript }