From f3b58b32fd7ea1f0e9042d45567ccdcc9c77d222 Mon Sep 17 00:00:00 2001 From: Sepehr Ramezani Date: Tue, 21 Apr 2026 22:55:15 +0200 Subject: [PATCH] ci: add deploy script and simplify CI/CD workflow - scripts/deploy.sh: automated deployment with interactive env setup (--env-only, --build, --full) - Supports OpenRouter, OpenAI, Ollama, DeepSeek providers - Auto-generates NEXTAUTH_SECRET and postgres password - Waits for healthchecks, initializes DB, shows status - CI/CD workflow simplified to call deploy.sh --build Co-Authored-By: Claude Opus 4.5 --- .gitea/workflows/deploy.yaml | 27 +--- scripts/deploy.sh | 295 +++++++++++++++++++++++++++++++++++ 2 files changed, 299 insertions(+), 23 deletions(-) create mode 100755 scripts/deploy.sh diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index e1a56cb..31f2124 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -17,27 +17,8 @@ jobs: username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} port: ${{ secrets.DEPLOY_PORT || 22 }} + script_stop: true + command_timeout: 10m script: | - set -e - - echo "=== Memento Deploy ===" - echo "Branch: ${{ gitea.ref }}" - - # Go to project directory - cd ${{ secrets.DEPLOY_PATH }}/Momento - - # Pull latest code - git pull origin main - - # Build and restart containers - docker compose build --parallel - docker compose up -d - - # Wait for healthchecks - echo "Waiting for containers to be healthy..." - sleep 10 - - # Check status - docker compose ps - - echo "=== Deploy complete ===" + cd ${{ secrets.DEPLOY_PATH }} + bash scripts/deploy.sh --build diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..17208ce --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,295 @@ +#!/bin/bash +set -euo pipefail + +# ============================================================ +# Memento - Deploy Script +# ============================================================ +# Usage: ./scripts/deploy.sh [--env-only | --build | --full] +# +# --env-only : Generate .env.docker interactively +# --build : Build and start containers (default) +# --full : env + build + first-time setup +# ============================================================ + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +ENV_FILE="$PROJECT_DIR/.env.docker" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } + +# ----------------------------------------------------------- +# Generate .env.docker +# ----------------------------------------------------------- +generate_env() { + if [ -f "$ENV_FILE" ]; then + warn "$ENV_FILE already exists." + read -rp "Overwrite? [y/N] " confirm + [[ "$confirm" != "y" && "$confirm" != "Y" ]] && return 0 + fi + + info "Generating $ENV_FILE ..." + + # Auto-generate NEXTAUTH_SECRET + local secret + secret=$(openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64) + + # Auto-generate secure postgres password + local pg_pass + pg_pass=$(openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | hexdump -v -e '/1 "%02x"') + + # Ask for URL + local url="http://192.168.1.190" + read -rp "NEXTAUTH_URL [$url]: " input_url + [ -n "$input_url" ] && url="$input_url" + + # Ask for AI provider + echo "" + echo "AI Provider:" + echo " 1) OpenRouter (recommended, works out of the box)" + echo " 2) Ollama (requires ollama container)" + echo " 3) OpenAI" + echo " 4) DeepSeek" + echo " 5) Skip AI configuration" + read -rp "Choice [1]: " ai_choice + ai_choice="${ai_choice:-1}" + + local ai_tags_provider="openrouter" + local ai_tags_model="google/gemma-3-27b-it" + local ai_embed_provider="openrouter" + local ai_embed_model="text-embedding-3-small" + local ai_chat_provider="openrouter" + local ai_chat_model="google/gemma-3-27b-it" + local custom_api_key="" + local custom_base_url="" + + case "$ai_choice" in + 1) + read -rp "OpenRouter API Key: " custom_api_key + [ -z "$custom_api_key" ] && error "API key is required" + custom_base_url="https://openrouter.ai/api/v1" + ;; + 2) + ai_tags_provider="ollama" + ai_tags_model="granite4:latest" + ai_embed_provider="ollama" + ai_embed_model="embeddinggemma:latest" + ai_chat_provider="ollama" + ai_chat_model="granite4:latest" + custom_base_url="http://ollama:11434" + ;; + 3) + ai_tags_provider="openai" + ai_tags_model="gpt-4o-mini" + ai_embed_provider="openai" + ai_embed_model="text-embedding-3-small" + ai_chat_provider="openai" + ai_chat_model="gpt-4o-mini" + read -rp "OpenAI API Key: " custom_api_key + [ -z "$custom_api_key" ] && error "API key is required" + ;; + 4) + ai_tags_provider="deepseek" + ai_tags_model="deepseek-chat" + ai_embed_provider="openrouter" + ai_embed_model="text-embedding-3-small" + ai_chat_provider="deepseek" + ai_chat_model="deepseek-chat" + read -rp "DeepSeek API Key: " custom_api_key + [ -z "$custom_api_key" ] && error "API key is required" + custom_base_url="https://api.deepseek.com/v1" + ;; + 5) + info "Skipping AI configuration" + ;; + *) + error "Invalid choice" + ;; + esac + + # Ask for email provider + echo "" + echo "Email provider:" + echo " 1) Resend" + echo " 2) SMTP" + echo " 3) Skip" + read -rp "Choice [3]: " email_choice + email_choice="${email_choice:-3}" + + local resend_key="" + local smtp_config="" + + case "$email_choice" in + 1) + read -rp "Resend API Key: " resend_key + ;; + 2) + local smtp_host smtp_port smtp_user smtp_pass smtp_from + read -rp "SMTP Host: " smtp_host + read -rp "SMTP Port [587]: " smtp_port; smtp_port="${smtp_port:-587}" + read -rp "SMTP User: " smtp_user + read -rp "SMTP Password: " smtp_pass + read -rp "SMTP From: " smtp_from + smtp_config="SMTP_HOST=\"$smtp_host\" +SMTP_PORT=\"$smtp_port\" +SMTP_USER=\"$smtp_user\" +SMTP_PASS=\"$smtp_pass\" +SMTP_FROM=\"$smtp_from\"" + ;; + esac + + # Write .env.docker + cat > "$ENV_FILE" << ENVEOF +# ============================================================================= +# Memento - Docker Environment (auto-generated) +# ============================================================================= +NEXTAUTH_URL="${url}" +NEXTAUTH_SECRET="${secret}" +ALLOW_REGISTRATION="true" + +# PostgreSQL +POSTGRES_PORT=5432 +POSTGRES_DB=memento +POSTGRES_USER=memento +POSTGRES_PASSWORD="${pg_pass}" + +# MCP Server +MCP_MODE="sse" +MCP_PORT="3001" + +# AI - Tags +AI_PROVIDER_TAGS=${ai_tags_provider} +AI_MODEL_TAGS="${ai_tags_model}" + +# AI - Embeddings +AI_PROVIDER_EMBEDDING=${ai_embed_provider} +AI_MODEL_EMBEDDING="${ai_embed_model}" + +# AI - Chat +AI_PROVIDER_CHAT=${ai_chat_provider} +AI_MODEL_CHAT="${ai_chat_model}" +ENVEOF + + # Add API key config if applicable + if [ -n "$custom_api_key" ]; then + case "$ai_choice" in + 1) + echo "CUSTOM_OPENAI_API_KEY=\"${custom_api_key}\"" >> "$ENV_FILE" + echo "CUSTOM_OPENAI_BASE_URL=\"${custom_base_url}\"" >> "$ENV_FILE" + ;; + 3) + echo "OPENAI_API_KEY=\"${custom_api_key}\"" >> "$ENV_FILE" + ;; + 4) + echo "CUSTOM_OPENAI_API_KEY=\"${custom_api_key}\"" >> "$ENV_FILE" + echo "CUSTOM_OPENAI_BASE_URL=\"${custom_base_url}\"" >> "$ENV_FILE" + ;; + esac + fi + + # Add Ollama URL if using ollama + if [ "$ai_choice" = "2" ]; then + echo "OLLAMA_BASE_URL=\"http://ollama:11434\"" >> "$ENV_FILE" + fi + + # Add email config + if [ -n "$resend_key" ]; then + echo "RESEND_API_KEY=\"${resend_key}\"" >> "$ENV_FILE" + fi + if [ -n "$smtp_config" ]; then + echo "" >> "$ENV_FILE" + echo "$smtp_config" >> "$ENV_FILE" + fi + + ok "$ENV_FILE created" + echo "" + info "Contents:" + grep -v "KEY\|PASSWORD\|SECRET" "$ENV_FILE" | sed 's/^/ /' + echo " (sensitive values hidden)" +} + +# ----------------------------------------------------------- +# Build and deploy +# ----------------------------------------------------------- +deploy() { + [ -f "$ENV_FILE" ] || error "$ENV_FILE not found. Run with --env-only first." + + cd "$PROJECT_DIR" + + info "Pulling latest code..." + git pull origin main 2>/dev/null || warn "Could not pull (maybe already up to date or no git)" + + info "Building containers..." + docker compose build --parallel 2>&1 + + info "Starting containers..." + docker compose up -d 2>&1 + + info "Waiting for healthchecks..." + local retries=0 + local max_retries=30 + while [ $retries -lt $max_retries ]; do + local unhealthy + unhealthy=$(docker compose ps --format '{{.Status}}' 2>/dev/null | grep -c -v "healthy\|Up" || true) + if [ "$unhealthy" -eq 0 ]; then + break + fi + retries=$((retries + 1)) + sleep 2 + printf "." + done + echo "" + + info "Initializing database..." + docker compose exec memento-note npx prisma db push --skip-generate 2>/dev/null || \ + warn "DB push failed (may already be synced)" + + echo "" + echo "==========================================" + docker compose ps + echo "==========================================" + echo "" + ok "Deploy complete!" + info "App: $(grep NEXTAUTH_URL "$ENV_FILE" | cut -d= -f2 | tr -d '"')" + + # Show admin setup hint if first time + local user_count + user_count=$(docker compose exec -T postgres psql -U memento -d memento -t -c 'SELECT COUNT(*) FROM "User"' 2>/dev/null | tr -d ' ' || echo "0") + if [ "$user_count" = "0" ]; then + echo "" + warn "No users found. Register at $(grep NEXTAUTH_URL "$ENV_FILE" | cut -d= -f2 | tr -d '"')/register" + warn "Then run: docker compose exec memento-note npx tsx scripts/grant-all-admins.ts" + fi +} + +# ----------------------------------------------------------- +# Main +# ----------------------------------------------------------- +ACTION="${1:---build}" + +case "$ACTION" in + --env-only) + generate_env + ;; + --build) + deploy + ;; + --full) + generate_env + echo "" + deploy + ;; + *) + echo "Usage: $0 [--env-only | --build | --full]" + exit 1 + ;; +esac