← back to Butlr
scripts/predeploy-guard.sh
51 lines
#!/usr/bin/env bash
# Pre-deploy guard for Butlr: abort if any call is currently active.
# A pm2 reload kills the WSS Media Stream lane, which silently drops
# the PCM audio capture and any in-flight ffmpeg→MP3 finalize. Discovered
# 2026-05-13 when a mid-call deploy lost 5 recordings (3 calls completed
# but had no audio archived).
#
# Usage (called from .deploy.conf):
# PREDEPLOY_HOOK="bash scripts/predeploy-guard.sh"
#
# Exits 0 if safe. Exits 2 with explanation if at least one call is
# in dialing / on_hold / connected. Override with FORCE_DEPLOY=1 if
# you've decided the deploy is worth dropping any active recordings.
set -euo pipefail
DEPLOY_HOST="${DEPLOY_HOST:-45.61.58.125}"
DEPLOY_PATH="${DEPLOY_PATH:-/root/public-projects/butlr}"
if [[ "${FORCE_DEPLOY:-}" == "1" ]]; then
echo " ⚠️ FORCE_DEPLOY=1 — skipping active-call guard"
exit 0
fi
# Read prod calls.json over ssh. Statuses considered active:
# dialing — call is being placed
# on_hold — Twilio Stream is open, capturing audio
# connected — Stream + AI loop running, audio still flowing
active=$(ssh -o ConnectTimeout=5 "root@${DEPLOY_HOST}" \
"node -e 'const d=require(\"${DEPLOY_PATH}/lib/data\"); const a=d.listCallsUnscoped(); const live=a.filter(c=>[\"dialing\",\"on_hold\",\"connected\"].includes(c.status)); console.log(JSON.stringify(live.map(c=>({id:c.id,status:c.status,biz:c.business_name}))))'" \
2>/dev/null || echo "[]")
count=$(node -e "process.stdout.write(String(JSON.parse(process.argv[1]).length))" "$active")
if [[ "$count" == "0" ]]; then
echo " ✓ no active calls — safe to deploy"
exit 0
fi
echo
echo " ✗ ABORTING DEPLOY — $count active call(s) on prod:"
node -e "
const a = JSON.parse(process.argv[1]);
for (const c of a) console.log(' ' + c.id.padEnd(10) + ' | ' + c.status.padEnd(10) + ' | ' + (c.biz || ''));
" "$active"
echo
echo " Wait for calls to complete (pm2 logs butlr | grep status), or override:"
echo " FORCE_DEPLOY=1 bash ~/Projects/_shared/scripts/deploy.sh"
echo
exit 2