Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 52 additions & 15 deletions deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# vps — triggered by a manual SSH/shell call on the VPS
#
# What it does:
# 0. Skip deploy if VPS already has the latest commit
# 0. Skip deploy if VPS already has the latest commit (and service is active)
# 1. Stop the systemd service (prevents file locks)
# 2. git fetch --all + git reset --hard origin/main + git clean
# 3. npm ci --omit=dev (install/update production deps)
Expand All @@ -38,14 +38,16 @@ say "Date: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
cd "$PROJECT_DIR"

# ---------------------------------------------------------
# Load .env so BOT_TOKEN and ADMIN_IDS are available for
# Telegram notifications throughout the script.
# Load only the variables needed for Telegram notifications.
# Using set -o allexport would export every line in .env
# (including unrelated or potentially sensitive variables).
# Instead, extract only BOT_TOKEN and ADMIN_IDS explicitly.
# ---------------------------------------------------------
if [[ -f "$PROJECT_DIR/.env" ]]; then
set -o allexport
# shellcheck source=/dev/null
source "$PROJECT_DIR/.env"
set +o allexport
_env_file="$PROJECT_DIR/.env"
BOT_TOKEN="$(grep -E '^BOT_TOKEN=' "$_env_file" | head -1 | cut -d= -f2- | tr -d '"'"'"' | tr -d $'\r')" || true
ADMIN_IDS="$(grep -E '^ADMIN_IDS=' "$_env_file" | head -1 | cut -d= -f2- | tr -d '"'"'"' | tr -d $'\r')" || true
export BOT_TOKEN ADMIN_IDS
fi

# ---------------------------------------------------------
Expand All @@ -72,14 +74,31 @@ send_admin() {
}

# ---------------------------------------------------------
# Error trap — always notify admins if the script fails
# Track whether the service was stopped so the ERR trap can
# attempt a recovery restart if the deploy fails mid-way.
# ---------------------------------------------------------
_SERVICE_STOPPED=false

# ---------------------------------------------------------
# Error trap — notify admins and attempt service recovery.
# ${BASH_LINENO[0]} gives the caller's line number when
# evaluated inside an ERR trap (more reliable than $LINENO).
# ---------------------------------------------------------
_on_error() {
local line="$1"
local line="${BASH_LINENO[0]:-?}"
warn "Script failed at line $line"
send_admin "❌ Deploy failed at line $line — check VPS logs. (source: $DEPLOY_SOURCE)"
send_admin "❌ Deploy FAILED at line $line — check VPS logs. (source: $DEPLOY_SOURCE)"
# If the service was already stopped before the failure, try to bring it
# back up on whatever code is currently on disk so the bot isn't left down.
if [[ "$_SERVICE_STOPPED" == "true" ]] && command -v systemctl >/dev/null 2>&1; then
warn "Attempting service recovery restart on existing code…"
systemctl start "${APP_NAME}.service" || true
local _recovery_status
_recovery_status="$(systemctl is-active "${APP_NAME}.service" 2>/dev/null || echo unknown)"
send_admin "🔁 Recovery restart attempted after deploy failure — service is now: $_recovery_status"
fi
}
trap '_on_error "$LINENO"' ERR
trap '_on_error' ERR

# ---------------------------------------------------------
# 0) Skip deploy if VPS already has the latest commit
Expand Down Expand Up @@ -119,6 +138,7 @@ if command -v systemctl >/dev/null 2>&1; then
say "Step 1/4 — Stopping ${APP_NAME} service…"
send_admin "🛑 Stopping bot service…"
systemctl stop "${APP_NAME}.service" || true
_SERVICE_STOPPED=true
fi

# ---------------------------------------------------------
Expand Down Expand Up @@ -147,21 +167,37 @@ else
# Restart the service so the bot comes back up on the old code
if command -v systemctl >/dev/null 2>&1; then
systemctl start "${APP_NAME}.service" || true
_SERVICE_STOPPED=false
fi
exit 1
fi

# ---------------------------------------------------------
# 3) Install / update production dependencies
# Wrapped in a conditional identical to the git guard:
# if npm fails the service is left down without this guard.
# On failure we restart on the existing node_modules so
# the bot comes back up on whatever was last working.
# ---------------------------------------------------------
say "Step 3/4 — Installing production dependencies…"
send_admin "📦 Installing dependencies…"
if [[ -f package-lock.json ]]; then
npm ci --omit=dev

NPM_CMD="npm install --omit=dev"
[[ -f package-lock.json ]] && NPM_CMD="npm ci --omit=dev"

