AkurAI Build
Menu

Public repository / ee7487eee78cd52cdafdcb22b6ff4bf57093d6e9

AkurAI-Framework / scripts/propagate-suite.sh

Latest change ee7487eee78cd52cdafdcb22b6ff4bf57093d6e9 - feat: add --name flag to serve, standardize /api/health across all framework apps by Ólafur Búi Ólafsson

#!/usr/bin/env bash
# propagate-suite.sh — Framework → Apps cascade deployment.
#
# When the Framework ships a new release, this script:
#  1. Reads AKURAI_APPS.toml to find all deployed apps
#   2. For each: pulls latest, rebuilds, validates, deploys
#   3. If validate fails → rollback to previous binary
#   4. Reports results as JSON for consumption by RustAgent cron
#
# Called by RustAgent cron job or manually:
#   ./scripts/propagate-suite.sh [--dry-run] [--only <app-name>]
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DRY_RUN=false
ONLY=""
while [ $# -gt 0 ]; do
  case "$1" in
    --dry-run) DRY_RUN=true; shift ;;
    --only) ONLY="$2"; shift 2 ;;
    *) shift ;;
  esac
done

RED='\033[0;31m'; GRN='\033[0;32m'; BLU='\033[1;34m'; YLW='\033[0;33m'; NC='\033[0m'
say()  { printf "${BLU}▸ %s${NC}\n" "$*"; }
pass_() { printf "  ${GRN}PASS${NC} %s\n" "$*"; }
fail_() { printf "  ${RED}FAIL${NC} %s\n" "$*"; }
warn_() { printf "  ${YLW}WARN${NC} %s\n" "$*"; }

