diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml
index fcf9671..9aecde9 100644
--- a/.gitea/workflows/deploy-prod.yml
+++ b/.gitea/workflows/deploy-prod.yml
@@ -7,18 +7,16 @@ on:
env:
DEPLOY_HOST: 172.17.0.1
- # NOTE: Gitea와 운영서버가 같은 호스트에 있음 (hz-prod-01)
- # 구조: 공인 IP 178.104.200.7/quant → Nginx reverse proxy → localhost:5000 (quantengine)
- # 배포: .NET DLL을 /home/kjh2064/quantengine_active에 배포
- # Nginx 설정: /etc/nginx/sites-available/gitea-ip.conf (이미 구성됨)
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-test:
- name: Build Release Package
+ build-and-deploy:
+ name: Build & Deploy to Production
runs-on: ubuntu-latest
steps:
@@ -42,12 +40,19 @@ jobs:
- name: "[GATE] Run Core Validations"
run: |
- # CI 게이트: 핵심 검증 먼저 실행
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
@@ -64,7 +69,6 @@ jobs:
dotnet test tests/unit \
-c Release \
--no-build \
- --logger "trx;LogFileName=test-results.trx" \
|| echo "⚠️ Some tests failed (non-blocking for web service)"
fi
@@ -75,329 +79,107 @@ jobs:
--no-build \
-o ./publish-output
- echo "📦 Package size:"
- du -sh ./publish-output
-
- - name: Create Deployment Archive
- run: |
- cd publish-output
- tar -czf ../quant-engine-release-${{ github.run_number }}.tar.gz .
- cd ..
- ls -lh quant-engine-release-${{ github.run_number }}.tar.gz
-
- - name: Upload Artifact
- uses: actions/upload-artifact@v3
- with:
- name: quant-engine-release
- path: quant-engine-release-${{ github.run_number }}.tar.gz
- retention-days: 30
-
- deploy-to-prod:
- name: Deploy to Production Server
- needs: build-and-test
- runs-on: ubuntu-latest
- if: github.ref == 'refs/heads/main' && github.event_name == 'push'
-
- steps:
- - name: Checkout Code
- uses: actions/checkout@v3
-
- - name: Download Artifact
- uses: actions/download-artifact@v3
- with:
- name: quant-engine-release
-
- name: Setup SSH
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
- echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
+ # 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: Create Backup
+ - name: Package Artifact
run: |
- echo "📦 Creating backup of current deployment..."
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF'
- set -e
- BACKUP_DIR="/home/kjh2064/quantengine_backup"
- BACKUP_NAME="quantengine_$(date +%Y%m%d_%H%M%S)"
+ tar -czf quant_engine_deploy.tgz -C ./publish-output .
+ echo "✓ Package size: $(du -sh quant_engine_deploy.tgz | cut -f1)"
- # Create backup without stopping the service (minimize downtime)
- mkdir -p $BACKUP_DIR
- if [ -d ${{ env.DEPLOY_PATH }} ]; then
- cp -r ${{ env.DEPLOY_PATH }} "$BACKUP_DIR/$BACKUP_NAME"
- echo "✅ Backup created: $BACKUP_DIR/$BACKUP_NAME"
+ - 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 }}"
- # Keep only last 5 backups
- BACKUP_COUNT=$(ls -1 $BACKUP_DIR | wc -l)
- if [ "$BACKUP_COUNT" -gt 5 ]; then
- OLD_BACKUPS=$(ls -1t $BACKUP_DIR | tail -n +6)
- for backup in $OLD_BACKUPS; do
- rm -rf "$BACKUP_DIR/$backup"
- done
- echo "🧹 Old backups cleaned"
- fi
- else
- echo "⚠️ No existing deployment found"
+ 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
- EOF
-
- - name: Deploy Package
- run: |
- echo "📤 Deploying package to production..."
-
- ARCHIVE_NAME=$(ls -1 quant-engine-release-*.tar.gz | head -1)
-
- # Create temporary directory on remote
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
- "mkdir -p /tmp/quant-deploy && chmod 777 /tmp/quant-deploy"
-
- # Transfer archive
- scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 "$ARCHIVE_NAME" \
- ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}:/tmp/quant-deploy/
-
- echo "✅ Package transferred"
-
- - name: Extract and Install
- run: |
- echo "📦 Extracting and installing..."
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF'
- set -e
-
- DEPLOY_PATH="${{ env.DEPLOY_PATH }}"
- ARCHIVE_NAME=$(ls -1 /tmp/quant-deploy/quant-engine-release-*.tar.gz | head -1)
-
- # Create deployment directory
- mkdir -p "$DEPLOY_PATH"
-
- # Extract new package
- tar -xzf "$ARCHIVE_NAME" -C "$DEPLOY_PATH"
- echo "✅ Package extracted to $DEPLOY_PATH"
-
- # Verify key files
- if [ -f "$DEPLOY_PATH/QuantEngine.Web.dll" ]; then
- echo "✅ QuantEngine.Web.dll verified"
- else
- echo "❌ QuantEngine.Web.dll not found!"
+ 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
-
- # Cleanup temp
- rm -rf /tmp/quant-deploy
- EOF
-
- - name: Restart Service
- run: |
- echo "🔄 Restarting quantengine service to apply changes (Downtime minimal)..."
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF'
- set -e
-
- # Restart service
- echo "⏹️ Restarting quantengine service..."
- sudo systemctl restart ${{ env.SERVICE_NAME }}
+ echo " 대기 중... (\\\$i/\\\$ATTEMPTS, HTTP \\\$STATUS)"
sleep 3
-
- # Check status
- if sudo systemctl is-active --quiet ${{ env.SERVICE_NAME }}; then
- echo "✅ ${{ env.SERVICE_NAME }} restarted successfully"
- sudo systemctl status ${{ env.SERVICE_NAME }} | head -5
- else
- echo "❌ ${{ env.SERVICE_NAME }} failed to start"
- sudo systemctl status ${{ env.SERVICE_NAME }}
- exit 1
- fi
- EOF
-
- - name: Health Check
- run: |
- echo "🧪 Running health checks on remote host..."
-
- # Wait for service to be ready (localhost:5000/quant/ through Kestrel inside remote host)
- for i in {1..30}; do
- HTTP_CODE=$(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
- 'curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5000/quant/' || echo "000")
-
- if [ "$HTTP_CODE" = "200" ]; then
- echo "✅ Health check passed (HTTP $HTTP_CODE inside remote host)"
- break
- fi
-
- echo "⏳ Waiting for service... (attempt $i/30, HTTP $HTTP_CODE)"
- sleep 2
done
+ REMOTE
- if [ "$HTTP_CODE" != "200" ]; then
- echo "❌ Health check failed after 60 seconds"
- echo "Service logs:"
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \
- "sudo journalctl -u ${{ env.SERVICE_NAME }} -n 20" || true
- exit 1
- fi
-
- - name: Verify Deployment
- run: |
- echo "📊 Verifying deployment..."
-
- # Check MudBlazor is loaded (via public IP)
- PUBLIC_IP="178.104.200.7"
- MUDBLAZOR_CHECK=$(curl -s "http://$PUBLIC_IP/quant/" | grep -c "MudBlazor" || echo "0")
-
- if [ "$MUDBLAZOR_CHECK" -gt "0" ]; then
- echo "✅ MudBlazor UI loaded successfully"
- else
- echo "⚠️ MudBlazor might not be loaded correctly"
- fi
-
- # Get page title
- PAGE_TITLE=$(curl -s "http://$PUBLIC_IP/quant/" | grep -o "
${COMMIT}
+ 시간: ${TIMESTAMP}
+ 대상: ${DEPLOY_HOST}"
diff --git a/.gitea/workflows/snapshot_admin_deploy.yml b/.gitea/workflows/snapshot_admin_deploy.yml
index 98dfa0d..b6cc790 100644
--- a/.gitea/workflows/snapshot_admin_deploy.yml
+++ b/.gitea/workflows/snapshot_admin_deploy.yml
@@ -10,6 +10,12 @@ concurrency:
group: snapshot-admin-deploy-main
cancel-in-progress: true
+env:
+ DEPLOY_HOST: 178.104.200.7
+ DEPLOY_USER: kjh2064
+ TELEGRAM_BOT_TOKEN_DEFAULT: "8734507814:AAFyacLMai8GB4K-hQ_Nd3t3D01A-h1ZdV0"
+ TELEGRAM_CHAT_ID_DEFAULT: "-5460205872"
+
jobs:
build-and-deploy:
runs-on: ubuntu-latest
@@ -33,38 +39,84 @@ jobs:
echo "[deploy] compressing publish output"
tar -czf quantengine.tar.gz -C ./publish .
- - name: Deploy to Host via Local SSH
- env:
- SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
+ - name: Setup SSH
run: |
- echo "[deploy] setting up SSH and deploying shadow copy"
mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
+ 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
- # Upload artifact and deploy script to host
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 kjh2064@178.104.200.7 "mkdir -p /home/kjh2064/tmp"
- scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 quantengine.tar.gz kjh2064@178.104.200.7:/home/kjh2064/tmp/quantengine.tar.gz
-
- # Execute hot deploy script
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 kjh2064@178.104.200.7 "chmod +x /home/kjh2064/tmp/deploy.sh 2>/dev/null || true"
- scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 tools/deploy_quantengine.sh kjh2064@178.104.200.7:/home/kjh2064/tmp/deploy.sh
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 kjh2064@178.104.200.7 "chmod +x /home/kjh2064/tmp/deploy.sh && /home/kjh2064/tmp/deploy.sh"
-
- - name: Verify Public Routes
+ - name: Deploy & Verify on Server
run: |
set -e
- root_html=$(curl -s "http://178.104.200.7/quant/")
- ops_html=$(curl -s "http://178.104.200.7/quant/operations")
+ TIMESTAMP=$(date +%Y%m%d_%H%M%S)
+ COMMIT=$(git rev-parse --short HEAD)
+ DEPLOY_HOST="${{ env.DEPLOY_HOST }}"
+ DEPLOY_USER="${{ env.DEPLOY_USER }}"
+
+ 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 "❌ Snapshot Admin 배포 실패
+
+ 커밋: ${COMMIT}
+ 시간: ${TIMESTAMP}
+ 단계: snapshot_admin_deploy (Deploy Execution)"
+ exit "$exit_code"
+ }
+
+ trap notify_failure ERR
+
+ echo "=== Deploying Snapshot Admin $COMMIT ($TIMESTAMP) ==="
+
+ # 1. 원격지 임시 폴더 생성 및 업로드
+ 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"
+
+ # 2. 배포 스크립트 실행
+ ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 "$DEPLOY_USER@$DEPLOY_HOST" "chmod +x /home/kjh2064/tmp/deploy.sh && /home/kjh2064/tmp/deploy.sh"
+
+ # 3. 배포 성공 검증
+ echo "=== Verifying Public Routes ==="
+ root_html=$(curl -sf "http://${DEPLOY_HOST}/quant/" 2>/dev/null || echo "")
+ ops_html=$(curl -sf "http://${DEPLOY_HOST}/quant/operations" 2>/dev/null || echo "")
+
root_code=$(printf '%s' "$root_html" | grep -q "Quant Engine" && echo 200 || echo 500)
ops_code=$(printf '%s' "$ops_html" | grep -q "Operational Report" && echo 200 || echo 500)
+
echo "/quant/ -> ${root_code}"
echo "/quant/operations -> ${ops_code}"
+
if [ "$root_code" != "200" ]; then
- echo "Deployment content check failed for /quant/"
+ echo "Deployment content check failed for /quant/" >&2
exit 1
fi
if [ "$ops_code" != "200" ]; then
- echo "Deployment content check failed for /quant/operations"
+ echo "Deployment content check failed for /quant/operations" >&2
exit 1
fi
+
+ echo "✓ 배포 완료: quantengine_${TIMESTAMP} @ $DEPLOY_HOST"
+ send_telegram "✅ Snapshot Admin 배포 완료
+
+ 커밋: ${COMMIT}
+ 시간: ${TIMESTAMP}
+ 대상: ${DEPLOY_HOST}"