fix(ci): trigger workflow on main branch to enable :latest tag
Changes:
- Create Gitea workflow for ai-stack-deployer
- Trigger on main branch (default branch)
- Use oussamadouhou + REGISTRY_TOKEN for authentication
- Build from ./Dockerfile
This enables :latest tag creation via {{is_default_branch}}.
Tags created:
- git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:latest
- git.app.flexinit.nl/oussamadouhou/ai-stack-deployer:<sha>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
176
scripts/claude-session.sh
Executable file
176
scripts/claude-session.sh
Executable file
@@ -0,0 +1,176 @@
|
||||
#!/bin/bash
|
||||
# scripts/claude-session.sh
|
||||
# Description: Manage persistent Claude Code sessions for AI Stack Deployer
|
||||
# Usage: bash scripts/claude-session.sh [list|delete <name>|help]
|
||||
|
||||
# Configuration
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# Store sessions in user's home directory, NOT in project root
|
||||
SESSION_DIR="$HOME/.claude/sessions/ai-stack-deployer"
|
||||
mkdir -p "$SESSION_DIR"
|
||||
|
||||
# Claude Code built-in session storage
|
||||
CLAUDE_BUILTIN_DIR="$HOME/.claude/projects/-home-odouhou-locale-projects-ai-stack-deployer"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
function success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
function error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
return 1
|
||||
}
|
||||
|
||||
function info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
function warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
# List all sessions
|
||||
function list_sessions() {
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " AI Stack Deployer Claude Code Sessions"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
local has_custom=false
|
||||
local has_builtin=false
|
||||
|
||||
# List custom sessions
|
||||
if [ "$(ls -A $SESSION_DIR 2>/dev/null)" ]; then
|
||||
has_custom=true
|
||||
echo -e "${MAGENTA}📁 Custom Persistent Sessions${NC}"
|
||||
echo ""
|
||||
|
||||
for session_file in "$SESSION_DIR"/*.session; do
|
||||
if [ -f "$session_file" ]; then
|
||||
source "$session_file"
|
||||
local name=$(basename "$session_file" .session)
|
||||
echo -e "${BLUE}Session:${NC} $name"
|
||||
echo " ID: $CLAUDE_SESSION_ID"
|
||||
echo " Started: $CLAUDE_SESSION_START"
|
||||
echo " Last used: ${CLAUDE_SESSION_LAST_USED:-Never}"
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# List Claude Code built-in sessions
|
||||
if [ -d "$CLAUDE_BUILTIN_DIR" ] && [ "$(ls -A $CLAUDE_BUILTIN_DIR/*.jsonl 2>/dev/null)" ]; then
|
||||
has_builtin=true
|
||||
|
||||
if [ "$has_custom" = true ]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo -e "${MAGENTA}🤖 Claude Code Built-in Sessions${NC}"
|
||||
echo ""
|
||||
|
||||
for jsonl_file in "$CLAUDE_BUILTIN_DIR"/*.jsonl; do
|
||||
if [ -f "$jsonl_file" ]; then
|
||||
local session_id=$(basename "$jsonl_file" .jsonl)
|
||||
local file_size=$(du -h "$jsonl_file" | cut -f1)
|
||||
local created=$(stat -c %y "$jsonl_file" 2>/dev/null | cut -d'.' -f1 || stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$jsonl_file" 2>/dev/null)
|
||||
local modified=$(stat -c %y "$jsonl_file" 2>/dev/null | cut -d'.' -f1 || stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$jsonl_file" 2>/dev/null)
|
||||
|
||||
echo -e "${BLUE}Session ID:${NC} $session_id"
|
||||
echo " Size: $file_size"
|
||||
echo " Created: $created"
|
||||
echo " Last modified: $modified"
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$has_custom" = false ] && [ "$has_builtin" = false ]; then
|
||||
warning "No sessions found"
|
||||
echo ""
|
||||
info "Custom sessions: $SESSION_DIR"
|
||||
info "Built-in sessions: $CLAUDE_BUILTIN_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# Delete a session
|
||||
function delete_session() {
|
||||
local SESSION_NAME="$1"
|
||||
local SESSION_FILE="$SESSION_DIR/$SESSION_NAME.session"
|
||||
|
||||
if [ ! -f "$SESSION_FILE" ]; then
|
||||
error "Session not found: $SESSION_NAME"
|
||||
return 1
|
||||
fi
|
||||
|
||||
source "$SESSION_FILE"
|
||||
|
||||
echo ""
|
||||
warning "Delete session: $SESSION_NAME"
|
||||
echo " ID: $CLAUDE_SESSION_ID"
|
||||
echo " Started: $CLAUDE_SESSION_START"
|
||||
echo ""
|
||||
read -p "Are you sure? (yes/no): " -r REPLY
|
||||
|
||||
if [ "$REPLY" == "yes" ]; then
|
||||
rm "$SESSION_FILE"
|
||||
success "Session deleted: $SESSION_NAME"
|
||||
else
|
||||
info "Deletion cancelled"
|
||||
fi
|
||||
}
|
||||
|
||||
# Show help
|
||||
function show_help() {
|
||||
echo "Usage: bash scripts/claude-session.sh [command]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " list - List all sessions"
|
||||
echo " delete <name> - Delete a session"
|
||||
echo " help - Show this help"
|
||||
echo ""
|
||||
echo "Environment variables set:"
|
||||
echo " CLAUDE_SESSION_ID - Persistent session UUID"
|
||||
echo " CLAUDE_SESSION_NAME - Session name"
|
||||
echo " CLAUDE_SESSION_START - When session was created"
|
||||
echo " CLAUDE_SESSION_LAST_USED - Last usage timestamp"
|
||||
echo " CLAUDE_SESSION_PROJECT - Project name (ai-stack-deployer)"
|
||||
echo " CLAUDE_SESSION_MCP_GROUP - MCP group ID (project_ai_stack_deployer)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " bash scripts/claude-session.sh list"
|
||||
echo " bash scripts/claude-session.sh delete feature-mcp-tools"
|
||||
echo ""
|
||||
echo "To start a session:"
|
||||
echo " ./scripts/claude-start.sh feature-name"
|
||||
}
|
||||
|
||||
# Main logic
|
||||
case "${1:-}" in
|
||||
list)
|
||||
list_sessions
|
||||
;;
|
||||
delete)
|
||||
if [ -z "${2:-}" ]; then
|
||||
error "Usage: $0 delete <session-name>"
|
||||
exit 1
|
||||
fi
|
||||
delete_session "$2"
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
show_help
|
||||
;;
|
||||
esac
|
||||
201
scripts/claude-start.sh
Executable file
201
scripts/claude-start.sh
Executable file
@@ -0,0 +1,201 @@
|
||||
#!/bin/bash
|
||||
# scripts/claude-start.sh
|
||||
# Description: Start Claude Code with persistent session management for AI Stack Deployer
|
||||
# Usage: ./scripts/claude-start.sh [session-name] [additional-flags]
|
||||
|
||||
set -e # Exit on error
|
||||
set -u # Exit on undefined variable
|
||||
|
||||
# Configuration
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Store sessions in user's home directory, NOT in project root
|
||||
SESSION_DIR="$HOME/.claude/sessions/ai-stack-deployer"
|
||||
mkdir -p "$SESSION_DIR"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
function success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
function error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
function warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
function header() {
|
||||
echo ""
|
||||
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${MAGENTA}$1${NC}"
|
||||
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
}
|
||||
|
||||
# Generate UUID v4
|
||||
function generate_uuid() {
|
||||
if command -v uuidgen &> /dev/null; then
|
||||
uuidgen | tr '[:upper:]' '[:lower:]'
|
||||
elif command -v python3 &> /dev/null; then
|
||||
python3 -c "import uuid; print(str(uuid.uuid4()))"
|
||||
else
|
||||
# Fallback: use random + timestamp
|
||||
echo "$(date +%s)-$(( RANDOM % 100000 ))-$(( RANDOM % 100000 ))-$(( RANDOM % 100000 ))"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get or create session
|
||||
function get_or_create_session() {
|
||||
local SESSION_NAME="${1:-$(date +%Y%m%d-%H%M)}"
|
||||
local SESSION_FILE="$SESSION_DIR/$SESSION_NAME.session"
|
||||
|
||||
# Check if session exists
|
||||
if [ -f "$SESSION_FILE" ]; then
|
||||
# Load existing session
|
||||
source "$SESSION_FILE"
|
||||
|
||||
info "Resuming session: ${MAGENTA}$SESSION_NAME${NC}"
|
||||
info "Session ID: ${BLUE}$CLAUDE_SESSION_ID${NC}"
|
||||
info "Started: $CLAUDE_SESSION_START"
|
||||
|
||||
# Calculate session age
|
||||
if command -v python3 &> /dev/null; then
|
||||
SESSION_AGE=$(python3 -c "
|
||||
from datetime import datetime
|
||||
start = datetime.strptime('$CLAUDE_SESSION_START', '%Y-%m-%d %H:%M:%S')
|
||||
age = datetime.now() - start
|
||||
days = age.days
|
||||
hours = age.seconds // 3600
|
||||
print(f'{days}d {hours}h')
|
||||
" 2>/dev/null || echo "N/A")
|
||||
info "Age: $SESSION_AGE"
|
||||
fi
|
||||
|
||||
# Update last used timestamp
|
||||
echo "export CLAUDE_SESSION_LAST_USED=\"$(date +%Y-%m-%d\ %H:%M:%S)\"" >> "$SESSION_FILE"
|
||||
else
|
||||
# Create new session
|
||||
CLAUDE_SESSION_ID=$(generate_uuid)
|
||||
CLAUDE_SESSION_NAME="$SESSION_NAME"
|
||||
CLAUDE_SESSION_START="$(date +%Y-%m-%d\ %H:%M:%S)"
|
||||
CLAUDE_SESSION_LAST_USED="$(date +%Y-%m-%d\ %H:%M:%S)"
|
||||
|
||||
# Save session
|
||||
cat > "$SESSION_FILE" << SESSIONEOF
|
||||
# Claude Code Session: $SESSION_NAME
|
||||
# Created: $CLAUDE_SESSION_START
|
||||
export CLAUDE_SESSION_ID="$CLAUDE_SESSION_ID"
|
||||
export CLAUDE_SESSION_NAME="$SESSION_NAME"
|
||||
export CLAUDE_SESSION_START="$CLAUDE_SESSION_START"
|
||||
export CLAUDE_SESSION_LAST_USED="$CLAUDE_SESSION_LAST_USED"
|
||||
export CLAUDE_SESSION_PROJECT="ai-stack-deployer"
|
||||
export CLAUDE_SESSION_MCP_GROUP="project_ai_stack_deployer"
|
||||
SESSIONEOF
|
||||
|
||||
success "Created new session: ${MAGENTA}$SESSION_NAME${NC}"
|
||||
info "Session ID: ${BLUE}$CLAUDE_SESSION_ID${NC}"
|
||||
info "Session file: $SESSION_FILE"
|
||||
fi
|
||||
|
||||
# Export variables
|
||||
export CLAUDE_SESSION_ID
|
||||
export CLAUDE_SESSION_NAME
|
||||
export CLAUDE_SESSION_START
|
||||
export CLAUDE_SESSION_LAST_USED
|
||||
export CLAUDE_SESSION_PROJECT="ai-stack-deployer"
|
||||
export CLAUDE_SESSION_MCP_GROUP="project_ai_stack_deployer"
|
||||
}
|
||||
|
||||
# Main script logic
|
||||
function main() {
|
||||
local SESSION_NAME="${1:-$(date +%Y%m%d-%H%M)}"
|
||||
shift || true # Remove first argument if it exists
|
||||
local ADDITIONAL_FLAGS="$@"
|
||||
|
||||
header "AI Stack Deployer - Claude Code Session Manager"
|
||||
|
||||
# Get or create session
|
||||
get_or_create_session "$SESSION_NAME"
|
||||
|
||||
# Display helpful information
|
||||
echo ""
|
||||
info "Starting Claude Code with persistent session..."
|
||||
echo ""
|
||||
warning "Session Environment Variables:"
|
||||
echo " CLAUDE_SESSION_ID=$CLAUDE_SESSION_ID"
|
||||
echo " CLAUDE_SESSION_NAME=$CLAUDE_SESSION_NAME"
|
||||
echo " CLAUDE_SESSION_PROJECT=$CLAUDE_SESSION_PROJECT"
|
||||
echo " CLAUDE_SESSION_MCP_GROUP=$CLAUDE_SESSION_MCP_GROUP"
|
||||
echo ""
|
||||
info "Permission Mode: ${CLAUDE_PERMISSION_MODE:-bypassPermissions} (set CLAUDE_PERMISSION_MODE to override)"
|
||||
|
||||
echo ""
|
||||
info "Quick Commands:"
|
||||
echo " 🔧 Dev server: bun run dev"
|
||||
echo " 🚀 MCP server: bun run mcp"
|
||||
echo " ✅ Type check: bun run typecheck"
|
||||
echo " 🧪 Test clients: bun run src/test-clients.ts"
|
||||
echo ""
|
||||
|
||||
info "Memory Management:"
|
||||
echo " 🧠 Search: graphiti-memory_search_memory_facts({query: '...', group_ids: ['project_ai_stack_deployer']})"
|
||||
echo " 💾 Store: graphiti-memory_add_memory({name: '...', episode_body: '...', group_id: 'project_ai_stack_deployer'})"
|
||||
echo ""
|
||||
|
||||
# Start Claude Code with session ID
|
||||
header "Starting Claude Code"
|
||||
echo ""
|
||||
|
||||
# Build Claude Code command
|
||||
CLAUDE_CMD="claude --session-id \"$CLAUDE_SESSION_ID\""
|
||||
|
||||
# Add permission mode (default: bypassPermissions, override with CLAUDE_PERMISSION_MODE env var)
|
||||
PERMISSION_MODE="${CLAUDE_PERMISSION_MODE:-bypassPermissions}"
|
||||
CLAUDE_CMD="$CLAUDE_CMD --permission-mode $PERMISSION_MODE"
|
||||
|
||||
# Add additional flags if provided
|
||||
if [ -n "$ADDITIONAL_FLAGS" ]; then
|
||||
CLAUDE_CMD="$CLAUDE_CMD $ADDITIONAL_FLAGS"
|
||||
fi
|
||||
|
||||
info "Command: $CLAUDE_CMD"
|
||||
echo ""
|
||||
|
||||
# Execute Claude Code
|
||||
eval $CLAUDE_CMD
|
||||
|
||||
# After Claude Code exits
|
||||
echo ""
|
||||
header "Session Ended"
|
||||
success "Session complete: $CLAUDE_SESSION_NAME"
|
||||
info "Session ID: $CLAUDE_SESSION_ID"
|
||||
|
||||
echo ""
|
||||
warning "Don't forget to store your learnings in Graphiti Memory!"
|
||||
echo "Example:"
|
||||
echo ""
|
||||
echo "graphiti-memory_add_memory({"
|
||||
echo " name: \"Session: $CLAUDE_SESSION_NAME - $(date +%Y-%m-%d)\","
|
||||
echo " episode_body: \"Accomplished: [...]. Decisions: [...]. Issues: [...].\","
|
||||
echo " group_id: \"project_ai_stack_deployer\""
|
||||
echo "})"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
386
scripts/deploy-to-dokploy.sh
Executable file
386
scripts/deploy-to-dokploy.sh
Executable file
@@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# AI Stack Deployer - Automated Deployment Script
|
||||
# Deploys the AI Stack Deployer application to Dokploy
|
||||
#
|
||||
# Usage: ./scripts/deploy-to-dokploy.sh [options]
|
||||
# Options:
|
||||
# --registry REGISTRY Docker registry to push to (default: local Docker)
|
||||
# --domain DOMAIN Domain name for deployment (default: portal.ai.flexinit.nl)
|
||||
# --project-name NAME Dokploy project name (default: ai-stack-deployer-portal)
|
||||
# --skip-build Skip Docker build (use existing image)
|
||||
# --skip-test Skip local testing
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="${SCRIPT_DIR}/.."
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
LOG_FILE="${PROJECT_ROOT}/deployment-${TIMESTAMP}.log"
|
||||
|
||||
# Default values
|
||||
REGISTRY=""
|
||||
DOMAIN="portal.ai.flexinit.nl"
|
||||
PROJECT_NAME="ai-stack-deployer-portal"
|
||||
APP_NAME="ai-stack-deployer-web"
|
||||
IMAGE_NAME="ai-stack-deployer"
|
||||
SKIP_BUILD=false
|
||||
SKIP_TEST=false
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $*" | tee -a "${LOG_FILE}"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $*" | tee -a "${LOG_FILE}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" | tee -a "${LOG_FILE}"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "\n${GREEN}==>${NC} $*\n" | tee -a "${LOG_FILE}"
|
||||
}
|
||||
|
||||
# Error handler
|
||||
error_exit() {
|
||||
log_error "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--registry)
|
||||
REGISTRY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--domain)
|
||||
DOMAIN="$2"
|
||||
shift 2
|
||||
;;
|
||||
--project-name)
|
||||
PROJECT_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-build)
|
||||
SKIP_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--skip-test)
|
||||
SKIP_TEST=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
log_info "Deployment started at ${TIMESTAMP}"
|
||||
log_info "Target domain: ${DOMAIN}"
|
||||
log_info "Project name: ${PROJECT_NAME}"
|
||||
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
# Load environment variables
|
||||
if [ ! -f .env ]; then
|
||||
error_exit ".env file not found. Please create it from .env.example"
|
||||
fi
|
||||
|
||||
source .env
|
||||
|
||||
if [ -z "${DOKPLOY_API_TOKEN}" ]; then
|
||||
error_exit "DOKPLOY_API_TOKEN not set in .env"
|
||||
fi
|
||||
|
||||
if [ -z "${DOKPLOY_URL}" ]; then
|
||||
error_exit "DOKPLOY_URL not set in .env"
|
||||
fi
|
||||
|
||||
# Phase 1: Pre-deployment checks
|
||||
log_step "Phase 1: Pre-deployment Verification"
|
||||
|
||||
log_info "Checking Dokploy API connectivity..."
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "x-api-key: ${DOKPLOY_API_TOKEN}" \
|
||||
"${DOKPLOY_URL}/api/project.all" || echo "000")
|
||||
|
||||
if [ "${HTTP_CODE}" != "200" ]; then
|
||||
error_exit "Dokploy API unreachable or unauthorized (HTTP ${HTTP_CODE})"
|
||||
fi
|
||||
|
||||
log_info "✓ Dokploy API accessible"
|
||||
|
||||
# Check Docker
|
||||
if ! command -v docker &> /dev/null; then
|
||||
error_exit "Docker not installed"
|
||||
fi
|
||||
|
||||
# Try docker with sg if needed
|
||||
if ! docker ps &> /dev/null; then
|
||||
if sg docker -c "docker ps" &> /dev/null; then
|
||||
log_info "Using 'sg docker' for Docker commands"
|
||||
DOCKER_CMD="sg docker -c"
|
||||
else
|
||||
error_exit "Docker not accessible. Check permissions."
|
||||
fi
|
||||
else
|
||||
DOCKER_CMD=""
|
||||
fi
|
||||
|
||||
log_info "✓ Docker accessible"
|
||||
|
||||
# Phase 2: Build Docker image
|
||||
if [ "${SKIP_BUILD}" = false ]; then
|
||||
log_step "Phase 2: Building Docker Image"
|
||||
|
||||
log_info "Building image: ${IMAGE_NAME}:${TIMESTAMP}"
|
||||
|
||||
${DOCKER_CMD} docker build \
|
||||
-t "${IMAGE_NAME}:${TIMESTAMP}" \
|
||||
-t "${IMAGE_NAME}:latest" \
|
||||
. 2>&1 | tee -a "${LOG_FILE}" || error_exit "Docker build failed"
|
||||
|
||||
# Get image info
|
||||
IMAGE_ID=$(${DOCKER_CMD} docker images -q "${IMAGE_NAME}:latest")
|
||||
IMAGE_SIZE=$(${DOCKER_CMD} docker images "${IMAGE_NAME}:latest" --format "{{.Size}}")
|
||||
|
||||
log_info "✓ Image built successfully"
|
||||
log_info " Image ID: ${IMAGE_ID}"
|
||||
log_info " Size: ${IMAGE_SIZE}"
|
||||
else
|
||||
log_warn "Skipping build (--skip-build specified)"
|
||||
fi
|
||||
|
||||
# Phase 3: Local testing
|
||||
if [ "${SKIP_TEST}" = false ]; then
|
||||
log_step "Phase 3: Local Container Testing"
|
||||
|
||||
log_info "Starting test container..."
|
||||
|
||||
# Stop existing test container if any
|
||||
${DOCKER_CMD} docker stop ai-stack-deployer-test 2>/dev/null || true
|
||||
${DOCKER_CMD} docker rm ai-stack-deployer-test 2>/dev/null || true
|
||||
|
||||
# Start test container
|
||||
${DOCKER_CMD} docker run -d \
|
||||
--name ai-stack-deployer-test \
|
||||
-p 3001:3000 \
|
||||
--env-file .env \
|
||||
"${IMAGE_NAME}:latest" || error_exit "Failed to start test container"
|
||||
|
||||
log_info "Waiting for container to be healthy..."
|
||||
sleep 5
|
||||
|
||||
# Test health endpoint
|
||||
if ! curl -sf http://localhost:3001/health > /dev/null; then
|
||||
${DOCKER_CMD} docker logs ai-stack-deployer-test | tail -20
|
||||
${DOCKER_CMD} docker stop ai-stack-deployer-test
|
||||
${DOCKER_CMD} docker rm ai-stack-deployer-test
|
||||
error_exit "Health check failed"
|
||||
fi
|
||||
|
||||
log_info "✓ Container healthy"
|
||||
|
||||
# Cleanup
|
||||
${DOCKER_CMD} docker stop ai-stack-deployer-test
|
||||
${DOCKER_CMD} docker rm ai-stack-deployer-test
|
||||
|
||||
log_info "✓ Local testing complete"
|
||||
else
|
||||
log_warn "Skipping local test (--skip-test specified)"
|
||||
fi
|
||||
|
||||
# Phase 4: Registry push (if specified)
|
||||
if [ -n "${REGISTRY}" ]; then
|
||||
log_step "Phase 4: Pushing to Registry"
|
||||
|
||||
FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:latest"
|
||||
|
||||
log_info "Tagging image for registry: ${FULL_IMAGE_NAME}"
|
||||
${DOCKER_CMD} docker tag "${IMAGE_NAME}:latest" "${FULL_IMAGE_NAME}"
|
||||
|
||||
log_info "Pushing to registry..."
|
||||
${DOCKER_CMD} docker push "${FULL_IMAGE_NAME}" || error_exit "Failed to push to registry"
|
||||
|
||||
log_info "✓ Image pushed to registry"
|
||||
IMAGE_FOR_DOKPLOY="${FULL_IMAGE_NAME}"
|
||||
else
|
||||
log_warn "No registry specified - Dokploy must have access to local Docker"
|
||||
IMAGE_FOR_DOKPLOY="${IMAGE_NAME}:latest"
|
||||
fi
|
||||
|
||||
# Phase 5: Deploy to Dokploy
|
||||
log_step "Phase 5: Deploying to Dokploy"
|
||||
|
||||
# Check if project exists
|
||||
log_info "Checking for existing project..."
|
||||
EXISTING_PROJECT=$(curl -s \
|
||||
-H "x-api-key: ${DOKPLOY_API_TOKEN}" \
|
||||
"${DOKPLOY_URL}/api/project.all" | \
|
||||
jq -r ".[]? | select(.name==\"${PROJECT_NAME}\") | .projectId" || echo "")
|
||||
|
||||
if [ -n "${EXISTING_PROJECT}" ]; then
|
||||
log_info "✓ Found existing project: ${EXISTING_PROJECT}"
|
||||
PROJECT_ID="${EXISTING_PROJECT}"
|
||||
else
|
||||
log_info "Creating new project..."
|
||||
|
||||
CREATE_PROJECT_RESPONSE=$(curl -s -X POST \
|
||||
-H "x-api-key: ${DOKPLOY_API_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${DOKPLOY_URL}/api/project.create" \
|
||||
-d "{
|
||||
\"name\": \"${PROJECT_NAME}\",
|
||||
\"description\": \"Self-service portal for deploying AI stacks\"
|
||||
}") || error_exit "Failed to create project"
|
||||
|
||||
PROJECT_ID=$(echo "${CREATE_PROJECT_RESPONSE}" | jq -r '.project.projectId // empty')
|
||||
|
||||
if [ -z "${PROJECT_ID}" ]; then
|
||||
log_error "API Response: ${CREATE_PROJECT_RESPONSE}"
|
||||
error_exit "Failed to extract project ID"
|
||||
fi
|
||||
|
||||
log_info "✓ Created project: ${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# Create/Update application
|
||||
log_info "Creating application..."
|
||||
|
||||
# Prepare environment variables
|
||||
ENV_VARS="DOKPLOY_URL=${DOKPLOY_URL}
|
||||
DOKPLOY_API_TOKEN=${DOKPLOY_API_TOKEN}
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
STACK_DOMAIN_SUFFIX=${STACK_DOMAIN_SUFFIX:-ai.flexinit.nl}
|
||||
STACK_IMAGE=${STACK_IMAGE:-git.app.flexinit.nl/oussamadouhou/oh-my-opencode-free:latest}
|
||||
RESERVED_NAMES=${RESERVED_NAMES:-admin,api,www,root,system,test,demo,portal}"
|
||||
|
||||
CREATE_APP_RESPONSE=$(curl -s -X POST \
|
||||
-H "x-api-key: ${DOKPLOY_API_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${DOKPLOY_URL}/api/application.create" \
|
||||
-d "{
|
||||
\"name\": \"${APP_NAME}\",
|
||||
\"projectId\": \"${PROJECT_ID}\",
|
||||
\"dockerImage\": \"${IMAGE_FOR_DOKPLOY}\",
|
||||
\"env\": \"${ENV_VARS}\"
|
||||
}") || error_exit "Failed to create application"
|
||||
|
||||
APP_ID=$(echo "${CREATE_APP_RESPONSE}" | jq -r '.application.applicationId // .applicationId // empty')
|
||||
|
||||
if [ -z "${APP_ID}" ]; then
|
||||
log_error "API Response: ${CREATE_APP_RESPONSE}"
|
||||
error_exit "Failed to extract application ID"
|
||||
fi
|
||||
|
||||
log_info "✓ Created application: ${APP_ID}"
|
||||
|
||||
# Configure domain
|
||||
log_info "Configuring domain: ${DOMAIN}"
|
||||
|
||||
curl -s -X POST \
|
||||
-H "x-api-key: ${DOKPLOY_API_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${DOKPLOY_URL}/api/domain.create" \
|
||||
-d "{
|
||||
\"domain\": \"${DOMAIN}\",
|
||||
\"applicationId\": \"${APP_ID}\",
|
||||
\"https\": true,
|
||||
\"port\": 3000
|
||||
}" || log_warn "Domain configuration may have failed (might already exist)"
|
||||
|
||||
log_info "✓ Domain configured"
|
||||
|
||||
# Deploy application
|
||||
log_info "Triggering deployment..."
|
||||
|
||||
DEPLOY_RESPONSE=$(curl -s -X POST \
|
||||
-H "x-api-key: ${DOKPLOY_API_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${DOKPLOY_URL}/api/application.deploy" \
|
||||
-d "{
|
||||
\"applicationId\": \"${APP_ID}\"
|
||||
}") || error_exit "Failed to trigger deployment"
|
||||
|
||||
DEPLOY_ID=$(echo "${DEPLOY_RESPONSE}" | jq -r '.deploymentId // "unknown"')
|
||||
|
||||
log_info "✓ Deployment triggered: ${DEPLOY_ID}"
|
||||
log_info "Monitor at: ${DOKPLOY_URL}/project/${PROJECT_ID}"
|
||||
|
||||
# Phase 6: Verification
|
||||
log_step "Phase 6: Deployment Verification"
|
||||
|
||||
log_info "Waiting for deployment (60 seconds)..."
|
||||
sleep 60
|
||||
|
||||
log_info "Testing health endpoint..."
|
||||
if curl -sf "https://${DOMAIN}/health" > /dev/null 2>&1; then
|
||||
log_info "✓ Application is healthy at https://${DOMAIN}"
|
||||
|
||||
# Get health status
|
||||
HEALTH_RESPONSE=$(curl -s "https://${DOMAIN}/health")
|
||||
echo "${HEALTH_RESPONSE}" | jq . || echo "${HEALTH_RESPONSE}"
|
||||
else
|
||||
log_warn "Health check failed - deployment may still be in progress"
|
||||
log_info "Check status at: ${DOKPLOY_URL}/project/${PROJECT_ID}"
|
||||
fi
|
||||
|
||||
# Save deployment record
|
||||
log_step "Deployment Summary"
|
||||
|
||||
cat > "deployment-record-${TIMESTAMP}.txt" << EOF
|
||||
Deployment Completed: $(date -Iseconds)
|
||||
===========================================
|
||||
|
||||
Configuration:
|
||||
- Project Name: ${PROJECT_NAME}
|
||||
- Application Name: ${APP_NAME}
|
||||
- Domain: https://${DOMAIN}
|
||||
- Image: ${IMAGE_FOR_DOKPLOY}
|
||||
|
||||
Dokploy IDs:
|
||||
- Project ID: ${PROJECT_ID}
|
||||
- Application ID: ${APP_ID}
|
||||
- Deployment ID: ${DEPLOY_ID}
|
||||
|
||||
Management:
|
||||
- Dokploy Console: ${DOKPLOY_URL}/project/${PROJECT_ID}
|
||||
- Application URL: https://${DOMAIN}
|
||||
- Health Check: https://${DOMAIN}/health
|
||||
|
||||
Build Info:
|
||||
- Timestamp: ${TIMESTAMP}
|
||||
- Image ID: ${IMAGE_ID:-N/A}
|
||||
- Image Size: ${IMAGE_SIZE:-N/A}
|
||||
|
||||
Status: SUCCESS
|
||||
EOF
|
||||
|
||||
log_info "Deployment record saved: deployment-record-${TIMESTAMP}.txt"
|
||||
log_info "Deployment log saved: ${LOG_FILE}"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} Deployment Completed Successfully!${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e " 🌐 Application URL: ${GREEN}https://${DOMAIN}${NC}"
|
||||
echo -e " ⚙️ Dokploy Console: ${DOKPLOY_URL}/project/${PROJECT_ID}"
|
||||
echo -e " 📊 Health Check: https://${DOMAIN}/health"
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user