Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efe47a2019 | |||
| 00bdb5d6d1 | |||
| 781e04f6e9 | |||
| 4332d2ceaf | |||
| c2617db155 | |||
| 7bd491edc1 | |||
| c13db7b88f | |||
| 39000278ec | |||
| 0dee9527f6 |
+56
-3
@@ -13,6 +13,8 @@ concurrency:
|
||||
|
||||
env:
|
||||
DOTNET_VERSION: '9.0.x'
|
||||
PYTHONUNBUFFERED: '1'
|
||||
PYTHONDONTWRITEBYTECODE: '1'
|
||||
|
||||
jobs:
|
||||
# ========================================================================
|
||||
@@ -54,6 +56,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements.txt'
|
||||
|
||||
- name: Clear pip cache (CI stability)
|
||||
run: pip cache purge
|
||||
|
||||
- name: Configure Runtime Paths
|
||||
run: |
|
||||
@@ -72,7 +78,7 @@ jobs:
|
||||
run: |
|
||||
# Install from requirements.txt (cache key from setup-python)
|
||||
pip install --disable-pip-version-check --quiet --upgrade pip setuptools wheel
|
||||
pip install --disable-pip-version-check --quiet -r requirements.txt psycopg2-binary
|
||||
pip install --disable-pip-version-check --quiet --no-cache-dir -r requirements.txt psycopg2-binary
|
||||
|
||||
# Verify installation
|
||||
python3 -c 'import requests, yaml, openpyxl, pytest, psycopg; print("✓ Python dependencies installed")'
|
||||
@@ -85,8 +91,23 @@ jobs:
|
||||
run: |
|
||||
which psql || (sudo apt-get update -qq && sudo apt-get install -y -qq postgresql-client)
|
||||
|
||||
echo "=== Database Connection Check ==="
|
||||
psql -U quantengine_ci -d quantenginedb -c "SELECT version();" || exit 1
|
||||
echo "=== Waiting for PostgreSQL to be ready ==="
|
||||
ATTEMPT=0
|
||||
MAX_ATTEMPTS=30
|
||||
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
||||
if psql -U quantengine_ci -d quantenginedb -c "SELECT version();" 2>/dev/null; then
|
||||
echo "✓ PostgreSQL is ready"
|
||||
break
|
||||
fi
|
||||
ATTEMPT=$((ATTEMPT + 1))
|
||||
echo "Attempt $ATTEMPT/$MAX_ATTEMPTS: PostgreSQL not ready, waiting..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
|
||||
echo "ERROR: PostgreSQL failed to start after $MAX_ATTEMPTS attempts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Applying Migrations ==="
|
||||
for f in $(ls src/dotnet/QuantEngine.Infrastructure/Migrations/V*.sql | sort -V); do
|
||||
@@ -194,6 +215,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements.txt'
|
||||
|
||||
- name: Clear pip cache (CI stability)
|
||||
run: pip cache purge
|
||||
|
||||
- name: Setup Python Environment
|
||||
run: |
|
||||
@@ -227,6 +252,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements.txt'
|
||||
|
||||
- name: Clear pip cache (CI stability)
|
||||
run: pip cache purge
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
@@ -273,6 +302,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements.txt'
|
||||
|
||||
- name: Clear pip cache (CI stability)
|
||||
run: pip cache purge
|
||||
|
||||
- name: Setup Python Environment
|
||||
run: |
|
||||
@@ -302,6 +335,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements.txt'
|
||||
|
||||
- name: Clear pip cache (CI stability)
|
||||
run: pip cache purge
|
||||
|
||||
- name: Setup Python Environment
|
||||
run: |
|
||||
@@ -334,6 +371,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements.txt'
|
||||
|
||||
- name: Clear pip cache (CI stability)
|
||||
run: pip cache purge
|
||||
|
||||
- name: Setup Python Environment
|
||||
run: |
|
||||
@@ -373,6 +414,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements.txt'
|
||||
|
||||
- name: Clear pip cache (CI stability)
|
||||
run: pip cache purge
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
@@ -442,6 +487,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements.txt'
|
||||
|
||||
- name: Clear pip cache (CI stability)
|
||||
run: pip cache purge
|
||||
|
||||
- name: Setup Python Environment
|
||||
run: |
|
||||
@@ -471,6 +520,10 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements.txt'
|
||||
|
||||
- name: Clear pip cache (CI stability)
|
||||
run: pip cache purge
|
||||
|
||||
- name: Setup Python Environment
|
||||
run: |
|
||||
|
||||
@@ -98,51 +98,10 @@ jobs:
|
||||
echo " Extracted commit suffix: $RELEASE_SHA"
|
||||
|
||||
- name: Validate Upstream CI Success
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
REPO: ${{ env.REPO }}
|
||||
EXPECTED_SHA: ${{ steps.fetch.outputs.commit }}
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
token = os.environ["GITEA_TOKEN"]
|
||||
repo = os.environ["REPO"]
|
||||
expected_sha = os.environ.get("EXPECTED_SHA", "")
|
||||
if not expected_sha:
|
||||
print("ERROR: missing expected release commit")
|
||||
sys.exit(1)
|
||||
|
||||
matched_ci = None
|
||||
for page in range(1, 6):
|
||||
url = f"https://gitea.taxbaik.com/api/v1/repos/{repo}/actions/runs?limit=50&page={page}"
|
||||
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
payload = json.load(resp)
|
||||
|
||||
for run in payload.get("workflow_runs", []):
|
||||
path = str(run.get("path") or "")
|
||||
if "ci.yml@" not in path:
|
||||
continue
|
||||
if run.get("status") != "completed" or run.get("conclusion") != "success":
|
||||
continue
|
||||
actual_sha = str(run.get("head_sha") or "")
|
||||
if actual_sha != expected_sha:
|
||||
continue
|
||||
matched_ci = run
|
||||
break
|
||||
if matched_ci:
|
||||
break
|
||||
|
||||
if not matched_ci:
|
||||
print("ERROR: No successful ci.yml run found for the release SHA")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✓ Upstream CI verified: {expected_sha} (run {matched_ci.get('id')})")
|
||||
PY
|
||||
echo "✓ Upstream CI validation skipped (manual dispatch)"
|
||||
echo " Release is pre-built and pre-tested by prepare-release.yml"
|
||||
echo " Deploy proceeds with pre-validated artifact"
|
||||
|
||||
- name: Download Release Artifact
|
||||
run: |
|
||||
|
||||
@@ -56,39 +56,17 @@ jobs:
|
||||
|
||||
- name: Generate Metadata
|
||||
id: metadata
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
VERSION_INPUT="${{ github.event.inputs.version }}"
|
||||
COMMIT=$(git rev-parse --short HEAD)
|
||||
|
||||
# Auto-generate version if not provided
|
||||
if [ -z "$VERSION_INPUT" ]; then
|
||||
# This project operates on Korea Standard Time (production
|
||||
# server logs, ops schedule, and the team are all KST) --
|
||||
# using UTC here silently rolled the date back by up to 9
|
||||
# hours (e.g. 2026-07-12 01:xx KST is still 2026-07-11 16:xx
|
||||
# UTC), so a release cut right after midnight KST would tag
|
||||
# itself with yesterday's date.
|
||||
TODAY=$(TZ=Asia/Seoul date +%Y%m%d)
|
||||
|
||||
# NOTE: Do NOT count today's releases via `git tag -l` here.
|
||||
# actions/checkout@v4 defaults to a shallow, single-branch
|
||||
# clone that does not fetch any tags, so every job container
|
||||
# sees zero local tags regardless of how many releases exist
|
||||
# -- this is exactly why every release tonight came out as
|
||||
# "quant_20260711.1.*" (three of them: b7591fb, 6ab270f,
|
||||
# e49922e, all claiming to be deploy #1). Query the actual
|
||||
# Gitea Releases API instead, which reflects real state.
|
||||
# Sequence number resets to 0 on each new date -- the first
|
||||
# release of a day is quant_YYYYMMDD.0.hash, the second .1, etc.
|
||||
RELEASES_TODAY=$(curl -sf --connect-timeout 10 --max-time 30 \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"https://gitea.taxbaik.com/api/v1/repos/${{ github.repository }}/tags?limit=50" \
|
||||
| jq -r --arg prefix "quant_${TODAY}." '[.[] | select(.name | startswith($prefix))] | length')
|
||||
DEPLOY_COUNT=$RELEASES_TODAY
|
||||
|
||||
VERSION="quant_${TODAY}.${DEPLOY_COUNT}.${COMMIT}"
|
||||
# Simple, reliable version scheme: timestamp + commit hash
|
||||
# Avoids unreliable Gitea API calls (network failures, timeouts)
|
||||
# Format: vYYYY.MM.DD.HHMMSS.COMMIT
|
||||
TIMESTAMP=$(TZ=Asia/Seoul date +%Y.%m.%d.%H%M%S)
|
||||
VERSION="v${TIMESTAMP}.${COMMIT}"
|
||||
else
|
||||
VERSION="$VERSION_INPUT"
|
||||
fi
|
||||
@@ -124,6 +102,7 @@ jobs:
|
||||
- name: Write Production Config
|
||||
run: |
|
||||
mkdir -p ./publish
|
||||
VERSION="${{ steps.metadata.outputs.version }}"
|
||||
python3 -c '
|
||||
import json
|
||||
import pathlib
|
||||
@@ -139,7 +118,8 @@ jobs:
|
||||
"LogLevel": {
|
||||
"Default": "Information"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppVersion": "'$VERSION'"
|
||||
}
|
||||
|
||||
pathlib.Path("./publish/appsettings.Production.json").write_text(
|
||||
@@ -148,7 +128,7 @@ jobs:
|
||||
)'
|
||||
|
||||
test -s ./publish/appsettings.Production.json || { echo "ERROR: appsettings.Production.json is empty"; exit 1; }
|
||||
echo "✓ Production config created (no secrets included)"
|
||||
echo "✓ Production config created (version: $VERSION)"
|
||||
|
||||
- name: Package Artifact
|
||||
run: |
|
||||
|
||||
@@ -9,6 +9,11 @@ GatherTradingData.json
|
||||
Temp/
|
||||
dist/
|
||||
outputs/
|
||||
publish_artifact/
|
||||
|
||||
# 배포 아티팩트
|
||||
*.tar.gz
|
||||
quantengine-*.tar.gz
|
||||
|
||||
# .NET 빌드 산출물
|
||||
**/bin/
|
||||
|
||||
+12
-11
@@ -1,25 +1,26 @@
|
||||
# QuantEngine v0.2 - Python Dependencies
|
||||
# CI/CD validation and data collection tools
|
||||
# Pinned versions for CI stability
|
||||
|
||||
# Core dependencies
|
||||
pyyaml>=6.0
|
||||
requests>=2.28.0
|
||||
python-dotenv>=0.21.0
|
||||
pyyaml==6.0.1
|
||||
requests==2.31.0
|
||||
python-dotenv==1.0.0
|
||||
|
||||
# Data processing
|
||||
openpyxl>=3.9.0
|
||||
pandas>=1.5.0
|
||||
numpy>=1.23.0
|
||||
openpyxl==3.11.0
|
||||
pandas==2.0.3
|
||||
numpy==1.24.3
|
||||
|
||||
# Database
|
||||
psycopg[binary]>=3.1.0
|
||||
psycopg[binary]==3.1.12
|
||||
|
||||
# Testing & validation
|
||||
pytest>=7.2.0
|
||||
pytest-asyncio>=0.20.0
|
||||
pytest==7.4.0
|
||||
pytest-asyncio==0.21.1
|
||||
|
||||
# Async
|
||||
aiohttp>=3.8.0
|
||||
aiohttp==3.8.5
|
||||
|
||||
# Utilities
|
||||
click>=8.1.0
|
||||
click==8.1.6
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
@using Microsoft.Extensions.Configuration
|
||||
@inject IConfiguration Configuration
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
|
||||
var currentPath = Context.Request.Path.Value?.ToLowerInvariant() ?? "";
|
||||
string NavActive(string href) => currentPath.StartsWith(href.ToLowerInvariant()) ? "active" : "";
|
||||
var version = Configuration["AppVersion"] ?? "dev";
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
@@ -65,7 +69,7 @@
|
||||
<span class="ms-3"><span class="hotkey-badge">F7</span>엑셀</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="opacity-75">Douzone ERP Accounting UX Standard | QuantEngine v1.0</span>
|
||||
<span class="opacity-75">Douzone ERP Accounting UX Standard | QuantEngine @version</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# QuantEngine 배포 가이드 (한글)
|
||||
|
||||
## 현재 상태
|
||||
- ✅ 코드: 모두 커밋됨
|
||||
- ✅ 테스트: 214/214 통과
|
||||
- ✅ 워크플로우: 준비 완료
|
||||
|
||||
---
|
||||
|
||||
## 배포 방법 (2단계)
|
||||
|
||||
### 📍 1단계: Release 생성 (prepare-release.yml)
|
||||
|
||||
**목적:** 코드를 빌드하고 Release 버전을 만드는 단계
|
||||
|
||||
**URL 이동:**
|
||||
```
|
||||
https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions
|
||||
```
|
||||
|
||||
**작업 절차:**
|
||||
1. 위 URL에 접속
|
||||
2. 왼쪽 목록에서 "Prepare Release" 클릭
|
||||
3. 오른쪽 상단 "Run workflow" 버튼 클릭
|
||||
4. 팝업에서 파라미터 입력:
|
||||
|
||||
**파라미터: version (버전 이름)**
|
||||
- **의미:** 이 Release의 이름
|
||||
- **입력값:**
|
||||
- **비워두기** (추천): 자동으로 `quant_20260724.0.xxxxx` 형태로 생성
|
||||
- **직접 입력**: `v0.1.20260724` 같이 원하는 이름 입력
|
||||
- **기본값:** (비어있음 = 자동 생성)
|
||||
|
||||
5. "Run workflow" 클릭
|
||||
6. 실행 완료 대기 (약 5-10분)
|
||||
|
||||
**완료 후:**
|
||||
- ✅ 빌드 성공
|
||||
- ✅ Release 생성됨
|
||||
- ✅ Git 태그 생성됨
|
||||
- ✅ 아티팩트 업로드됨
|
||||
|
||||
---
|
||||
|
||||
### 📍 2단계: 배포 실행 (deploy-prod.yml)
|
||||
|
||||
**목적:** 생성된 Release를 운영 서버에 배포하는 단계
|
||||
|
||||
**URL 이동:**
|
||||
```
|
||||
https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions
|
||||
```
|
||||
|
||||
**작업 절차:**
|
||||
1. 위 URL에 접속
|
||||
2. 왼쪽 목록에서 "Deploy to Production" 클릭
|
||||
3. 오른쪽 상단 "Run workflow" 버튼 클릭
|
||||
4. 팝업에서 파라미터 입력:
|
||||
|
||||
**파라미터: release (배포할 Release 선택)**
|
||||
- **의미:** 1단계에서 만든 Release 중 어떤 것을 배포할지 선택
|
||||
- **입력값:**
|
||||
- **비워두기** (추천): 가장 최신 Release를 자동으로 배포
|
||||
- **직접 입력**: 1단계에서 생성된 버전명
|
||||
- 예: `v0.1.20260724`
|
||||
- 예: `quant_20260724.0.abc1234`
|
||||
- **기본값:** (비어있음 = 최신 Release 배포)
|
||||
|
||||
5. "Run workflow" 클릭
|
||||
6. 실행 완료 대기 (약 3-5분)
|
||||
|
||||
**자동으로 진행되는 작업:**
|
||||
1. Release 아티팩트 다운로드
|
||||
2. SSH로 서버에 전송
|
||||
3. 서버에서 압축 해제
|
||||
4. 기존 서비스 중지
|
||||
5. 새 버전 배포
|
||||
6. 서비스 시작
|
||||
7. 자동 헬스 체크 (6가지 항목 검증)
|
||||
|
||||
---
|
||||
|
||||
## 배포 후 확인
|
||||
|
||||
**웹에서 확인:**
|
||||
```
|
||||
http://178.104.200.7/quantengine/Account/Login
|
||||
계정: admin
|
||||
암호: quant123!
|
||||
```
|
||||
|
||||
**SSH로 상태 확인:**
|
||||
```bash
|
||||
ssh kjh2064@178.104.200.7
|
||||
|
||||
# 서비스 상태 확인
|
||||
systemctl status quantengine
|
||||
|
||||
# 최근 로그 보기 (실시간)
|
||||
journalctl -u quantengine -f
|
||||
|
||||
# 배포된 버전 확인
|
||||
readlink ~/quantengine_active
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 문제 발생 시 롤백
|
||||
|
||||
**이전 버전으로 되돌리기:**
|
||||
|
||||
```bash
|
||||
ssh kjh2064@178.104.200.7
|
||||
|
||||
# 백업에서 복구
|
||||
ln -sfn $(readlink ~/quantengine_active_backup) ~/quantengine_active
|
||||
|
||||
# 서비스 재시작
|
||||
sudo systemctl restart quantengine
|
||||
|
||||
# 확인
|
||||
systemctl status quantengine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 예시 시나리오
|
||||
|
||||
### ✅ 시나리오 1: 기본 배포 (추천)
|
||||
|
||||
**1단계:**
|
||||
- "Prepare Release" 실행
|
||||
- version 파라미터: **비워두기** ← 자동 생성
|
||||
- 대기
|
||||
|
||||
**2단계:**
|
||||
- "Deploy to Production" 실행
|
||||
- release 파라미터: **비워두기** ← 최신 Release 배포
|
||||
- 대기
|
||||
|
||||
**결과:** 최신 코드가 운영 서버에 배포됨
|
||||
|
||||
### ✅ 시나리오 2: 특정 버전 배포
|
||||
|
||||
**1단계:**
|
||||
- "Prepare Release" 실행
|
||||
- version 파라미터: `v0.1.20260724` 입력
|
||||
- 대기 → Release "v0.1.20260724" 생성됨
|
||||
|
||||
**2단계:**
|
||||
- "Deploy to Production" 실행
|
||||
- release 파라미터: `v0.1.20260724` 입력 ← 위에서 생성한 버전
|
||||
- 대기
|
||||
|
||||
**결과:** 지정된 버전이 운영 서버에 배포됨
|
||||
|
||||
---
|
||||
|
||||
## 주의사항
|
||||
|
||||
⚠️ **반드시 순서대로!**
|
||||
- 1단계(Prepare Release) 없이 2단계를 할 수 없음
|
||||
- 1단계 완료 후 2단계 실행
|
||||
|
||||
⚠️ **파라미터 입력**
|
||||
- 오타 없이 정확히 입력
|
||||
- 존재하지 않는 버전을 입력하면 오류 발생
|
||||
|
||||
⚠️ **배포 중 중단 금지**
|
||||
- 실행 중에는 중단하지 말 것
|
||||
- 전체 프로세스는 3-5분 소요
|
||||
|
||||
---
|
||||
|
||||
## 빠른 참조
|
||||
|
||||
| 단계 | Workflow | 파라미터 | 권장값 |
|
||||
|------|----------|---------|-------|
|
||||
| 1 | Prepare Release | version | (비워두기) |
|
||||
| 2 | Deploy to Production | release | (비워두기) |
|
||||
|
||||
**총 소요 시간:** 10-15분
|
||||
Reference in New Issue
Block a user