NPM_OUT=""
if NPM_OUT=$(${NPM_CMD} 2>&1); then
say "Dependencies installed."
else
npm install --omit=dev
warn "npm install failed — restarting bot on existing node_modules"
warn "npm output: $NPM_OUT"
send_admin "❌ npm install failed — restarting bot on existing deps. Error: ${NPM_OUT:0:200} (source: $DEPLOY_SOURCE)"
if command -v systemctl >/dev/null 2>&1; then
systemctl start "${APP_NAME}.service" || true
_SERVICE_STOPPED=false
fi
exit 1
fi
say "Dependencies installed."

# ---------------------------------------------------------
# 4) Start bot via systemctl
Expand All @@ -170,6 +206,7 @@ say "Step 4/4 — Starting bot via systemctl…"
send_admin "🚀 Starting bot service…"
if command -v systemctl >/dev/null 2>&1; then
systemctl start "${APP_NAME}.service"
_SERVICE_STOPPED=false
say "Bot started."
else
warn "systemctl not found. Start manually: node $PROJECT_DIR/index.js"
Expand Down
67 changes: 44 additions & 23 deletions scripts/disk-protect.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,28 @@ JOURNAL_VACUUM_MB=300

# ── Logging ────────────────────────────────────────────────────────────────────

log() { echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] [disk-protect] $*"; }
log() { echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] [disk-protect] $*"; }
warn() { echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] [disk-protect] WARN: $*" >&2; }

# ── Helper: delete files found by find and log each one reliably ───────────────
# Uses process substitution + null-delimited names to handle filenames with
# spaces or special characters, and separates print/delete so logging is never
# suppressed by find implementation differences.
delete_found() {
# Usage: delete_found <find_args...>
# Example: delete_found "$LOG_DIR" -type f -mtime +7
local _count=0
while IFS= read -r -d '' f; do
if rm -f "$f"; then
log " Deleted: $f"
_count=$((_count + 1))
else
warn " Failed to delete: $f"
fi
done < <(find "$@" -print0 2>/dev/null)
log " Total deleted: $_count file(s)"
}

# ── 1. Log rotation (via logrotate if available) ───────────────────────────────
log "Step 1: Log rotation"
if command -v logrotate >/dev/null 2>&1; then
Expand All @@ -45,25 +64,27 @@ fi
# ── 2. Compress logs older than 1 day ─────────────────────────────────────────
log "Step 2: Compress logs older than 1 day"
if [[ -d "$LOG_DIR" ]]; then
find "$LOG_DIR" -type f \( -name "*.log" -o -name "*.out" \) -mtime +1 ! -name "*.gz" | while read -r f; do
gzip -f "$f" && log " Compressed: $f"
done
while IFS= read -r -d '' f; do
if gzip -f "$f" 2>/dev/null; then
log " Compressed: $f"
else
warn " Failed to compress: $f"
fi
done < <(find "$LOG_DIR" -type f \( -name "*.log" -o -name "*.out" \) -mtime +1 ! -name "*.gz" -print0 2>/dev/null)
else
log " Log directory not found: $LOG_DIR — skipping"
fi

# ── 3. Delete logs older than LOG_MAX_DAYS days ────────────────────────────────
log "Step 3: Delete logs older than ${LOG_MAX_DAYS} days"
if [[ -d "$LOG_DIR" ]]; then
find "$LOG_DIR" -type f -mtime "+${LOG_MAX_DAYS}" -delete -print | while read -r f; do
log " Deleted old log: $f"
done
delete_found "$LOG_DIR" -type f -mtime "+${LOG_MAX_DAYS}"
fi

# ── 4. journalctl vacuum ───────────────────────────────────────────────────────
log "Step 4: journalctl vacuum (keep up to ${JOURNAL_VACUUM_MB}MB)"
if command -v journalctl >/dev/null 2>&1; then
journalctl --vacuum-size="${JOURNAL_VACUUM_MB}M" 2>&1 | grep -v "^$" | while read -r line; do
journalctl --vacuum-size="${JOURNAL_VACUUM_MB}M" 2>&1 | grep -v "^$" | while IFS= read -r line; do
log " [journal] $line"
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Because the script uses set -euo pipefail, a non-zero exit status from journalctl --vacuum-size (for example, due to insufficient permissions) will cause the entire disk-protect script to abort at Step 4, skipping later cleanup and safety checks even though the journal vacuum is intended as a best-effort, non-critical operation; adding a guard to ignore failures here prevents unintended termination. [logic error]