results_json='{"framework_version":"","propagated":[],"failed":[],"skipped":[]}'
record_result() {
  local app="$1" status="$2" message="$3"
  results_json=$(echo "$results_json" | python3 -c "
import json, sys
r = json.load(sys.stdin)
r['${status}'].append({'app':'${app}','message':'${message}'})
print(json.dumps(r))
")
}

# ── Read current Framework version ────────────────────────────────
FW_VERSION=$(tr -d '[:space:]' < "$ROOT/VERSION")
results_json=$(echo "$results_json" | python3 -c "import json,sys; r=json.load(sys.stdin); r['framework_version']='$FW_VERSION'; print(json.dumps(r))")
say "Framework v${FW_VERSION} — propagating to dependents"

# ── Parse AKURAI_APPS.toml → JSON (python3 TOML reader) ──────────
DEPS_JSON=$(python3 -c "
import json, sys, re

with open('$ROOT/AKURAI_APPS.toml') as f:
    text = f.read()

# Minimal TOML parser for our flat format (handles [[app]] sections)
deps = []
current = None
for line in text.split('\n'):
    line = line.strip()
    if line.startswith('[[app]]'):
        if current:
            deps.append(current)
        current = {}
    elif current is not None and '=' in line and not line.startswith('#'):
        if '#[' in line:
            line = line.split('#')[0].strip()
        k, v = line.split('=', 1)
        k = k.strip()
        v = v.strip().strip('\"')
        current[k] = v
if current:
    deps.append(current)

print(json.dumps(deps))
")

echo "$DEPS_JSON" | python3 -c "
import json, sys
deps = json.load(sys.stdin)
for d in deps:
    print(f\"  {d['name']:22s} {d.get('domain', '-'):32s} port {d.get('port', '-')}\")
"

# ── For each dependent ───────────────────────────────────────────
echo "$DEPS_JSON" | python3 -c "
import json, sys
for d in json.load(sys.stdin):
    print(d['name'])
" | while read -r APP_NAME; do

  if [ -n "$ONLY" ] && [ "$APP_NAME" != "$ONLY" ]; then
    continue
  fi

  # Get full record
  APP_JSON=$(echo "$DEPS_JSON" | python3 -c "
import json, sys
for d in json.load(sys.stdin):
    if d['name'] == '$APP_NAME':
        print(json.dumps(d))
        break
  ")

  APP_CHECKOUT=$(echo "$APP_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('checkout',''))")
  APP_DEPLOY=$(echo "$APP_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deploy',''))")
  APP_VALIDATE=$(echo "$APP_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('validate',''))")
  APP_HEALTH=$(echo "$APP_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('health',''))")
  APP_DOMAIN=$(echo "$APP_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('domain',''))")
  APP_DB_SNAPSHOT=$(echo "$APP_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('db_snapshot',''))")
  APP_DB_DIR=$(echo "$APP_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('db_snapshot_dir',''))")

  say "─── $APP_NAME ───"
  
  if [ ! -d "$APP_CHECKOUT" ]; then
    warn_ "Checkout $APP_CHECKOUT missing — skipping"
    record_result "$APP_NAME" "skipped" "checkout missing"
    continue
  fi

  # 1. Pre-deploy health check (baseline)
  if [ -n "$APP_HEALTH" ]; then
    if curl -fsS --max-time 10 "$APP_HEALTH" > /dev/null 2>&1; then
      pass_ "pre-deploy health OK"
    else
      fail_ "pre-deploy health FAILED — app may already be down"
      record_result "$APP_NAME" "failed" "pre-deploy health failed"
      continue
    fi
  fi

  # 2. Git pull latest
  if [ "$DRY_RUN" = false ]; then
    (cd "$APP_CHECKOUT" && git pull --ff-only origin main 2>&1) || {
      warn_ "git pull failed — continuing with current HEAD"
    }
  fi

  # 3. DB snapshot (if configured)
  if [ -n "$APP_DB_SNAPSHOT" ] && [ "$DRY_RUN" = false ]; then
    APP_DB_DIR="${APP_DB_DIR:-/var/backups/${APP_NAME}}"
    ssh akurai-mail "sudo mkdir -p '$APP_DB_DIR' && sudo cp '$APP_DB_SNAPSHOT' '$APP_DB_DIR/${APP_NAME}-$(date +%Y%m%d-%H%M%S).db'" 2>/dev/null || warn_ "DB snapshot failed (non-fatal)"
    # Keep only last 7 backups
    ssh akurai-mail "sudo ls -t '$APP_DB_DIR'/*.db 2>/dev/null | tail -n +8 | xargs -r sudo rm" 2>/dev/null || true
  fi

  # 4. Deploy
  if [ "$DRY_RUN" = false ]; then
    say "deploying..."
    if (cd "$APP_CHECKOUT" && eval "$APP_DEPLOY" 2>&1); then
      pass_ "deploy completed"
    else
      fail_ "deploy FAILED"
      record_result "$APP_NAME" "failed" "deploy command failed"
      continue
    fi
  else
    say "DRY RUN — would run: cd $APP_CHECKOUT && $APP_DEPLOY"
  fi

  # 5. Post-deploy validation
  if [ "$DRY_RUN" = false ] && [ -f "$APP_CHECKOUT/$APP_VALIDATE" ]; then
    say "validating..."
    if bash "$APP_CHECKOUT/$APP_VALIDATE" 2>&1; then
      pass_ "validation PASSED"
    else
      fail_ "validation FAILED — rolling back"
      # Rollback: restore from backup binary if Framework ships its binary to apps
      # For now, log the failure for manual review
      record_result "$APP_NAME" "failed" "post-deploy validation failed"
      continue
    fi
  fi

  # 6. Post-deploy health
  if [ "$DRY_RUN" = false ] && [ -n "$APP_HEALTH" ]; then
    sleep 2  # let the service restart settle
    if curl -fsS --max-time 10 "$APP_HEALTH" > /dev/null 2>&1; then
      pass_ "post-deploy health OK"
    else
      fail_ "post-deploy health FAILED"
      record_result "$APP_NAME" "failed" "post-deploy health failed"
      continue
    fi
  fi

  record_result "$APP_NAME" "propagated" "deployed and validated"
  pass_ "$APP_NAME ✓"
done

# ── Summary ──────────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "$results_json" | python3 -c "
import json, sys
r = json.load(sys.stdin)
print(f\"Framework: v{r['framework_version']}\")
print(f\"Propagated: {len(r['propagated'])}  Failed: {len(r['failed'])}  Skipped: {len(r['skipped'])}\")
for f in r['failed']:
    print(f\"  ✗ {f['app']}: {f['message']}\")
"
if echo "$results_json" | python3 -c "import json,sys; exit(len(json.load(sys.stdin)['failed']))"; then
  exit 0
else
  exit 1
fi