Files
OSIT-AE-App-Native-Electron/deploy/deploy.sh
Scott Idem 3feaf1bbc3 feat(deploy): add deploy script and per-device config files
deploy/deploy.sh — automated deploy: arch detection, scp .app, write
seed.json, verify. Supports single laptop, list, or "all"; --seed-only
flag skips .app copy for key-rotation runs.

deploy/devices.conf — all 19 laptops (num / IP / event_device_id).
deploy/event.env.example — template for gitignored event.env (API key).

README updated: deploy/ table, script usage, manual steps moved to
reference section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 15:02:15 -04:00

202 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# deploy.sh — Deploy Aether Native Launcher to onsite Mac laptops
#
# USAGE:
# ./deploy.sh <num> [num ...] Deploy to one or more laptops (e.g. 03 04 05)
# ./deploy.sh all Deploy to all laptops in devices.conf
# ./deploy.sh --seed-only <num> Update seed.json only — skip .app copy
# ./deploy.sh --seed-only all
#
# REQUIRES:
# event.env — copy from event.env.example and fill in AETHER_API_KEY
# builds/ — run "npm run package:mac" first if .app is missing
#
# SSH keys must already be installed on each target (run ssh-copy-id once per laptop).
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEVICES_FILE="$SCRIPT_DIR/devices.conf"
EVENT_ENV="$SCRIPT_DIR/event.env"
SSH_USER="speaker ready"
BUILD_DIR="$SCRIPT_DIR/../builds"
# ── Argument parsing ──────────────────────────────────────────────────────────
SEED_ONLY=false
TARGETS=()
usage() {
grep '^#' "$0" | grep -v '^#!/' | sed 's/^# \{0,1\}//'
exit 1
}
if [[ $# -eq 0 ]]; then usage; fi
while [[ $# -gt 0 ]]; do
case "$1" in
--seed-only) SEED_ONLY=true ;;
--help|-h) usage ;;
all) TARGETS+=("all") ;;
*) TARGETS+=("$1") ;;
esac
shift
done
if [[ ${#TARGETS[@]} -eq 0 ]]; then
echo "ERROR: No targets specified."
usage
fi
# ── Load event config ─────────────────────────────────────────────────────────
if [[ ! -f "$EVENT_ENV" ]]; then
echo "ERROR: $EVENT_ENV not found."
echo " Copy event.env.example → event.env and fill in AETHER_API_KEY."
exit 1
fi
# shellcheck source=/dev/null
source "$EVENT_ENV"
: "${AETHER_API_KEY:?event.env must set AETHER_API_KEY}"
: "${PRIMARY_API_BASE_URL:?event.env must set PRIMARY_API_BASE_URL}"
: "${BACKUP_API_BASE_URL:?event.env must set BACKUP_API_BASE_URL}"
# Render onsite URL as JSON null or quoted string
if [[ -n "${ONSITE_API_BASE_URL:-}" ]]; then
ONSITE_JSON="\"$ONSITE_API_BASE_URL\""
else
ONSITE_JSON="null"
fi
# ── Device list helpers ───────────────────────────────────────────────────────
# Returns "ip device_id" for a laptop number, or empty if not found
lookup_device() {
local num="$1"
grep -E "^[[:space:]]*${num}[[:space:]]" "$DEVICES_FILE" \
| grep -v '^[[:space:]]*#' \
| awk '{print $2, $3}' \
| head -1
}
# Returns all laptop numbers from devices.conf (first column, non-comment lines)
all_device_nums() {
grep -v '^[[:space:]]*#' "$DEVICES_FILE" \
| grep -v '^[[:space:]]*$' \
| awk '{print $1}'
}
# ── Deploy one laptop ─────────────────────────────────────────────────────────
deploy_laptop() {
local num="$1"
local info
info=$(lookup_device "$num")
if [[ -z "$info" ]]; then
echo " ERROR: Laptop $num not found in devices.conf"
return 1
fi
local ip device_id
ip=$(echo "$info" | awk '{print $1}')
device_id=$(echo "$info" | awk '{print $2}')
echo ""
echo "══════════════════════════════════════════════"
echo " Laptop $num · $ip · $device_id"
echo "══════════════════════════════════════════════"
# ── Copy .app ──────────────────────────────────────────────────────────
if [[ "$SEED_ONLY" != "true" ]]; then
echo " Detecting architecture..."
local arch
arch=$(ssh "$SSH_USER@$ip" "uname -m" 2>/dev/null) || {
echo " ERROR: SSH failed for $ip — is the laptop on the network?"
return 1
}
local bundle
case "$arch" in
x86_64) bundle="$BUILD_DIR/aether_launcher-darwin-x64/aether_launcher.app" ;;
arm64) bundle="$BUILD_DIR/aether_launcher-darwin-arm64/aether_launcher.app" ;;
*)
echo " ERROR: Unknown arch '$arch' on $ip"
return 1
;;
esac
if [[ ! -d "$bundle" ]]; then
echo " ERROR: Build not found: $bundle"
echo " Run: npm run package:mac"
return 1
fi
echo " Arch: $arch → copying $(basename "$bundle")..."
scp -r "$bundle" "$SSH_USER@$ip:/Applications/aether_launcher.app" || {
echo " ERROR: scp failed."
return 1
}
echo " .app copied."
else
echo " (--seed-only: skipping .app copy)"
fi
# ── Write seed.json ────────────────────────────────────────────────────
echo " Writing seed.json..."
ssh "$SSH_USER@$ip" "cat > ~/seed.json" <<EOF
{
"event_device_id": "$device_id",
"aether_api_key": "$AETHER_API_KEY",
"primary_api_base_url": "$PRIMARY_API_BASE_URL",
"backup_api_base_url": "$BACKUP_API_BASE_URL",
"onsite_api_base_url": $ONSITE_JSON
}
EOF
# ── Verify ─────────────────────────────────────────────────────────────
echo " Verifying..."
ssh "$SSH_USER@$ip" "cat ~/seed.json"
if [[ "$SEED_ONLY" != "true" ]]; then
ssh "$SSH_USER@$ip" \
"test -d /Applications/aether_launcher.app && echo ' ✓ .app present' || echo ' ✗ .app NOT found'"
fi
echo " ✓ Laptop $num done."
return 0
}
# ── Expand "all" target ───────────────────────────────────────────────────────
EXPANDED_TARGETS=()
for t in "${TARGETS[@]}"; do
if [[ "$t" == "all" ]]; then
while IFS= read -r num; do
EXPANDED_TARGETS+=("$num")
done < <(all_device_nums)
else
EXPANDED_TARGETS+=("$t")
fi
done
# ── Run deploys ───────────────────────────────────────────────────────────────
FAILED=()
for num in "${EXPANDED_TARGETS[@]}"; do
if ! deploy_laptop "$num"; then
FAILED+=("$num")
fi
done
echo ""
echo "══════════════════════════════════════════════"
if [[ ${#FAILED[@]} -eq 0 ]]; then
echo " All done. ✓"
else
echo " FAILED: ${FAILED[*]}"
echo " Re-run with just those numbers to retry."
exit 1
fi