#!/bin/bash # coolify-deploy.sh — Build, push, and deploy to Coolify # Usage: coolify-deploy.sh [project-dir] [app-uuid] # # Examples: # coolify-deploy.sh # interactive, lists known apps # coolify-deploy.sh ~/projects/coinex-dashboard # auto-detect app from .coolify # coolify-deploy.sh ~/projects/coinex-dashboard fcs04o8w... # explicit app UUID # coolify-deploy.sh --status fcs04o8w... # check latest deploy status # coolify-deploy.sh --skip-build ~/projects/coinex-dashboard # push + deploy only set -euo pipefail COOLIFY_HOST="${COOLIFY_HOST:-192.168.86.44}" COOLIFY_PORT="${COOLIFY_PORT:-8000}" COOLIFY_URL="http://${COOLIFY_HOST}:${COOLIFY_PORT}/api/v1" COOLIFY_TOKEN="${COOLIFY_TOKEN:-}" CREDS_FILE="$HOME/.openclaw/workspace/.credentials/coolify.env" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' log() { echo -e "${BLUE}[deploy]${NC} $*"; } ok() { echo -e "${GREEN}[ ok ]${NC} $*"; } warn() { echo -e "${YELLOW}[ warn ]${NC} $*"; } fail() { echo -e "${RED}[FAILED]${NC} $*"; exit 1; } # Load token load_token() { if [ -z "$COOLIFY_TOKEN" ]; then if [ -f "$CREDS_FILE" ]; then source "$CREDS_FILE" COOLIFY_TOKEN="${COOLIFY_API_TOKEN:-${COOLIFY_TOKEN:-}}" fi fi [ -z "$COOLIFY_TOKEN" ] && fail "No COOLIFY_TOKEN. Set env var or create $CREDS_FILE with COOLIFY_API_TOKEN=..." } api() { local method="$1" path="$2" shift 2 curl -sf -X "$method" \ -H "Authorization: Bearer $COOLIFY_TOKEN" \ -H "Content-Type: application/json" \ "$COOLIFY_URL$path" "$@" 2>/dev/null } # Save/load app UUID mapping save_app_config() { local dir="$1" uuid="$2" echo "$uuid" > "$dir/.coolify-app" echo "$uuid" } load_app_config() { local dir="$1" if [ -f "$dir/.coolify-app" ]; then cat "$dir/.coolify-app" fi } # --- Commands --- cmd_status() { local app_uuid="$1" load_token log "Checking deployments for app $app_uuid..." local deploys deploys=$(api GET "/applications/$app_uuid/deployments" 2>/dev/null) || fail "Could not fetch deployments" echo "$deploys" | python3 -c " import json, sys data = json.load(sys.stdin) deploys = data if isinstance(data, list) else data.get('data', data.get('deployments', [])) for d in deploys[:5]: status = d.get('status', '?') commit = d.get('commit', '?')[:8] created = d.get('created_at', '?')[:19] uuid = d.get('deployment_uuid', '?')[:12] icon = '✅' if status == 'finished' else '❌' if status == 'failed' else '⏳' if status == 'in_progress' else '❓' print(f'{icon} {created} {status:<12} {commit} {uuid}') " 2>/dev/null || echo "$deploys" | python3 -m json.tool 2>/dev/null || echo "$deploys" } cmd_deploy() { local project_dir="$1" local app_uuid="$2" local skip_build="${3:-false}" load_token cd "$project_dir" local project_name project_name=$(basename "$project_dir") # Step 1: Build if [ "$skip_build" = "false" ]; then log "Building $project_name..." # Clean previous build rm -rf .next 2>/dev/null || true if npm run build 2>&1 | tee /tmp/coolify-build.log | tail -5; then ok "Build succeeded" else fail "Build failed. Full log: /tmp/coolify-build.log" fi else warn "Skipping build (--skip-build)" fi # Step 2: Git commit & push log "Committing and pushing..." if git diff --quiet && git diff --cached --quiet; then warn "No changes to commit — pushing existing commits" else git add -A local msg msg=$(git log --oneline -1 2>/dev/null | cut -d' ' -f2-) git commit -m "deploy: ${msg:-update}" --quiet 2>/dev/null || true fi if git push --quiet 2>&1; then ok "Pushed to $(git remote get-url origin 2>/dev/null | sed 's/.*@//' | sed 's/\.git$//')" else fail "Git push failed" fi # Step 3: Trigger Coolify deploy log "Triggering Coolify deployment..." local result result=$(api POST "/applications/$app_uuid/start") || fail "Could not trigger deployment" local deploy_uuid deploy_uuid=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deployment_uuid',''))" 2>/dev/null) if [ -z "$deploy_uuid" ]; then fail "No deployment UUID returned: $result" fi ok "Deployment queued: $deploy_uuid" # Step 4: Poll for completion log "Waiting for deployment..." local attempts=0 local max_attempts=40 # 40 * 10s = ~6.5 min local status="" while [ $attempts -lt $max_attempts ]; do sleep 10 attempts=$((attempts + 1)) status=$(api GET "/deployments/$deploy_uuid" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status','unknown'))" 2>/dev/null) case "$status" in finished) echo "" ok "✅ Deployment successful! ($((attempts * 10))s)" # Show app URL local app_info app_info=$(api GET "/applications/$app_uuid" 2>/dev/null) local fqdn fqdn=$(echo "$app_info" | python3 -c "import json,sys; print(json.load(sys.stdin).get('fqdn',''))" 2>/dev/null) [ -n "$fqdn" ] && ok "Live at: $fqdn" return 0 ;; failed) echo "" fail "❌ Deployment failed after $((attempts * 10))s. Check Coolify UI at http://$COOLIFY_HOST:$COOLIFY_PORT" ;; *) printf "\r${CYAN}[ .. ]${NC} Status: %-15s (%ds)" "$status" "$((attempts * 10))" ;; esac done echo "" fail "Deployment timed out after $((max_attempts * 10))s (status: $status)" } # --- Main --- SKIP_BUILD=false STATUS_ONLY=false PROJECT_DIR="" APP_UUID="" # Parse args while [ $# -gt 0 ]; do case "$1" in --skip-build) SKIP_BUILD=true; shift ;; --status) STATUS_ONLY=true; shift ;; --help|-h) echo "Usage: coolify-deploy.sh [options] [project-dir] [app-uuid]" echo "" echo "Options:" echo " --skip-build Push and deploy without building locally" echo " --status UUID Check deployment status for an app" echo " -h, --help Show this help" echo "" echo "If app-uuid is omitted, reads from .coolify-app in project dir." echo "Token from \$COOLIFY_TOKEN env or $CREDS_FILE" exit 0 ;; *) if [ -z "$PROJECT_DIR" ]; then PROJECT_DIR="$1" else APP_UUID="$1" fi shift ;; esac done # Status check mode if [ "$STATUS_ONLY" = "true" ]; then [ -z "$PROJECT_DIR" ] && fail "Usage: coolify-deploy.sh --status " cmd_status "$PROJECT_DIR" exit 0 fi # Resolve project dir if [ -z "$PROJECT_DIR" ]; then if [ -f "package.json" ]; then PROJECT_DIR="$(pwd)" else fail "No project directory specified and not in a Node.js project" fi fi PROJECT_DIR=$(cd "$PROJECT_DIR" && pwd) [ -f "$PROJECT_DIR/package.json" ] || fail "$PROJECT_DIR is not a Node.js project (no package.json)" # Resolve app UUID if [ -z "$APP_UUID" ]; then APP_UUID=$(load_app_config "$PROJECT_DIR") fi if [ -z "$APP_UUID" ]; then fail "No app UUID. Pass it as argument or create $PROJECT_DIR/.coolify-app with the UUID" fi # Save for next time save_app_config "$PROJECT_DIR" "$APP_UUID" > /dev/null # Deploy cmd_deploy "$PROJECT_DIR" "$APP_UUID" "$SKIP_BUILD"