refactor(deploy): TaxBaik 성공 사례(run 458) 기반 단일 빌드/배포 파이프라인 개편 및 텔레그램 연동 강화
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Snapshot Admin Deployment / build-and-deploy (push) Failing after 44s
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 7s
Deploy to Production / Build & Deploy to Production (push) Failing after 1m33s

This commit is contained in:
2026-06-29 12:26:13 +09:00
parent 79f4a45b98
commit 704a168cda
2 changed files with 171 additions and 337 deletions
+100 -318
View File
@@ -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 "❌ <b>QuantEngine 배포 실패</b>
커밋: <code>${COMMIT}</code>
시간: <code>${TIMESTAMP}</code>
단계: 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 "<title>.*</title>" | head -1)
echo "📄 Page title: $PAGE_TITLE"
- name: Generate Deployment Report
if: always()
run: |
cat > deployment-report.txt << EOF
═══════════════════════════════════════════════════════
Quant Engine v9 Deployment Report
═══════════════════════════════════════════════════════
Deployment Date: $(date -u '+%Y-%m-%d %H:%M:%S UTC')
Run Number: ${{ github.run_number }}
Commit: ${{ github.sha }}
Branch: ${{ github.ref }}
🎯 Target Environment
Server: hz-prod-01
Internal IP: ${{ env.DEPLOY_HOST }}
Public IP: 178.104.200.7
Deploy Path: ${{ env.DEPLOY_PATH }}
Service: ${{ env.SERVICE_NAME }}
📊 Deployment Status: COMPLETED
✅ Release Build: Successful
✅ Package Created: 24MB+
✅ Backup Created: /home/kjh2064/quantengine_backup/
✅ Package Deployed: ${{ env.DEPLOY_PATH }}
✅ Service Started: ${{ env.SERVICE_NAME }}
✅ Health Check: PASS (localhost:5000)
✅ MudBlazor UI: Verified via public IP
🌐 Access Information
Public URL: http://178.104.200.7/quant/
Service Port: 127.0.0.1:5000
Nginx Config: /etc/nginx/sites-available/gitea-ip.conf
📝 Service Architecture
- Nginx (reverse proxy) listens on port 80/443
- /quant/ path → localhost:5000 (quantengine service)
- quantengine runs as user kjh2064
- WorkingDirectory: /home/kjh2064/quantengine_active
🔍 Monitoring & Logs
- Service: sudo systemctl status ${{ env.SERVICE_NAME }}
- Logs: sudo journalctl -u ${{ env.SERVICE_NAME }} -f
- Nginx: sudo tail -f /var/log/nginx/error.log
- Deployment Log: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
🔄 Rollback Command (if needed):
ssh kjh2064@${{ env.DEPLOY_HOST }} 'LATEST=\$(ls -t /home/kjh2064/quantengine_backup | head -1); cp -r /home/kjh2064/quantengine_backup/\$LATEST/* /home/kjh2064/quantengine_active/ && sudo systemctl restart ${{ env.SERVICE_NAME }}'
═══════════════════════════════════════════════════════
EOF
cat deployment-report.txt
- name: Upload Deployment Report
uses: actions/upload-artifact@v3
if: always()
with:
name: deployment-report
path: deployment-report.txt
retention-days: 90
- name: Notify Telegram
if: always()
run: |
STATUS=${{ job.status }}
if [ "$STATUS" = "success" ]; then
EMOJI="✅"
TEXT="*Quant Engine v9 Deployment SUCCESS* $EMOJI%0A• Run: #${{ github.run_number }}%0A• Commit: ${{ github.sha }}%0A• Service: ${{ env.SERVICE_NAME }}%0A• URL: http://178.104.200.7/quant/"
else
EMOJI="❌"
TEXT="*Quant Engine v9 Deployment FAILED* $EMOJI%0A• Run: #${{ github.run_number }}%0A• Commit: ${{ github.sha }}%0A• Service: ${{ env.SERVICE_NAME }}%0A• URL: http://178.104.200.7/quant/"
fi
curl -s -X POST "https://api.telegram.org/bot8734507814:AAFyacLMai8GB4K-hQ_Nd3t3D01A-h1ZdV0/sendMessage" \
-d "chat_id=-5460205872" \
-d "text=$TEXT" \
-d "parse_mode=Markdown"
post-deployment:
name: Post-Deployment Checks
needs: deploy-to-prod
runs-on: ubuntu-latest
if: success()
steps:
- name: Performance Baseline
run: |
echo "📈 Collecting performance metrics via Public IP..."
# Page load time
START=$(date +%s%N)
curl -s http://178.104.200.7/quant/ > /dev/null
END=$(date +%s%N)
LOAD_TIME=$(( (END - START) / 1000000 ))
echo "⏱️ Page load time: ${LOAD_TIME}ms"
if [ $LOAD_TIME -lt 2000 ]; then
echo "✅ Load time acceptable (< 2s)"
else
echo "⚠️ Load time slightly slow (> 2s), but acceptable"
fi
- name: Create Deployment Checklist
run: |
cat > deployment-checklist.txt << 'EOF'
✅ Quant Engine v9 Deployment Complete
Web Service:
[✓] Release build successful (24MB)
[✓] Deployed to: http://178.104.200.7/quant/
[✓] nginx restarted
[✓] Health check: HTTP 200 OK
[✓] MudBlazor UI verified
[✓] Page load time: < 2s
Backup & Recovery:
[✓] Backup created: /var/www/quant_backup/
[✓] 5 previous backups retained
[✓] Rollback ready
Next Steps:
[ ] Monitor nginx logs: ssh kjh2064@178.104.200.7 'sudo tail -f /var/log/nginx/error.log'
[ ] Check dashboard: http://178.104.200.7/quant/
[ ] Verify all components loaded
[ ] Test responsive design (mobile/tablet)
[ ] Monitor performance metrics
GAS Deployment (Manual):
[ ] Deploy gas_data_feed.gs to Google Apps Script
[ ] Deploy live_outcome_ledger.gs
[ ] Test signal tracking
Documentation:
[ ] DEPLOYMENT_GUIDE.md
[ ] DEPLOYMENT_STEPS.md
[ ] UI_COMPLETENESS_REPORT.md
[ ] V9_HARDENING_IMPLEMENTATION_ROADMAP.md
EOF
cat deployment-checklist.txt
- name: Upload Checklist
uses: actions/upload-artifact@v3
with:
name: post-deployment-checklist
path: deployment-checklist.txt
retention-days: 30
echo "✓ 배포 완료: quantengine_${TIMESTAMP} @ $DEPLOY_HOST"
send_telegram "✅ <b>QuantEngine 배포 완료</b>
커밋: <code>${COMMIT}</code>
시간: <code>${TIMESTAMP}</code>
대상: <code>${DEPLOY_HOST}</code>"