← back to Dw Marketing Reels

scripts/cron-run.sh

82 lines

#!/bin/zsh
# Nightly: refresh feed → render reel locally ($0) → auto-post to social (fail-safe:
# simulates/queues until creds land) → push reels + data to the Kamatera console.
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
export HYPERFRAMES_SKIP_SKILLS=1
cd "$(dirname "$0")/.."
[ -f .env ] && set -a && source .env && set +a

node scripts/fetch-new-arrivals.mjs && node scripts/build-reel.mjs || exit 1

# LEAK GATE (fail-closed, 2026-07-27): a private-label / upstream name in the staged
# manifests must NEVER reach a public post or the console. If scan-leaks finds a hit it
# exits non-zero and we abort BEFORE publish + push. Turns the old one-shot manual "0
# leaks" check into a standing gate.
node scripts/scan-leaks.mjs || { echo "LEAK GATE FAILED — aborting publish+push"; exit 1; }

# Meta pulls `video_url` from the public marketing host while processing the container. The old
# order called Norma BEFORE rsync, so every newly-rendered filename was still a public 404 and Meta
# returned 2207076. When live IG is armed, fail closed unless the media is public before publish.
PUSH_HOST="${PUSH_HOST:-root@45.61.58.125}"
PUSH_PATH="${PUSH_PATH:-/root/public-projects/dw-marketing-reels}"
if [ "${SOCIAL_LIVE_ARMED:-0}" = "1" ]; then
  if ssh -o BatchMode=yes -o ConnectTimeout=6 "$PUSH_HOST" "test -d $PUSH_PATH" 2>/dev/null; then
    timeout 120 rsync -az reels/ "$PUSH_HOST:$PUSH_PATH/reels/" \
      || { echo "PRE-PUBLISH MEDIA PUSH FAILED — refusing live post with a 404 URL"; exit 1; }
    echo "pre-published reel media -> $PUSH_HOST:$PUSH_PATH/reels/"
  else
    echo "PRE-PUBLISH HOST UNREACHABLE — refusing live post with a 404 URL"
    exit 1
  fi
fi

# auto-post (Steve: auto-post on the nightly schedule). Runs on Mac3 where Norma/tiktok live.
SOCIAL_AUTOPOST="${SOCIAL_AUTOPOST:-1}" node scripts/publish-social.mjs

# TK-10395 publish-result gate — runs IMMEDIATELY after publish, BEFORE the push, so a hanging or
# failing Kamatera push (e.g. host at 1% disk) can never suppress the alert (that exact ordering bug
# swallowed the 2026-08-11 07:10 failure — the log died in the push and the gate never ran). The
# publisher exits 0 even when the IG post fails (how 2207076 went silently green); this detects an
# ARMED post that did NOT land and PUSHES an alert (Steve-approved "PUSH OPEN"): CNCP card + George
# email. We record the failure and still do the push, then exit non-zero at the very end so launchd
# sees the failure without losing the console push.
PUBLISH_FAILED=0
# TK-10929 (2026-09-16): capture what the checker ACTUALLY observed and report THAT.
# The previous MSG hardcoded a diagnosis ("a 2207076 ... verify IG content-publish
# permission / re-auth") and emitted it for EVERY failure regardless of the real error.
# For 3 weeks the true error was "fetch failed" — a TRANSPORT failure because the Norma
# instagram-agent on :9810 was dead — yet every CNCP card + George email asserted a Meta
# credential problem, so TK-10929 sat blocked on "NEEDS STEVE: IG re-auth" while the
# tokens were healthy the whole time. An alert must never assert a cause it did not measure.
CHECK_OUT="$(node scripts/check-publish-status.mjs 2>&1)"; CHECK_RC=$?
echo "$CHECK_OUT"
if [ "$CHECK_RC" -ne 0 ]; then
  PUBLISH_FAILED=1
  REAL="$(printf '%s' "$CHECK_OUT" | tr '\n\r\t' '   ' | tr -d '"\\' | sed 's/  */ /g')"
  MSG="TK-10395: nightly IG reel publish did NOT land (armed) on the $(date +%H:%M) run — OBSERVED: ${REAL} | TRIAGE: 'fetch failed' = TRANSPORT, i.e. the Norma instagram-agent :9810 is unreachable (check: pm2 describe norma-instagram; lsof -nP -iTCP:9810 -sTCP:LISTEN) — that is NOT a Meta credential problem. A genuine Meta failure (e.g. 2207076) arrives as a parsed error code, and token health is separately covered by meta-token-canary. See reels.json publish.instagram + reels-cron.log."
  echo "IG PUBLISH CHECK FAILED — $MSG"
  curl -sS --max-time 10 "${CNCP_URL:-http://localhost:3333}/api/parking-lot" \
    -H 'Content-Type: application/json' \
    -d "{\"url\":\"http://marketing.designerwallcoverings.com/\",\"note\":\"$MSG\"}" \
    >/dev/null 2>&1 && echo "CNCP card posted" || echo "CNCP post failed (non-fatal)"
  if [ -f "$HOME/.claude/skills/_shared/george-send.sh" ]; then
    . "$HOME/.claude/skills/_shared/george-send.sh"
    george_send steve-office "${REELS_ALERT_TO:-steve@designerwallcoverings.com}" \
      "⚠ DW nightly reel IG publish FAILED — $(date +%Y-%m-%d)" \
      "<p>$MSG</p>" >/dev/null 2>&1 && echo "George alert sent" || echo "George send failed (non-fatal)"
  fi
fi

# Push to the Kamatera console. Bounded so a full/hung host can't wedge the whole job (and can't
# suppress the gate above, which already ran). rsync gets its own timeout + non-fatal guard.
if ssh -o BatchMode=yes -o ConnectTimeout=6 "$PUSH_HOST" "test -d $PUSH_PATH" 2>/dev/null; then
  timeout 120 rsync -az reels/ "$PUSH_HOST:$PUSH_PATH/reels/" || echo "reels rsync failed/timed out (non-fatal)"
  timeout 120 rsync -az data/  "$PUSH_HOST:$PUSH_PATH/data/"  || echo "data rsync failed/timed out (non-fatal)"
  echo "pushed reels+data -> $PUSH_HOST:$PUSH_PATH"
else
  echo "push skipped — $PUSH_HOST:$PUSH_PATH unreachable (non-fatal)"
fi

# Report the IG-publish failure to launchd LAST, after the push has had its chance.
[ "$PUBLISH_FAILED" = 0 ] || { echo "nightly completed with IG PUBLISH FAILURE (see alert above)"; exit 1; }