Menu
Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f
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