#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" AUDIT_LOG="$ROOT_DIR/logs/audit.log" PROGRESS_FILE="$ROOT_DIR/.deploy-state/progress.md" now() { date +"%Y-%m-%dT%H:%M:%S%z" } audit() { local action="$1" local status="$2" local message="${3:-}" mkdir -p "$(dirname "$AUDIT_LOG")" printf '{"time":"%s","action":"%s","status":"%s","message":"%s"}\n' \ "$(now)" "$action" "$status" "$message" >> "$AUDIT_LOG" } progress() { local step="$1" local status="$2" local notes="${3:-}" mkdir -p "$(dirname "$PROGRESS_FILE")" if [ ! -f "$PROGRESS_FILE" ]; then printf '# Deployment Progress\n\n| Time | Step | Status | Notes |\n|------|------|--------|-------|\n' > "$PROGRESS_FILE" fi printf '| %s | %s | %s | %s |\n' "$(now)" "$step" "$status" "$notes" >> "$PROGRESS_FILE" } fail_json() { local code="$1" local message="$2" local step="${3:-unknown}" printf '{"code":"%s","message":"%s","step":"%s","recoverable":true}\n' "$code" "$message" "$step" >&2 audit "$step" "FAILED" "$message" progress "$step" "FAILED" "$message" exit 1 } load_env() { set -a [ -f "$ROOT_DIR/.env" ] && . "$ROOT_DIR/.env" [ -f "$ROOT_DIR/config/infra.env" ] && . "$ROOT_DIR/config/infra.env" [ -f "$ROOT_DIR/config/xuqm.env" ] && . "$ROOT_DIR/config/xuqm.env" [ -f "$ROOT_DIR/config/secrets.env" ] && . "$ROOT_DIR/config/secrets.env" set +a } require_cmd() { command -v "$1" >/dev/null 2>&1 || fail_json "XUQM_PRIVATE_4001" "missing required command: $1" "preflight" } random_secret() { if command -v openssl >/dev/null 2>&1; then openssl rand -base64 32 | tr -d '\n' else printf '%s_%s' "$(date +%s)" "$RANDOM" fi } ensure_secret_file() { if [ ! -f "$ROOT_DIR/config/secrets.env" ]; then cp "$ROOT_DIR/config/secrets.env.example" "$ROOT_DIR/config/secrets.env" chmod 600 "$ROOT_DIR/config/secrets.env" || true fi } set_env_value() { local file="$1" local key="$2" local value="$3" mkdir -p "$(dirname "$file")" touch "$file" if grep -q "^${key}=" "$file"; then sed -i.bak "s|^${key}=.*|${key}=${value}|" "$file" rm -f "${file}.bak" else printf '%s=%s\n' "$key" "$value" >> "$file" fi } ensure_env_value() { local file="$1" local key="$2" local current="${3:-}" local generated="$4" if [ -z "$current" ] || [ "$current" = "change-me" ]; then set_env_value "$file" "$key" "$generated" printf '%s' "$generated" else printf '%s' "$current" fi } profile_contains() { local profiles="$1" local target="$2" case ",${profiles}," in *,"${target}",*) return 0 ;; *) return 1 ;; esac } add_profile() { local profiles="${1:-base}" local target="$2" if profile_contains "$profiles" "$target"; then printf '%s' "$profiles" else printf '%s,%s' "$profiles" "$target" fi } remove_profile() { local profiles="$1" local target="$2" local result="" local item IFS=',' read -r -a items <<< "$profiles" for item in "${items[@]}"; do [ "$item" = "$target" ] && continue [ -z "$item" ] && continue if [ -z "$result" ]; then result="$item" else result="$result,$item" fi done printf '%s' "${result:-base}" } compose() { docker compose --env-file "$ROOT_DIR/.env" -f "$ROOT_DIR/docker-compose.yml" -f "$ROOT_DIR/docker-compose.infra.yml" "$@" }