Severity Level: Major ⚠️
- ⚠️ Disk cleanup script aborts at journal vacuum step.
- ⚠️ Later log compression and temp file cleanup skipped.
- ⚠️ Safety check for .env and runtime-state.json may skip.
Suggested change
done
done || true
Steps of Reproduction ✅
1. Install the weekly disk-protect cron job by running `/workspace/Runewager/prod-run.sh`,
which writes `0 3 * * 0 $DISK_PROTECT_SCRIPT >> ${PROJECT_DIR}/logs/disk-protect.log 2>&1`
into crontab (see `/workspace/Runewager/prod-run.sh`, disk-protect block around lines
22–32 in the shown snippet).

2. Ensure the cron (or manual) execution runs
`/workspace/Runewager/scripts/disk-protect.sh` as a non-root user that lacks permission to
vacuum the systemd journal (file `/workspace/Runewager/scripts/disk-protect.sh`, `set -euo
pipefail` at line 12 and Step 4 at lines 85–92).

3. When Step 4 executes, `journalctl --vacuum-size="${JOURNAL_VACUUM_MB}M"` (line 87)
exits with a non-zero status due to insufficient permissions; with `set -e` and `pipefail`
enabled, this non-zero pipeline exit causes the entire script to terminate immediately
because the pipeline is not guarded by `|| true` or an `if` condition.

