name: Deploy to Production on: push: branches: [ main ] workflow_dispatch: env: DEPLOY_HOST: 172.17.0.1 DEPLOY_USER: kjh2064 DEPLOY_PATH: /home/kjh2064/quantengine_active SERVICE_NAME: quantengine DOTNET_VERSION: '10.0.x' 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 steps: - name: Checkout Code uses: actions/checkout@v3 with: fetch-depth: 0 - 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 # ๋นˆ ํŒจํ‚ท ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ dotnet test/run ์‹œ IO Exception ๋ฐฉ์–ด 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 \ -p:Version=1.0.${{ github.run_number }} - name: Run Unit Tests run: | if [ -d tests/unit ]; then dotnet test tests/unit \ -c Release \ --no-build \ || echo "โš ๏ธ Some tests failed (non-blocking for web service)" fi - name: Publish Release Package run: | dotnet publish src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \ -c Release \ --no-build \ -o ./publish-output - 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-output/wwwroot printf '{\n "version": "1.0.%s-%s",\n "built": "%s"\n}\n' "${{ github.run_number }}" "$COMMIT_HASH" "$BUILD_TIME" > ./publish-output/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 # SSH_PRIVATE_KEY๊ฐ€ ํ‰๋ฌธ PEM์ด๋“  base64๋“  ์œ ์—ฐํ•˜๊ฒŒ ์ฒ˜๋ฆฌ 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: Package Artifact run: | tar -czf quant_engine_deploy.tgz -C ./publish-output . echo "โœ“ Package size: $(du -sh quant_engine_deploy.tgz | cut -f1)" - name: Deploy & Verify on Server run: | set -e TIMESTAMP=$(date +%Y%m%d_%H%M%S) COMMIT=$(git rev-parse --short HEAD) DEPLOY_HOST="${{ env.DEPLOY_HOST }}" DEPLOY_USER="${{ env.DEPLOY_USER }}" # ํ…”๋ ˆ๊ทธ๋žจ ์„ค์ • ๋ฐ”์ธ๋”ฉ (Secret์— ์—†์„ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’ ๋ฐฑ์—… ์‚ฌ์šฉ) 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 } 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) ===" # 1. ์•„ํ‹ฐํŒฉํŠธ ๋ณต์‚ฌ scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ quant_engine_deploy.tgz "$DEPLOY_USER@$DEPLOY_HOST:/tmp/quantengine_${TIMESTAMP}.tgz" # 2. ์›๊ฒฉ ๋ฐฐํฌ ๋ช…๋ น์–ด ํ†ตํ•ฉ (SSH 1ํšŒ ์—ฐ๊ฒฐ) ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 \ -o ServerAliveInterval=10 \ "$DEPLOY_USER@$DEPLOY_HOST" bash << REMOTE set -e DEPLOY_HOME="/home/kjh2064" DEPLOY_DIR="\$DEPLOY_HOME/deployments/quantengine_${TIMESTAMP}" echo "--- [1/4] ์••์ถ• ํ•ด์ œ ---" mkdir -p "\$DEPLOY_DIR" tar -xzf "/tmp/quantengine_${TIMESTAMP}.tgz" -C "\$DEPLOY_DIR" rm -f "/tmp/quantengine_${TIMESTAMP}.tgz" echo "--- [2/4] ์‹ฌ๋ณผ๋ฆญ ๋งํฌ ์ „ํ™˜ ---" ln -sfn "\$DEPLOY_DIR" "${{ env.DEPLOY_PATH }}" echo "--- [3/4] ์„œ๋น„์Šค ์žฌ์‹œ์ž‘ ---" sudo /usr/bin/systemctl restart ${{ env.SERVICE_NAME }} echo "--- [4/4] ํ—ฌ์Šค ์ฒดํฌ ---" ATTEMPTS=20 for i in \$(seq 1 \$ATTEMPTS); do STATUS=\$(curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:5000/ 2>/dev/null || echo "000") if [ "\$STATUS" = "200" ]; then echo "โœ“ ํ—ฌ์Šค์ฒดํฌ ์„ฑ๊ณต (์‹œ๋„ \$i/\$ATTEMPTS, HTTP 200)" # ๊ตฌ ๋ฐฐํฌ ํด๋” ์ •๋ฆฌ (์ตœ๊ทผ 5๊ฐœ๋งŒ ๋ณด์กด) ls -1dt \$DEPLOY_HOME/deployments/quantengine_* 2>/dev/null | tail -n +6 | xargs rm -rf 2>/dev/null || true exit 0 fi if [ "\$i" -eq "\$ATTEMPTS" ]; then echo "=== FATAL: ์„œ๋น„์Šค๊ฐ€ ํ—ฌ์Šค์ฒดํฌ ์‘๋‹ต์„ ํ•˜์ง€ ์•Š์Œ ===" >&2 systemctl is-active ${{ env.SERVICE_NAME }} >&2 || true journalctl -u ${{ env.SERVICE_NAME }} --no-pager -n 50 >&2 exit 1 fi echo " ๋Œ€๊ธฐ ์ค‘... (\$i/\$ATTEMPTS, HTTP \$STATUS)" sleep 3 done REMOTE echo "โœ“ ๋ฐฐํฌ ์™„๋ฃŒ: quantengine_${TIMESTAMP} @ $DEPLOY_HOST" send_telegram "โœ… QuantEngine ๋ฐฐํฌ ์™„๋ฃŒ ์ปค๋ฐ‹: ${COMMIT} ์‹œ๊ฐ„: ${TIMESTAMP} ๋Œ€์ƒ: ${DEPLOY_HOST}"