chore(ci): consolidate production deploy workflow
This commit is contained in:
+151
-154
@@ -2,193 +2,190 @@ name: Deploy to Production
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches:
|
||||||
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-prod-main
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEPLOY_HOST: 172.17.0.1
|
DEPLOY_HOST: 178.104.200.7
|
||||||
DEPLOY_USER: kjh2064
|
DEPLOY_USER: kjh2064
|
||||||
DEPLOY_PATH: /home/kjh2064/quantengine_active
|
|
||||||
SERVICE_NAME: quantengine
|
SERVICE_NAME: quantengine
|
||||||
DOTNET_VERSION: '10.0.x'
|
DOTNET_VERSION: '10.0.x'
|
||||||
TELEGRAM_BOT_TOKEN_DEFAULT: "8734507814:AAFyacLMai8GB4K-hQ_Nd3t3D01A-h1ZdV0"
|
TELEGRAM_BOT_TOKEN_DEFAULT: "8734507814:AAFyacLMai8GB4K-hQ_Nd3t3D01A-H1ZdV0"
|
||||||
TELEGRAM_CHAT_ID_DEFAULT: "-5460205872"
|
TELEGRAM_CHAT_ID_DEFAULT: "-5460205872"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
name: Build & Deploy to Production
|
name: Build & Deploy to Production
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v3
|
||||||
with:
|
with:
|
||||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
|
|
||||||
- name: Install Python Dependencies
|
- name: Install Python Dependencies
|
||||||
run: pip install pyyaml openpyxl requests
|
run: pip install pyyaml openpyxl requests
|
||||||
|
|
||||||
- name: "[GATE] Run Core Validations"
|
- name: "[GATE] Run Core Validations"
|
||||||
run: |
|
run: |
|
||||||
echo "🔐 Running critical CI validations..."
|
echo "🔐 Running critical CI validations..."
|
||||||
python3 tools/validate_no_direct_api_trading_v1.py || exit 1
|
python3 tools/validate_no_direct_api_trading_v1.py || exit 1
|
||||||
python3 tools/validate_specs.py || exit 1
|
python3 tools/validate_specs.py || exit 1
|
||||||
echo "✅ All critical validations passed"
|
echo "✅ All critical validations passed"
|
||||||
|
|
||||||
- name: Ensure Temp Directory and Mock Packet
|
- name: Ensure Temp Directory and Mock Packet
|
||||||
run: |
|
run: |
|
||||||
mkdir -p Temp
|
mkdir -p Temp
|
||||||
# 빈 패킷 객체를 생성하여 dotnet test/run 시 IO Exception 방어
|
if [ ! -f Temp/final_decision_packet_active.json ]; then
|
||||||
if [ ! -f Temp/final_decision_packet_active.json ]; then
|
echo '{"active_decision": "PASS", "details": "CI dummy packet"}' > Temp/final_decision_packet_active.json
|
||||||
echo '{"active_decision": "PASS", "details": "CI dummy packet"}' > Temp/final_decision_packet_active.json
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Restore Dependencies
|
- name: Restore Dependencies
|
||||||
run: dotnet restore src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj
|
run: dotnet restore src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
run: |
|
run: |
|
||||||
dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \
|
dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \
|
||||||
-c Release \
|
-c Release \
|
||||||
--no-restore \
|
--no-restore
|
||||||
-p:Version=1.0.${{ github.run_number }}
|
|
||||||
|
|
||||||
- name: Run Unit Tests
|
- name: Run Unit Tests
|
||||||
run: |
|
run: |
|
||||||
if [ -d tests/unit ]; then
|
dotnet test src/dotnet/QuantEngine.Core.Tests/QuantEngine.Core.Tests.csproj \
|
||||||
dotnet test tests/unit \
|
-c Release \
|
||||||
|
--no-build
|
||||||
|
|
||||||
|
- name: Publish Release Package
|
||||||
|
run: |
|
||||||
|
dotnet publish src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \
|
||||||
-c Release \
|
-c Release \
|
||||||
--no-build \
|
--no-build \
|
||||||
|| echo "⚠️ Some tests failed (non-blocking for web service)"
|
-o ./publish
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Publish Release Package
|
- name: Generate Build Info
|
||||||
run: |
|
run: |
|
||||||
dotnet publish src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \
|
COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||||
-c Release \
|
BUILD_TIME=$(date -d "+9 hours" +'%Y-%m-%d %H:%M:%S KST')
|
||||||
--no-build \
|
mkdir -p ./publish/wwwroot
|
||||||
-o ./publish-output
|
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: Generate Build Info
|
- name: Setup SSH
|
||||||
run: |
|
run: |
|
||||||
COMMIT_HASH=$(git rev-parse --short HEAD)
|
mkdir -p ~/.ssh
|
||||||
BUILD_TIME=$(date -d "+9 hours" +'%Y-%m-%d %H:%M:%S KST')
|
chmod 700 ~/.ssh
|
||||||
mkdir -p ./publish-output/wwwroot
|
if echo "${{ secrets.SSH_PRIVATE_KEY }}" | grep -q "BEGIN"; then
|
||||||
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 "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||||
echo "✓ Generated version info: 1.0.${{ github.run_number }}-$COMMIT_HASH @ $BUILD_TIME"
|
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 quantengine.tar.gz -C ./publish .
|
||||||
|
echo "✓ Package size: $(du -sh quantengine.tar.gz | cut -f1)"
|
||||||
|
|
||||||
- name: Setup SSH
|
- name: Deploy & Verify on Server
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
set -e
|
||||||
chmod 700 ~/.ssh
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
# SSH_PRIVATE_KEY가 평문 PEM이든 base64든 유연하게 처리
|
COMMIT=$(git rev-parse --short HEAD)
|
||||||
if echo "${{ secrets.SSH_PRIVATE_KEY }}" | grep -q "BEGIN"; then
|
DEPLOY_HOST="${{ env.DEPLOY_HOST }}"
|
||||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
DEPLOY_USER="${{ env.DEPLOY_USER }}"
|
||||||
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
|
TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}"
|
||||||
run: |
|
[ -z "$TELEGRAM_BOT_TOKEN" ] && TELEGRAM_BOT_TOKEN="${{ env.TELEGRAM_BOT_TOKEN_DEFAULT }}"
|
||||||
tar -czf quant_engine_deploy.tgz -C ./publish-output .
|
TELEGRAM_CHAT_ID="${{ secrets.TELEGRAM_CHAT_ID }}"
|
||||||
echo "✓ Package size: $(du -sh quant_engine_deploy.tgz | cut -f1)"
|
[ -z "$TELEGRAM_CHAT_ID" ] && TELEGRAM_CHAT_ID="${{ env.TELEGRAM_CHAT_ID_DEFAULT }}"
|
||||||
|
|
||||||
- name: Deploy & Verify on Server
|
send_telegram() {
|
||||||
run: |
|
local text="$1"
|
||||||
set -e
|
curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
-d "chat_id=${TELEGRAM_CHAT_ID}" \
|
||||||
COMMIT=$(git rev-parse --short HEAD)
|
--data-urlencode "text=${text}" \
|
||||||
DEPLOY_HOST="${{ env.DEPLOY_HOST }}"
|
-d "parse_mode=HTML" >/dev/null || true
|
||||||
DEPLOY_USER="${{ env.DEPLOY_USER }}"
|
}
|
||||||
|
|
||||||
# 텔레그램 설정 바인딩 (Secret에 없을 경우 기본값 백업 사용)
|
notify_failure() {
|
||||||
TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}"
|
local exit_code=$?
|
||||||
[ -z "$TELEGRAM_BOT_TOKEN" ] && TELEGRAM_BOT_TOKEN="${{ env.TELEGRAM_BOT_TOKEN_DEFAULT }}"
|
send_telegram "❌ <b>QuantEngine 배포 실패</b>
|
||||||
TELEGRAM_CHAT_ID="${{ secrets.TELEGRAM_CHAT_ID }}"
|
|
||||||
[ -z "$TELEGRAM_CHAT_ID" ] && TELEGRAM_CHAT_ID="${{ env.TELEGRAM_CHAT_ID_DEFAULT }}"
|
|
||||||
|
|
||||||
send_telegram() {
|
커밋: <code>${COMMIT}</code>
|
||||||
local text="$1"
|
시간: <code>${TIMESTAMP}</code>
|
||||||
curl -fsS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
단계: deploy-to-prod (SSH Execution)"
|
||||||
-d "chat_id=${TELEGRAM_CHAT_ID}" \
|
exit "$exit_code"
|
||||||
--data-urlencode "text=${text}" \
|
}
|
||||||
-d "parse_mode=HTML" >/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
notify_failure() {
|
trap notify_failure ERR
|
||||||
local exit_code=$?
|
|
||||||
send_telegram "❌ <b>QuantEngine 배포 실패</b>
|
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"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
echo "=== Verifying Loopback Health ==="
|
||||||
|
loopback_html=$(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 "$DEPLOY_USER@$DEPLOY_HOST" "curl -sf http://127.0.0.1:5000/ || true")
|
||||||
|
if ! printf '%s' "$loopback_html" | grep -q "Quant Engine"; then
|
||||||
|
echo "Loopback health check failed for quantengine" >&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_html=$(curl -sf "http://${DEPLOY_HOST}/" 2>/dev/null || echo "")
|
||||||
|
ops_html=$(curl -sf "http://${DEPLOY_HOST}/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 "/ -> ${root_code}"
|
||||||
|
echo "/operations -> ${ops_code}"
|
||||||
|
|
||||||
|
if [ "$root_code" != "200" ]; then
|
||||||
|
echo "Deployment content check failed for /" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$ops_code" != "200" ]; then
|
||||||
|
echo "Deployment content check failed for /operations" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ 배포 완료: quantengine_${TIMESTAMP} @ $DEPLOY_HOST"
|
||||||
|
send_telegram "✅ <b>QuantEngine 배포 완료</b>
|
||||||
|
|
||||||
커밋: <code>${COMMIT}</code>
|
커밋: <code>${COMMIT}</code>
|
||||||
시간: <code>${TIMESTAMP}</code>
|
시간: <code>${TIMESTAMP}</code>
|
||||||
단계: deploy-to-prod (SSH Execution)"
|
대상: <code>${DEPLOY_HOST}</code>"
|
||||||
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 "✅ <b>QuantEngine 배포 완료</b>
|
|
||||||
|
|
||||||
커밋: <code>${COMMIT}</code>
|
|
||||||
시간: <code>${TIMESTAMP}</code>
|
|
||||||
대상: <code>${DEPLOY_HOST}</code>"
|
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
name: Snapshot Admin Deployment
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
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
|
|
||||||
timeout-minutes: 15
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup .NET SDK
|
|
||||||
uses: actions/setup-dotnet@v3
|
|
||||||
with:
|
|
||||||
dotnet-version: '10.0.x'
|
|
||||||
|
|
||||||
- name: Publish Blazor Web App
|
|
||||||
run: |
|
|
||||||
echo "[deploy] publishing .NET 10 Blazor app"
|
|
||||||
dotnet publish src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj -c Release -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: Compress Artifact
|
|
||||||
run: |
|
|
||||||
echo "[deploy] compressing publish output"
|
|
||||||
tar -czf quantengine.tar.gz -C ./publish .
|
|
||||||
|
|
||||||
- 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: 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 }}"
|
|
||||||
|
|
||||||
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 "❌ <b>Snapshot Admin 배포 실패</b>
|
|
||||||
|
|
||||||
커밋: <code>${COMMIT}</code>
|
|
||||||
시간: <code>${TIMESTAMP}</code>
|
|
||||||
단계: 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 && CI_DEPLOY=1 /home/kjh2064/tmp/deploy.sh"
|
|
||||||
|
|
||||||
# 3. 배포 성공 검증
|
|
||||||
echo "=== Verifying Loopback Health ==="
|
|
||||||
loopback_html=$(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/id_ed25519 "$DEPLOY_USER@$DEPLOY_HOST" "curl -sf http://127.0.0.1:5000/ || true")
|
|
||||||
if ! printf '%s' "$loopback_html" | grep -q "Quant Engine"; then
|
|
||||||
echo "Loopback health check failed for quantengine" >&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_html=$(curl -sf "http://${DEPLOY_HOST}/" 2>/dev/null || echo "")
|
|
||||||
ops_html=$(curl -sf "http://${DEPLOY_HOST}/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 "/ -> ${root_code}"
|
|
||||||
echo "/operations -> ${ops_code}"
|
|
||||||
|
|
||||||
if [ "$root_code" != "200" ]; then
|
|
||||||
echo "Deployment content check failed for /" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ "$ops_code" != "200" ]; then
|
|
||||||
echo "Deployment content check failed for /operations" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✓ 배포 완료: quantengine_${TIMESTAMP} @ $DEPLOY_HOST"
|
|
||||||
send_telegram "✅ <b>Snapshot Admin 배포 완료</b>
|
|
||||||
|
|
||||||
커밋: <code>${COMMIT}</code>
|
|
||||||
시간: <code>${TIMESTAMP}</code>
|
|
||||||
대상: <code>${DEPLOY_HOST}</code>"
|
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
- D+2 영업일 기준 현금을 즉시방어 자산으로 간주하고, 목표 예산 5억 원을 기준으로 포지션 사이징 및 리스크 버킷을 제어한다.
|
- D+2 영업일 기준 현금을 즉시방어 자산으로 간주하고, 목표 예산 5억 원을 기준으로 포지션 사이징 및 리스크 버킷을 제어한다.
|
||||||
- 매주 주말 리밸런싱(rebalance_required=true) 및 매월 1일/11일/21일 중간점검(mid_check_required=true) 운영 cadence를 준수한다.
|
- 매주 주말 리밸런싱(rebalance_required=true) 및 매월 1일/11일/21일 중간점검(mid_check_required=true) 운영 cadence를 준수한다.
|
||||||
- 커밋, 푸쉬, PR 작업 시 반드시 로컬의 .gs 파일을 Google Apps Script 원격 프로젝트에 업로드(python tools/deploy_gas.py 실행)하고, 사용자에게 스프레드시트 상의 스크립트 실행(예: runDataFeed)을 통한 검증을 유도 및 가이드해야 한다.
|
- 커밋, 푸쉬, PR 작업 시 반드시 로컬의 .gs 파일을 Google Apps Script 원격 프로젝트에 업로드(python tools/deploy_gas.py 실행)하고, 사용자에게 스프레드시트 상의 스크립트 실행(예: runDataFeed)을 통한 검증을 유도 및 가이드해야 한다.
|
||||||
- QuantEngine 배포는 CI 전용이다. 로컬에서 서버로 산출물을 직접 업로드하거나 `scp`/`rsync`로 수동 반영하지 않는다. 실배포는 `.gitea/workflows/snapshot_admin_deploy.yml`만 사용하며, 로컬 스크립트는 CI 환경에서만 실행 가능해야 한다.
|
- QuantEngine 배포는 CI 전용이다. 로컬에서 서버로 산출물을 직접 업로드하거나 `scp`/`rsync`로 수동 반영하지 않는다. 실배포는 `.gitea/workflows/deploy-prod.yml`만 사용하며, 로컬 스크립트는 CI 환경에서만 실행 가능해야 한다.
|
||||||
- 원격 서버 확인이 필요하면 `ssh kjh2064@178.104.200.7` 접속을 먼저 시도하고, 사용자에게 매번 접속 확인을 요구하지 말고 직접 상태/로그/헬스체크를 수집한 뒤 결과만 보고한다.
|
- 원격 서버 확인이 필요하면 `ssh kjh2064@178.104.200.7` 접속을 먼저 시도하고, 사용자에게 매번 접속 확인을 요구하지 말고 직접 상태/로그/헬스체크를 수집한 뒤 결과만 보고한다.
|
||||||
|
|
||||||
## 4. 보고 규칙
|
## 4. 보고 규칙
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ npm run prepare-upload-zip
|
|||||||
## CI / 배포 분리
|
## CI / 배포 분리
|
||||||
|
|
||||||
- `.gitea/workflows/ci.yml`은 검증 전용이다.
|
- `.gitea/workflows/ci.yml`은 검증 전용이다.
|
||||||
- `.gitea/workflows/snapshot_admin_deploy.yml`은 실배포 전용이다.
|
- `.gitea/workflows/deploy-prod.yml`은 실배포 전용이다.
|
||||||
- 공개 URL `http://178.104.200.7/quant/` 갱신은 deploy workflow 성공 여부로 판단한다.
|
- 공개 URL `http://178.104.200.7/quant/` 갱신은 deploy workflow 성공 여부로 판단한다.
|
||||||
|
|
||||||
## 운영 리포트 계약
|
## 운영 리포트 계약
|
||||||
|
|||||||
@@ -206,9 +206,9 @@ services:
|
|||||||
### 6.4. CI / 배포 분리
|
### 6.4. CI / 배포 분리
|
||||||
|
|
||||||
- `.gitea/workflows/ci.yml`: 검증 전용. 스펙/공식/리포트/아티팩트 생성까지만 수행한다.
|
- `.gitea/workflows/ci.yml`: 검증 전용. 스펙/공식/리포트/아티팩트 생성까지만 수행한다.
|
||||||
- `.gitea/workflows/snapshot_admin_deploy.yml`: 실배포 전용. `dotnet publish` 후 `tools/deploy_quantengine.sh`를 이용해 `/home/kjh2064/quantengine_active`로 반영한다.
|
- `.gitea/workflows/deploy-prod.yml`: 실배포 전용. `dotnet publish` 후 `tools/deploy_quantengine.sh`를 이용해 `/home/kjh2064/quantengine_active`로 반영한다.
|
||||||
- 수동 배포 금지: 로컬에서 `scp`/`rsync`로 `quantengine_active`를 갱신하지 않는다. 배포는 CI가 원격에서만 수행하고, 로컬 스크립트는 `CI_DEPLOY=1` 없이 실행되면 실패해야 한다.
|
- 수동 배포 금지: 로컬에서 `scp`/`rsync`로 `quantengine_active`를 갱신하지 않는다. 배포는 CI가 원격에서만 수행하고, 로컬 스크립트는 `CI_DEPLOY=1` 없이 실행되면 실패해야 한다.
|
||||||
- 공개 URL `/quant/` 갱신은 `snapshot_admin_deploy.yml`의 성공 여부를 기준으로 판단한다.
|
- 공개 URL 갱신은 `deploy-prod.yml`의 성공 여부를 기준으로 판단한다.
|
||||||
|
|
||||||
### 6.2. 러너 설정
|
### 6.2. 러너 설정
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ docker ps -a
|
|||||||
```bash
|
```bash
|
||||||
# CI에서만 배포
|
# CI에서만 배포
|
||||||
# 로컬에서 scp/rsync로 quantengine_active를 갱신하지 않는다.
|
# 로컬에서 scp/rsync로 quantengine_active를 갱신하지 않는다.
|
||||||
# 배포는 .gitea/workflows/snapshot_admin_deploy.yml 실행 결과로만 반영한다.
|
# 배포는 .gitea/workflows/deploy-prod.yml 실행 결과로만 반영한다.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Gitea Act Runner 등록
|
### Gitea Act Runner 등록
|
||||||
|
|||||||
+2
-2
@@ -925,7 +925,7 @@ python tools/validate_specs.py → PASS
|
|||||||
|------|------|
|
|------|------|
|
||||||
| **작업** | `src/quant_engine/snapshot_admin_server_v1.py`(Python 어드민 웹 UI)를 Gitea CI/CD 배포 스텝을 통해 Synology NAS에서 상시 서비스로 운영할 수 있는지 검토 |
|
| **작업** | `src/quant_engine/snapshot_admin_server_v1.py`(Python 어드민 웹 UI)를 Gitea CI/CD 배포 스텝을 통해 Synology NAS에서 상시 서비스로 운영할 수 있는지 검토 |
|
||||||
| **현재 상태** | **기술적으로는 가능**. 기본 루프백 보호 + Basic Auth 게이트를 추가했고, Synology 외부 노출은 리버스 프록시 기반 POC로 가이드함. 실배포 검증은 아직 필요 |
|
| **현재 상태** | **기술적으로는 가능**. 기본 루프백 보호 + Basic Auth 게이트를 추가했고, Synology 외부 노출은 리버스 프록시 기반 POC로 가이드함. 실배포 검증은 아직 필요 |
|
||||||
| **운영 분리** | `snapshot_admin.yml`은 `push`용 smoke 검증과 `workflow_dispatch`용 full 검증으로 분리하고, 배포는 별도 `snapshot_admin_deploy.yml` `workflow_dispatch`로 떼어냈다. `push`에서는 `Validate Snapshot Admin Workflow`까지만, full 검증에서는 `Validate Snapshot Admin Web UI`까지 수행한다. |
|
| **운영 분리** | `snapshot_admin.yml`은 `push`용 smoke 검증과 `workflow_dispatch`용 full 검증으로 분리하고, 배포는 별도 `deploy-prod.yml` `workflow_dispatch`로 떼어냈다. `push`에서는 `Validate Snapshot Admin Workflow`까지만, full 검증에서는 `Validate Snapshot Admin Web UI`까지 수행한다. |
|
||||||
| **runner 주의** | Gitea runner를 Docker mode로 두면 job 종료 시 `Cleaning up container` 로그가 남는다. host label로 재등록하면 job container 정리 로그를 피할 수 있다. |
|
| **runner 주의** | Gitea runner를 Docker mode로 두면 job 종료 시 `Cleaning up container` 로그가 남는다. host label로 재등록하면 job container 정리 로그를 피할 수 있다. |
|
||||||
| **KIS 분리** | `kis_data_collection.yml`은 `workflow_dispatch`용 mock/config smoke와 `schedule`용 live collection으로 분리했다. 수동 디스패치는 실제 수집을 돌리지 않고, 실수집은 스케줄 전용이다. |
|
| **KIS 분리** | `kis_data_collection.yml`은 `workflow_dispatch`용 mock/config smoke와 `schedule`용 live collection으로 분리했다. 수동 디스패치는 실제 수집을 돌리지 않고, 실수집은 스케줄 전용이다. |
|
||||||
| **담당 파일** | `.gitea/workflows/ci.yml`, `tools/run_snapshot_admin_server_v1.py`, `src/quant_engine/snapshot_admin_server_v1.py`, `docs/SYNOLOGY_SNAPSHOT_ADMIN_POC.md`, `docs/WBS_7_9_EVIDENCE_PACKET_FINAL.md` |
|
| **담당 파일** | `.gitea/workflows/ci.yml`, `tools/run_snapshot_admin_server_v1.py`, `src/quant_engine/snapshot_admin_server_v1.py`, `docs/SYNOLOGY_SNAPSHOT_ADMIN_POC.md`, `docs/WBS_7_9_EVIDENCE_PACKET_FINAL.md` |
|
||||||
@@ -1651,7 +1651,7 @@ WBS-10.1 (기반 결함 수정)
|
|||||||
| 10.10.2 | Dashboard 상태 페이지 — 데이터 비의존형 요약으로 단순화 | DB 실패 시에도 200 응답 (완료) |
|
| 10.10.2 | Dashboard 상태 페이지 — 데이터 비의존형 요약으로 단순화 | DB 실패 시에도 200 응답 (완료) |
|
||||||
| 10.10.3 | Counter.razor / Weather.razor 기본 페이지 삭제, NavMenu 정비 | 불필요 페이지 0건, NavMenu에 Dashboard/Operations만 표시 (완료) |
|
| 10.10.3 | Counter.razor / Weather.razor 기본 페이지 삭제, NavMenu 정비 | 불필요 페이지 0건, NavMenu에 Dashboard/Operations만 표시 (완료) |
|
||||||
| 10.10.4 | 다크 모드 + 반응형 레이아웃 적용 | 브라우저 렌더링 정상 확인 (완료) |
|
| 10.10.4 | 다크 모드 + 반응형 레이아웃 적용 | 브라우저 렌더링 정상 확인 (완료) |
|
||||||
| 10.10.5 | 배포 동기화 | `snapshot_admin_deploy.yml`가 `/quant/`와 `/quant/operations` 공개 라우트를 배포 후 검증하도록 구성됨 (완료) |
|
| 10.10.5 | 배포 동기화 | `deploy-prod.yml`가 공개 라우트를 배포 후 검증하도록 구성됨 (완료) |
|
||||||
|
|
||||||
**성공 하네스 (데이터 기준)**:
|
**성공 하네스 (데이터 기준)**:
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user