4. Observe in `logs/disk-protect.log` that log messages for later steps (e.g., "Step 5:
Clear npm cache" at line 95, "Step 6: Clear temp files…" at line 104, safety check "Step
8" at line 124, and disk usage summary "Step 9" at line 139) are missing, confirming that
the script aborted at Step 4 instead of treating journal vacuum as best-effort.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** scripts/disk-protect.sh
**Line:** 89:89
**Comment:**
	*Logic Error: Because the script uses `set -euo pipefail`, a non-zero exit status from `journalctl --vacuum-size` (for example, due to insufficient permissions) will cause the entire disk-protect script to abort at Step 4, skipping later cleanup and safety checks even though the journal vacuum is intended as a best-effort, non-critical operation; adding a guard to ignore failures here prevents unintended termination.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

else
Expand All @@ -73,55 +94,55 @@ fi
# ── 5. Clear npm cache ─────────────────────────────────────────────────────────
log "Step 5: Clear npm cache"
if command -v npm >/dev/null 2>&1; then
npm cache clean --force 2>&1 | tail -1 | while read -r line; do log " [npm] $line"; done
npm cache clean --force 2>&1 | tail -1 | while IFS= read -r line; do log " [npm] $line"; done
log " npm cache cleared"
Comment on lines 96 to 98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: With set -euo pipefail enabled, a non-zero exit from npm cache clean --force will cause the whole script to exit at Step 5 even though clearing the npm cache is non-critical, and it will still log "npm cache cleared" on failure; wrapping this pipeline in an if and handling the failure explicitly avoids terminating the script and ensures success is logged only when the command actually succeeds. [logic error]

Severity Level: Major ⚠️
- ⚠️ Npm cache errors abort script before later cleanup steps.
- ⚠️ No dedicated error log for npm cache failures.
Suggested change
if command -v npm >/dev/null 2>&1; then
npm cache clean --force 2>&1 | tail -1 | while read -r line; do log " [npm] $line"; done
npm cache clean --force 2>&1 | tail -1 | while IFS= read -r line; do log " [npm] $line"; done
log " npm cache cleared"
if command -v npm >/devnull 2>&1; then
if npm cache clean --force 2>&1 | tail -1 | while IFS= read -r line; do log " [npm] $line"; done; then
log " npm cache cleared"
else
warn " npm cache clear failed (non-fatal)"
fi
Steps of Reproduction ✅
1. Ensure `/workspace/Runewager/scripts/disk-protect.sh` is executed with `set -euo
pipefail` enabled (line 12) either via the cron job installed in
`/workspace/Runewager/prod-run.sh` (disk-protect cron block around lines 22–32 in the
shown snippet) or by invoking the script manually.

2. Run the script on a system where `npm` is present (`command -v npm >/dev/null 2>&1` at
line 96 succeeds) but where `npm cache clean --force` will exit with a non-zero status
(for example, due to permission problems or a corrupted npm cache), causing the pipeline
at line 97 to fail.

3. When Step 5 executes, the pipeline `npm cache clean --force 2>&1 | tail -1 | while ...`
(line 97) returns a non-zero exit status; under `set -e` with `pipefail`, this unguarded
failing pipeline causes the entire `disk-protect.sh` script to terminate at Step 5.

4. Observe in `logs/disk-protect.log` that subsequent steps (Step 6 temp file cleanup at
lines 104–111, Step 7 backup pruning at lines 114–119, Step 8 safety check at lines
124–136, and Step 9 disk usage summary at lines 139–145) are missing, confirming that an
npm cache error aborted the disk-protect run instead of being treated as best-effort.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** scripts/disk-protect.sh
**Line:** 96:98
**Comment:**
	*Logic Error: With `set -euo pipefail` enabled, a non-zero exit from `npm cache clean --force` will cause the whole script to exit at Step 5 even though clearing the npm cache is non-critical, and it will still log "npm cache cleared" on failure; wrapping this pipeline in an `if` and handling the failure explicitly avoids terminating the script and ensures success is logged only when the command actually succeeds.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

else
log " npm not found — skipping"
fi

# ── 6. Clear temp files ────────────────────────────────────────────────────────
log "Step 6: Clear temp files older than ${TEMP_MAX_DAYS} days"
# Clear any .tmp files left by atomic writes inside the app
if [[ -d "$DATA_DIR" ]]; then
find "$DATA_DIR" -type f -name "*.tmp.*" -mtime "+${TEMP_MAX_DAYS}" -delete -print | while read -r f; do
log " Deleted temp file: $f"
done
delete_found "$DATA_DIR" -type f -name "*.tmp.*" -mtime "+${TEMP_MAX_DAYS}"
fi
# Clear /tmp files with our app name
find /tmp -type f -name "runewager-*" -mtime "+${TEMP_MAX_DAYS}" -delete 2>/dev/null | true
# Clear /tmp files with our app name (best-effort; ignore errors)
while IFS= read -r -d '' f; do
rm -f "$f" 2>/dev/null && log " Deleted /tmp file: $f" || true
done < <(find /tmp -type f -name "runewager-*" -mtime "+${TEMP_MAX_DAYS}" -print0 2>/dev/null)

# ── 7. Delete old runtime state snapshots older than SNAPSHOT_MAX_DAYS ─────────
log "Step 7: Delete runtime state snapshots older than ${SNAPSHOT_MAX_DAYS} days"
if [[ -d "$BACKUP_DIR" ]]; then
find "$BACKUP_DIR" -type f -name "runtime-state-*.json" -mtime "+${SNAPSHOT_MAX_DAYS}" -delete -print | while read -r f; do
log " Deleted old snapshot: $f"
done
delete_found "$BACKUP_DIR" -type f -name "runtime-state-*.json" -mtime "+${SNAPSHOT_MAX_DAYS}"
else
log " Backup dir not found: $BACKUP_DIR — skipping"
fi

# ── 8. NEVER delete critical files — safety check ─────────────────────────────
# Verifies that the cleanup did not accidentally remove key runtime files.
# Sets CRITICAL_OK=false (and emits a warning) for every file that is missing.
log "Step 8: Safety check — verifying critical files are intact"
CRITICAL_OK=true
for f in "${APP_DIR}/.env" "${DATA_DIR}/runtime-state.json"; do
if [[ -f "$f" ]]; then
log " OK: $f"
else
log " (not present, not required): $f"
warn " MISSING: $f — expected file not found after cleanup"
CRITICAL_OK=false
fi
done
if [[ "$CRITICAL_OK" != "true" ]]; then
warn " ⚠️ Critical file check failed — review above output"
warn "⚠️ One or more critical files are missing — review the output above"
fi

# ── 9. Disk usage summary ──────────────────────────────────────────────────────
log "Step 9: Disk usage summary"
df -h / 2>/dev/null | tail -1 | while read -r line; do log " / : $line"; done
df -h / 2>/dev/null | tail -1 | while IFS= read -r line; do log " / : $line"; done
if [[ -d "$APP_DIR" ]]; then
du -sh "$APP_DIR" 2>/dev/null | while read -r line; do log " App dir: $line"; done
du -sh "$APP_DIR" 2>/dev/null | while IFS= read -r line; do log " App dir: $line"; done
fi
if [[ -d "$LOG_DIR" ]]; then
du -sh "$LOG_DIR" 2>/dev/null | while read -r line; do log " Logs: $line"; done
du -sh "$LOG_DIR" 2>/dev/null | while IFS= read -r line; do log " Logs: $line"; done
fi

log "Disk protection complete."
Loading