name: Deploy to Production on: push: branches: - main workflow_dispatch: concurrency: group: deploy-prod-main cancel-in-progress: true env: DEPLOY_HOST: 178.104.200.7 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: 15 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: Setup SSH run: | mkdir -p ~/.ssh chmod 700 ~/.ssh if echo "${{ secrets.SSH_PRIVATE_KEY }}" | grep -q "BEGIN"; then echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 else echo "${{ secrets.SSH_PRIVATE_KEY }}" | base64 -d > ~/.ssh/id_ed25519 || echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 fi chmod 600 ~/.ssh/id_ed25519 ssh-keyscan -H ${{ env.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true - name: Prepare QuantEngine DB Env run: | mkdir -p ./deploy cat > ./deploy/quantengine.env </dev/null || true } notify_failure() { local exit_code=$? send_telegram "❌ QuantEngine 배포 실패 커밋: ${COMMIT} 시간: ${TIMESTAMP} 단계: deploy-to-prod (SSH Execution)" exit "$exit_code" } trap notify_failure ERR echo "=== Deploying QuantEngine $COMMIT ($TIMESTAMP) ===" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p /home/kjh2064/tmp" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ quantengine.tar.gz "$DEPLOY_USER@$DEPLOY_HOST:/home/kjh2064/tmp/quantengine.tar.gz" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ tools/deploy_quantengine.sh "$DEPLOY_USER@$DEPLOY_HOST:/home/kjh2064/tmp/deploy.sh" scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ deploy/quantengine.env "$DEPLOY_USER@$DEPLOY_HOST:/home/kjh2064/tmp/quantengine.env" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ "$DEPLOY_USER@$DEPLOY_HOST" "chmod +x /home/kjh2064/tmp/deploy.sh && CI_DEPLOY=1 /home/kjh2064/tmp/deploy.sh" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p /home/kjh2064/.config && install -m 600 /home/kjh2064/tmp/quantengine.env /home/kjh2064/.config/quantengine.env && rm -f /home/kjh2064/tmp/quantengine.env" echo "=== Verifying Loopback Health ===" loopback_headers=$(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 "$DEPLOY_USER@$DEPLOY_HOST" "curl -s -D - -o /dev/null http://127.0.0.1:5000/") echo "$loopback_headers" if ! printf '%s' "$loopback_headers" | grep -qE '^HTTP/1\.1 30[12] '; then echo "Loopback health check failed for quantengine" >&2 exit 1 fi if ! printf '%s' "$loopback_headers" | grep -qiE '^Location: /not-found'; then echo "Loopback redirect target is unexpected" >&2 exit 1 fi echo "=== Verifying Favicon Assets ===" favicon_svg_code=$(curl -s -o /dev/null -w "%{http_code}" "http://${DEPLOY_HOST}/favicon.svg") favicon_png_code=$(curl -s -o /dev/null -w "%{http_code}" "http://${DEPLOY_HOST}/favicon.png") echo "/favicon.svg -> ${favicon_svg_code}" echo "/favicon.png -> ${favicon_png_code}" if [ "$favicon_svg_code" != "200" ] && [ "$favicon_png_code" != "200" ]; then echo "Favicon assets are not reachable after deploy" >&2 exit 1 fi echo "=== Verifying Public Routes ===" root_headers=$(curl -s -D - -o /dev/null "http://${DEPLOY_HOST}/") ops_headers=$(curl -s -D - -o /dev/null "http://${DEPLOY_HOST}/operations") root_code=$(printf '%s' "$root_headers" | awk 'NR==1 {print $2}') ops_code=$(printf '%s' "$ops_headers" | awk 'NR==1 {print $2}') echo "/ -> ${root_code}" echo "/operations -> ${ops_code}" if [ "$root_code" != "302" ]; then echo "Deployment content check failed for /" >&2 exit 1 fi if [ "$ops_code" != "302" ]; then echo "Deployment content check failed for /operations" >&2 exit 1 fi echo "✓ 배포 완료: quantengine_${TIMESTAMP} @ $DEPLOY_HOST" send_telegram "✅ QuantEngine 배포 완료 커밋: ${COMMIT} 시간: ${TIMESTAMP} 대상: ${DEPLOY_HOST}"