#!/bin/sh # Praxis — Orchestrator: deploy praxis to a Proxmox LXC container. # # Adapted from coreci/scripts/proxmox/lxc-deploy.sh. # Sequence: stage snippet → clone template → configure CT → start → # health-check → rollback on failure. # # Required env (see .env.example + ~/coreci/.ciagent/.env.secrets): # PROXMOX_API_URL — https://proxmox:8006/api2/json # PROXMOX_API_TOKEN — USER@REALM!TOKENID=SECRET # PROXMOX_NODE — target node name # PROXMOX_STORAGE — storage holding the template # PROXMOX_TEMPLATE_VOLID — local:vztmpl/debian-12-template.tar.zst # GITEA_TOKEN — bearer token for the private Gitea repo # (baked into the firstboot snippet by stage-snippet.sh) # # Optional env: # PROXMOX_LXC_VMID — target CT VMID (default: auto-allocate via pve_nextid) # PRAXIS_VERSION — git ref to deploy (default: main) # PRAXIS_PORT — server HTTP port (default: 8789) # PRAXIS_HEALTH_URL — override health-check URL # PROXMOX_MEMORY_MB — CT memory limit (default: 4096) # PROXMOX_TLS_SKIP_VERIFY— accept self-signed certs (default: false) # DEEPGRAM_API_KEY — voice-service key (optional, may be empty) # CARTESIA_API_KEY — voice-service key (optional, may be empty) # OLLAMA_API_KEY — voice-service key (optional, may be empty) # # Flags: # --recreate — rollback.sh (stop + destroy) then full redeploy # --reconfigure — re-PUT lxc-config.sh + restart (no clone) # # Exit: 0 on successful deploy, 1 on failure (with rollback attempted) set -eu SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJ_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" # shellcheck source=api.sh disable=SC1091 . "${SCRIPT_DIR}/api.sh" # shellcheck source=ct-exists.sh disable=SC1091 . "${SCRIPT_DIR}/ct-exists.sh" # shellcheck source=timing.sh disable=SC1091 . "${SCRIPT_DIR}/timing.sh" # ── Source secrets (D-026, MH-23) ────────────────────────────────── # Proxmox secrets come from ~/coreci/.ciagent/.env.secrets (same cluster, # same operator). Praxis secrets (GITEA_TOKEN, DEEPGRAM_API_KEY) come from # praxis's own .ciagent/.env.secrets. Missing files emit a warning (the # vars may already be in the environment from the CI runner); pve_env # below fails fast if required vars are still unset. CORECI_SECRETS="${HOME}/coreci/.ciagent/.env.secrets" PRAXIS_SECRETS="${PROJ_ROOT}/.ciagent/.env.secrets" if [ -f "$CORECI_SECRETS" ]; then # shellcheck source=/dev/null disable=SC1091 . "$CORECI_SECRETS" else echo "deploy: WARNING — ${CORECI_SECRETS} not found (PROXMOX_* vars must be in env)" >&2 fi if [ -f "$PRAXIS_SECRETS" ]; then # shellcheck source=/dev/null disable=SC1091 . "$PRAXIS_SECRETS" else echo "deploy: WARNING — ${PRAXIS_SECRETS} not found (GITEA_TOKEN/DEEPGRAM_API_KEY must be in env)" >&2 fi pve_env PROXMOX_API_URL PROXMOX_API_TOKEN PROXMOX_NODE \ PROXMOX_STORAGE PROXMOX_TEMPLATE_VOLID GITEA_TOKEN # ── Flag parsing ─────────────────────────────────────────────────── recreate=0 reconfigure=0 for arg in "$@"; do case "$arg" in --recreate) recreate=1 ;; --reconfigure) reconfigure=1 ;; *) echo "deploy: unknown argument: $arg" >&2; exit 2 ;; esac done # Step 0: stage the first-boot hookscript to Proxmox snippet storage. # G-101 FIX: stage-snippet.sh bakes GITEA_TOKEN into the snippet. hookscript_volid="${PROXMOX_STORAGE:-local}:snippets/praxis-firstboot.sh" existing=$(pve_get "/nodes/${PROXMOX_NODE}/storage/${PROXMOX_STORAGE:-local}/content" 2>/dev/null | jq -r --arg v "$hookscript_volid" '.[]? | select(.volid==$v) | .volid' 2>/dev/null || true) if [ -n "$existing" ]; then echo "deploy: hookscript snippet ${hookscript_volid} already staged — skipping upload" >&2 else "${SCRIPT_DIR}/stage-snippet.sh" fi # Resolve target VMID (D-027: auto-allocate by default). vmid="${PROXMOX_LXC_VMID:-auto}" if [ "$vmid" = "auto" ]; then vmid=$(pve_nextid) echo "deploy: auto-allocated VMID ${vmid}" >&2 else echo "deploy: using configured VMID ${vmid}" >&2 fi # Trap: rollback on any failure (mirrors coreci pattern). deploy_failed=0 skip_rollback=0 trap 'deploy_failed=1' INT TERM cleanup() { rc=$? if [ "$skip_rollback" -ne 1 ] && { [ "$deploy_failed" -ne 0 ] || [ "$rc" -ne 0 ]; }; then echo "deploy: FAILED (rc=${rc}) — rolling back VMID ${vmid}" >&2 "${SCRIPT_DIR}/rollback.sh" "$vmid" 2>&1 || true fi } trap cleanup EXIT # ── Idempotency: detect existing CT before clone ────────────────── if ct_exists "$vmid"; then echo "deploy: VMID ${vmid} already exists — checking health" >&2 ct_healthy=0 if ct_running "$vmid"; then if PRAXIS_HEALTH_TIMEOUT="${IDEMPOTENCY_HEALTH_TIMEOUT:-30}" \ "${SCRIPT_DIR}/health-check.sh" "$vmid" 2>/dev/null; then ct_healthy=1 fi fi if [ "$ct_healthy" -eq 1 ]; then echo "deploy: VMID ${vmid} already running + healthy — skipping clone/config/start (idempotent re-deploy)" >&2 skip_provision=1 elif [ "$reconfigure" -eq 1 ]; then echo "deploy: VMID ${vmid} exists but unhealthy — --reconfigure: re-PUT config + restart" >&2 skip_rollback=1 timing_start reconfigure "${SCRIPT_DIR}/lxc-config.sh" "$vmid" "${SCRIPT_DIR}/lxc-start.sh" "$vmid" timing_end reconfigure timing_start health "${SCRIPT_DIR}/health-check.sh" "$vmid" timing_end health skip_provision=1 elif [ "$recreate" -eq 1 ]; then echo "deploy: VMID ${vmid} exists but unhealthy — --recreate: rollback + redeploy" >&2 "${SCRIPT_DIR}/rollback.sh" "$vmid" skip_provision=0 else echo "deploy: ERROR — VMID ${vmid} exists but is unhealthy." >&2 echo "deploy: Use --recreate to rollback + redeploy, or --reconfigure to update config + restart." >&2 echo "deploy: No action taken (the existing CT was left intact for inspection)." >&2 skip_rollback=1 exit 1 fi else skip_provision=0 fi if [ "${skip_provision:-0}" -eq 0 ]; then # Step 1: Clone the template timing_start clone "${SCRIPT_DIR}/lxc-clone.sh" "$vmid" timing_end clone # Step 2: Configure the CT timing_start config "${SCRIPT_DIR}/lxc-config.sh" "$vmid" timing_end config # Step 3: Start the CT timing_start start "${SCRIPT_DIR}/lxc-start.sh" "$vmid" timing_end start # Step 4: Health-check (G-104: 600s timeout for Docker build) timing_start health "${SCRIPT_DIR}/health-check.sh" "$vmid" timing_end health fi deploy_failed=0 echo "deploy: praxis deployed successfully to VMID ${vmid}" >&2 printf 'VMID=%s\n' "$vmid"