256 lines
7.7 KiB
Bash
Executable File
256 lines
7.7 KiB
Bash
Executable File
#!/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 <app-uuid>"
|
|
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"
|