name: Deploy to Production (Manual) on: workflow_dispatch: # Phase 4: Manual-only deployment # Automatic deployment moved to merge-to-main.yml (Stage 5) # Use this workflow for manual deployments when needed concurrency: group: deploy-prod-main cancel-in-progress: true env: DEPLOY_HOST: quant.taxbaik.com DEPLOY_USER: kjh2064 SERVICE_NAME: quantengine DOTNET_VERSION: '10.0.x' QUANTENGINE_DB_NAME: quantenginedb QUANTENGINE_DB_USER: quantengine_app TELEGRAM_BOT_TOKEN_DEFAULT: "8734507814:AAFyacLMai8GB4K-hQ_Nd3t3D01A-H1ZdV0" TELEGRAM_CHAT_ID_DEFAULT: "-5460205872" jobs: build-and-deploy: name: Build & Deploy to Production runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout Code uses: actions/checkout@v3 - name: Setup .NET uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Setup Python uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install Python Dependencies run: pip install pyyaml openpyxl requests - name: "[GATE] Run Core Validations" run: | echo "πŸ” Running critical CI validations..." python3 tools/validate_no_direct_api_trading_v1.py || exit 1 python3 tools/validate_specs.py || exit 1 echo "βœ… All critical validations passed" - name: Ensure Temp Directory and Mock Packet run: | mkdir -p Temp if [ ! -f Temp/final_decision_packet_active.json ]; then echo '{"active_decision": "PASS", "details": "CI dummy packet"}' > Temp/final_decision_packet_active.json fi - name: Restore Dependencies run: dotnet restore src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj - name: Build Release run: | dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \ -c Release \ --no-restore - name: Run Unit Tests run: | dotnet test src/dotnet/QuantEngine.Core.Tests/QuantEngine.Core.Tests.csproj \ -c Release \ --no-build - name: Publish Release Package run: | dotnet publish src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \ -c Release \ --no-build \ -o ./publish - name: Generate Build Info run: | COMMIT_HASH=$(git rev-parse --short HEAD) BUILD_TIME=$(date -d "+9 hours" +'%Y-%m-%d %H:%M:%S KST') mkdir -p ./publish/wwwroot printf '{\n "version": "1.0.%s-%s",\n "built": "%s"\n}\n' "${{ github.run_number }}" "$COMMIT_HASH" "$BUILD_TIME" > ./publish/wwwroot/version.json echo "βœ“ Generated version info: 1.0.${{ github.run_number }}-$COMMIT_HASH @ $BUILD_TIME" - name: Prepare & Validate QuantEngine DB Env run: | echo "πŸ”§ Preparing database environment..." DB_PASSWORD="${{ secrets.QUANTENGINE_DB_PASSWORD }}" if [ -z "$DB_PASSWORD" ]; then echo "❌ QUANTENGINE_DB_PASSWORD secret not configured in Gitea" echo " Please set secret in Repository Settings > Secrets" exit 1 fi if [ -z "${{ env.QUANTENGINE_DB_NAME }}" ] || [ -z "${{ env.QUANTENGINE_DB_USER }}" ]; then echo "❌ DB configuration environment variables not set" exit 1 fi # 배포 폴더에 ν™˜κ²½ μ„€μ • 생성 mkdir -p ./deploy printf 'ConnectionStrings__DefaultConnection=Host=127.0.0.1;Database=%s;Username=%s;Password=%s;Search Path=quantengine;\n' \ "${{ env.QUANTENGINE_DB_NAME }}" \ "${{ env.QUANTENGINE_DB_USER }}" \ "$DB_PASSWORD" > ./deploy/quantengine.env chmod 600 ./deploy/quantengine.env # appsettings.Production.json 생성 mkdir -p ./publish cat < ./publish/appsettings.Production.json { "ConnectionStrings": { "DefaultConnection": "Host=127.0.0.1;Database=${{ env.QUANTENGINE_DB_NAME }};Username=${{ env.QUANTENGINE_DB_USER }};Password=${DB_PASSWORD};Search Path=quantengine;" } } EOF chmod 600 ./publish/appsettings.Production.json if [ ! -f ./deploy/quantengine.env ] || [ ! -f ./publish/appsettings.Production.json ]; then echo "❌ Failed to create database config files" exit 1 fi echo "βœ“ Database configuration prepared" - name: Copy Deployment Scripts run: | echo "πŸ“‹ Copying deployment scripts..." cp deploy_gb.sh ./publish/deploy_gb.sh mkdir -p ./publish/scripts cp scripts/validate_migrations.sh ./publish/scripts/validate_migrations.sh chmod +x ./publish/deploy_gb.sh ./publish/scripts/validate_migrations.sh echo "βœ“ Deployment scripts copied" - name: Package Artifact run: | echo "πŸ“¦ Creating deployment package..." if ! tar -czf quantengine.tar.gz -C ./publish .; then echo "❌ Failed to create package" exit 1 fi PACKAGE_SIZE=$(du -sh quantengine.tar.gz | cut -f1) PACKAGE_BYTES=$(stat -c%s quantengine.tar.gz 2>/dev/null || echo "0") if [ -z "$PACKAGE_BYTES" ] || [ "$PACKAGE_BYTES" -lt 1000000 ]; then echo "⚠️ Warning: Package seems too small ($PACKAGE_SIZE)" fi if [ ! -f quantengine.tar.gz ]; then echo "❌ Package file not created" exit 1 fi echo "βœ“ Package created: $PACKAGE_SIZE" tar -tzf quantengine.tar.gz | head -n 5 || true - name: Pre-Deployment Migration Validation run: | echo "=== Pre-Deployment Database Check ===" # 배포 νŒ¨ν‚€μ§€ μž„μ‹œ μΆ”μΆœ (κ²€μ¦μš©) TEMP_DEPLOY="/tmp/quantengine_validate" mkdir -p "$TEMP_DEPLOY" tar -xzf quantengine.tar.gz -C "$TEMP_DEPLOY" # λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 검증 μ‹€ν–‰ chmod +x "$TEMP_DEPLOY/scripts/validate_migrations.sh" "$TEMP_DEPLOY/scripts/validate_migrations.sh" "$TEMP_DEPLOY" # 정리 rm -rf "$TEMP_DEPLOY" - name: Local Deploy (Green-Blue) id: deploy run: | set -e TIMESTAMP=$(date +%Y%m%d_%H%M%S) COMMIT=$(git rev-parse --short HEAD) RUN_NUM="${{ github.run_number }}" DEPLOY_BASE="/home/kjh2064/deployments" ACTIVE_LINK="/home/kjh2064/quantengine_active" # Version format: quantengine_YYYYMMDD_HHMMSS_COMMIT_HASH_RUNNUM TARGET_DIR="${DEPLOY_BASE}/quantengine_${TIMESTAMP}_${COMMIT}_${RUN_NUM}" TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}" [ -z "$TELEGRAM_BOT_TOKEN" ] && TELEGRAM_BOT_TOKEN="${{ env.TELEGRAM_BOT_TOKEN_DEFAULT }}" TELEGRAM_CHAT_ID="${{ secrets.TELEGRAM_CHAT_ID }}" [ -z "$TELEGRAM_CHAT_ID" ] && TELEGRAM_CHAT_ID="${{ env.TELEGRAM_CHAT_ID_DEFAULT }}" send_telegram() { local text="$1" curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${TELEGRAM_CHAT_ID}" \ --data-urlencode "text=${text}" \ -d "parse_mode=HTML" >/dev/null || true } echo "=== Deploying QuantEngine $COMMIT ($TIMESTAMP) ===" # 배포 디렉토리 생성 mkdir -p "${DEPLOY_BASE}" mkdir -p "${TARGET_DIR}" # 배포 νŒ¨ν‚€μ§€ μΆ”μΆœ echo "πŸ“ Extracting build artifact..." tar -xzf quantengine.tar.gz -C "${TARGET_DIR}" rm -f quantengine.tar.gz # 배포 ꡬ쑰 μ •κ·œν™” (net10.0 디렉토리가 있으면 μ΅œμƒμœ„λ‘œ 이동) if [ -d "${TARGET_DIR}/net10.0" ]; then echo "Restructuring deployment (moving net10.0 contents to root)..." mv "${TARGET_DIR}/net10.0"/* "${TARGET_DIR}/" rmdir "${TARGET_DIR}/net10.0" || true fi # ν™˜κ²½ 파일 μ„€μΉ˜ echo "βš™οΈ Installing environment configuration..." mkdir -p /home/kjh2064/.config install -m 600 ./deploy/quantengine.env /home/kjh2064/.config/quantengine.env # Green-Blue 배포 μ‹€ν–‰ echo "πŸš€ Executing Green-Blue Deployment..." export DEPLOY_FROM_CI=1 chmod +x "${TARGET_DIR}/deploy_gb.sh" "${TARGET_DIR}/deploy_gb.sh" # 이전 버전 정보 μ €μž₯ if [ -L "${ACTIVE_LINK}" ]; then PREV_VERSION=$(readlink -f "${ACTIVE_LINK}") PREV_TIMESTAMP=$(basename "${PREV_VERSION}") else PREV_TIMESTAMP="none" fi echo "timestamp=${TIMESTAMP}" >> $GITHUB_OUTPUT echo "commit=${COMMIT}" >> $GITHUB_OUTPUT echo "prev_version=${PREV_TIMESTAMP}" >> $GITHUB_OUTPUT - name: Health Check & Auto-Rollback run: | TIMESTAMP="${{ steps.deploy.outputs.timestamp }}" COMMIT="${{ steps.deploy.outputs.commit }}" PREV_TIMESTAMP="${{ steps.deploy.outputs.prev_version }}" DEPLOY_BASE="/home/kjh2064/deployments" ACTIVE_LINK="/home/kjh2064/quantengine_active" TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}" [ -z "$TELEGRAM_BOT_TOKEN" ] && TELEGRAM_BOT_TOKEN="${{ env.TELEGRAM_BOT_TOKEN_DEFAULT }}" TELEGRAM_CHAT_ID="${{ secrets.TELEGRAM_CHAT_ID }}" [ -z "$TELEGRAM_CHAT_ID" ] && TELEGRAM_CHAT_ID="${{ env.TELEGRAM_CHAT_ID_DEFAULT }}" send_telegram() { local text="$1" curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${TELEGRAM_CHAT_ID}" \ --data-urlencode "text=${text}" \ -d "parse_mode=HTML" >/dev/null || true } echo "=== Verifying Loopback Health ===" health_check_passed=0 for i in 1 2 3; do echo " Health check attempt $i..." loopback_headers=$(curl -s -D - -o /dev/null -m 5 http://127.0.0.1:5000/ 2>&1) if printf '%s' "$loopback_headers" | grep -qE '^HTTP/1\.[01] (200|30[12]|401) '; then echo "βœ“ Loopback health check passed (auth required)" health_check_passed=1 break elif [ $i -lt 3 ]; then echo " Waiting 5s for service..." sleep 5 fi done if [ $health_check_passed -eq 0 ]; then echo "❌ Loopback health check failed after 3 attempts" # μžλ™ λ‘€λ°± if [ "$PREV_TIMESTAMP" != "none" ]; then echo "πŸ”„ Attempting automatic rollback to $PREV_TIMESTAMP..." PREV_DEPLOY="${DEPLOY_BASE}/quantengine_${PREV_TIMESTAMP}" ln -sfn "${PREV_DEPLOY}" "${ACTIVE_LINK}" sudo systemctl restart quantengine sleep 3 echo "βœ“ Rollback completed" send_telegram "❌ QuantEngine 배포 μ‹€νŒ¨ (μžλ™ λ‘€λ°± μ‹€ν–‰) 컀밋: ${COMMIT} λ‘€λ°± 버전: ${PREV_TIMESTAMP} 둜그: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions/runs/${{ github.run_id }}" else echo "⚠️ No previous deployment found for rollback" send_telegram "❌ QuantEngine 배포 μ‹€νŒ¨ (λ‘€λ°± λΆˆκ°€) 컀밋: ${COMMIT} μƒνƒœ: 이전 버전이 μ—†μ–΄ λ‘€λ°± λΆˆκ°€ 둜그: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions/runs/${{ github.run_id }}" fi exit 1 fi echo "=== Verifying Database Connectivity ===" db_status=$(psql -U quantengine_app -d quantenginedb -h 127.0.0.1 -c 'SELECT 1;' 2>&1 | head -1) if echo "$db_status" | grep -q "1"; then echo "βœ“ Database connectivity verified" else echo "⚠️ Database connectivity check: $db_status" fi echo "=== Verifying Public Routes ===" public_root_code=$(curl -s -o /dev/null -w "%{http_code}" "https://quant.taxbaik.com/") login_code=$(curl -s -o /dev/null -w "%{http_code}" "https://quant.taxbaik.com/Account/Login") echo "https://quant.taxbaik.com/ -> ${public_root_code}" echo "https://quant.taxbaik.com/Account/Login -> ${login_code}" if [ "$public_root_code" != "302" ] && [ "$public_root_code" != "200" ] && [ "$public_root_code" != "401" ]; then echo "⚠️ Unexpected public root response: $public_root_code" fi if [ "$login_code" != "200" ] && [ "$login_code" != "302" ]; then echo "⚠️ Unexpected login page response: $login_code" fi echo "=== Verifying Nginx Configuration ===" NGINX_CONF="" for f in /etc/nginx/sites-enabled/*; do if [ -e "$f" ] && grep -q "location /quantengine" "$f" 2>/dev/null; then NGINX_CONF="$f" break fi done if [ -n "$NGINX_CONF" ]; then echo "βœ“ Nginx configuration found: $NGINX_CONF" if nginx -t > /dev/null 2>&1; then echo "βœ“ Nginx syntax validated" else echo "⚠️ Nginx syntax check failed (service may still work)" fi else echo "⚠️ Nginx configuration not found" echo " Expected: /etc/nginx/sites-enabled/* with 'location /quantengine'" fi echo "βœ“ 배포 μ™„λ£Œ: quantengine_${TIMESTAMP}" send_telegram "βœ… QuantEngine 배포 μ™„λ£Œ (Green-Blue) 컀밋: ${COMMIT} μ‹œκ°„: ${TIMESTAMP} λŒ€μƒ: ${DEPLOY_HOST}" - name: Cleanup Old Deployments run: | DEPLOY_BASE="/home/kjh2064/deployments" echo "Cleaning up obsolete deployments (keeping last 5)..." cd "${DEPLOY_BASE}" ls -dt quantengine_* | tail -n +6 | while read -r old_dir; do echo "Removing old release: ${old_dir}" rm -rf "${old_dir}" done echo "Cleanup complete" ls -ldt quantengine_* | head -5 - name: Notify Failure if: failure() run: | COMMIT=$(git rev-parse --short HEAD) TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}" [ -z "$TELEGRAM_BOT_TOKEN" ] && TELEGRAM_BOT_TOKEN="${{ env.TELEGRAM_BOT_TOKEN_DEFAULT }}" TELEGRAM_CHAT_ID="${{ secrets.TELEGRAM_CHAT_ID }}" [ -z "$TELEGRAM_CHAT_ID" ] && TELEGRAM_CHAT_ID="${{ env.TELEGRAM_CHAT_ID_DEFAULT }}" curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${TELEGRAM_CHAT_ID}" \ --data-urlencode "text=❌ QuantEngine 배포 μ‹€νŒ¨\n컀밋: ${COMMIT}\n둜그: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions/runs/${{ github.run_id }}" \ -d "parse_mode=HTML" || true