name: Deploy to Production on: push: branches: [ main ] workflow_dispatch: concurrency: group: deploy-prod-main cancel-in-progress: false env: DEPLOY_HOST: 178.104.200.7 DEPLOY_USER: kjh2064 DEPLOY_PORT: 22 SERVICE_NAME: quantengine DOTNET_VERSION: '10.0.x' jobs: build: name: Build Release runs-on: ubuntu-latest timeout-minutes: 15 outputs: artifact-name: ${{ steps.metadata.outputs.artifact }} commit-hash: ${{ steps.metadata.outputs.commit }} timestamp: ${{ steps.metadata.outputs.timestamp }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Generate Metadata id: metadata run: | COMMIT=$(git rev-parse --short HEAD) TIMESTAMP=$(TZ=UTC date +%Y%m%d_%H%M%S) ARTIFACT="quantengine_${TIMESTAMP}_${COMMIT}.tar.gz" echo "artifact=${ARTIFACT}" >> $GITHUB_OUTPUT echo "commit=${COMMIT}" >> $GITHUB_OUTPUT echo "timestamp=${TIMESTAMP}" >> $GITHUB_OUTPUT - name: Restore 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:ContinuousIntegrationBuild=true - name: Publish run: | dotnet publish src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \ -c Release \ -o ./publish \ --no-restore \ --no-build - name: Package Artifact run: | ARTIFACT="${{ steps.metadata.outputs.artifact }}" tar -czf "$ARTIFACT" -C ./publish . echo "✓ Package: $(du -sh $ARTIFACT | cut -f1)" file "$ARTIFACT" - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: build-${{ github.run_number }} path: quantengine_*.tar.gz retention-days: 7 pre-deploy-check: name: Pre-Deployment Verification runs-on: ubuntu-latest needs: build timeout-minutes: 5 steps: - name: Verify SSH Key run: | SSH_KEY_B64="${{ secrets.DEPLOY_SSH_KEY_B64 }}" SSH_KEY_RAW="${{ secrets.DEPLOY_SSH_KEY }}" if [ -z "$SSH_KEY_B64" ] && [ -z "$SSH_KEY_RAW" ]; then echo "ERROR: DEPLOY_SSH_KEY_B64 or DEPLOY_SSH_KEY not configured" exit 1 fi echo "✓ SSH key configured" - name: Verify Secrets run: | [ -z "${{ secrets.DEPLOY_HOST }}" ] && { echo "ERROR: DEPLOY_HOST not configured"; exit 1; } [ -z "${{ secrets.DEPLOY_USER }}" ] && { echo "ERROR: DEPLOY_USER not configured"; exit 1; } echo "✓ All secrets configured" - name: Verify Build Artifact run: | if [ "${{ needs.build.outputs.artifact-name }}" = "" ]; then echo "ERROR: Build artifact not generated" exit 1 fi echo "✓ Artifact: ${{ needs.build.outputs.artifact-name }}" deploy: name: Deploy to Production runs-on: ubuntu-latest needs: [ build, pre-deploy-check ] timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4 - name: Download Artifact uses: actions/download-artifact@v4 with: name: build-${{ github.run_number }} - name: Setup SSH run: | mkdir -p ~/.ssh SSH_KEY_B64="${{ secrets.DEPLOY_SSH_KEY_B64 }}" SSH_KEY_RAW="${{ secrets.DEPLOY_SSH_KEY }}" if [ -n "$SSH_KEY_B64" ]; then printf '%s' "$SSH_KEY_B64" | base64 -d > ~/.ssh/deploy_key elif [ -n "$SSH_KEY_RAW" ]; then if printf '%s' "$SSH_KEY_RAW" | grep -q 'BEGIN.*PRIVATE KEY'; then printf '%b\n' "$SSH_KEY_RAW" > ~/.ssh/deploy_key else printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/deploy_key fi else echo "ERROR: No SSH key configured" exit 1 fi sed -i 's/\r$//' ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -p ${{ env.DEPLOY_PORT }} ${{ env.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true echo "✓ SSH configured" - name: Upload Artifact run: | ARTIFACT="${{ needs.build.outputs.artifact-name }}" scp -i ~/.ssh/deploy_key \ -P ${{ env.DEPLOY_PORT }} \ -o StrictHostKeyChecking=accept-new \ "$ARTIFACT" ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}:/tmp/ echo "✓ Artifact uploaded" - name: Deploy & Verify run: | ARTIFACT="${{ needs.build.outputs.artifact-name }}" COMMIT="${{ needs.build.outputs.commit-hash }}" TIMESTAMP="${{ needs.build.outputs.timestamp }}" ssh -i ~/.ssh/deploy_key \ -p ${{ env.DEPLOY_PORT }} \ -o StrictHostKeyChecking=accept-new \ ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash << 'REMOTE' set -e ARTIFACT='$ARTIFACT' COMMIT='$COMMIT' TIMESTAMP='$TIMESTAMP' DEPLOY_HOME=$HOME DEPLOY_DIR="$DEPLOY_HOME/deployments/quantengine_${TIMESTAMP}_${COMMIT}" echo "=== Deployment Start ===" echo "Artifact: $ARTIFACT" echo "Commit: $COMMIT" echo "Deploy Dir: $DEPLOY_DIR" echo "" # 1. Extract echo "【 1/4 Extract Artifact 】" mkdir -p "$DEPLOY_DIR" tar -xzf "/tmp/$ARTIFACT" -C "$DEPLOY_DIR" echo "✓ Extraction complete" # 2. Verify echo "" echo "【 2/4 Verify Deployment 】" if [ ! -f "$DEPLOY_DIR/QuantEngine.Web.dll" ]; then echo "ERROR: QuantEngine.Web.dll not found" exit 1 fi echo "✓ DLL verified" # 3. Update Symlink echo "" echo "【 3/4 Update Symlink 】" ln -sfn "$DEPLOY_DIR" "$DEPLOY_HOME/quantengine_active" echo "✓ Active: $(readlink $DEPLOY_HOME/quantengine_active)" # 4. Restart Service echo "" echo "【 4/4 Restart Service 】" sudo systemctl restart $SERVICE_NAME echo "✓ Service restarted" REMOTE post-deploy-check: name: Health Check & Verification runs-on: ubuntu-latest needs: [ build, deploy ] timeout-minutes: 10 steps: - name: Health Check run: | set -e ATTEMPTS=20 DEPLOY_HOST="${{ env.DEPLOY_HOST }}" echo "【 Health Checks (max ${ATTEMPTS} attempts) 】" for i in $(seq 1 $ATTEMPTS); do # Check 1: HTTP 200 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://$DEPLOY_HOST:5000/Account/Login 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "200" ]; then echo "✓ [1/5] HTTP 200 OK (attempt $i)" # Check 2: Login Page LOGIN_BODY=$(curl -s http://$DEPLOY_HOST:5000/Account/Login 2>/dev/null || echo "") if echo "$LOGIN_BODY" | grep -q "login\|Login\|로그인"; then echo "✓ [2/5] Login page content verified" else echo "⚠ [2/5] Login page content verification skipped" fi # Check 3: CSS loaded CSS_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://$DEPLOY_HOST:5000/css/admin.css 2>/dev/null || echo "000") if [ "$CSS_CODE" = "200" ]; then echo "✓ [3/5] CSS file loaded" else echo "⚠ [3/5] CSS file check skipped (status: $CSS_CODE)" fi # Check 4: Service active SERVICE_STATUS=$(ssh -i ~/.ssh/deploy_key \ -p 22 \ -o StrictHostKeyChecking=accept-new \ kjh2064@$DEPLOY_HOST \ "systemctl is-active quantengine" 2>/dev/null || echo "unknown") if [ "$SERVICE_STATUS" = "active" ]; then echo "✓ [4/5] Service active (running)" else echo "⚠ [4/5] Service status: $SERVICE_STATUS" fi # Check 5: Commit verified echo "✓ [5/5] Deployment commit: ${{ needs.build.outputs.commit-hash }}" echo "" echo "✅ All health checks passed!" exit 0 fi if [ $i -lt $ATTEMPTS ]; then echo " Attempt $i/$ATTEMPTS... (HTTP $HTTP_CODE, retrying in 3s)" sleep 3 else echo "" echo "❌ FAILED: Service did not respond after $ATTEMPTS attempts" exit 1 fi done post-deploy-report: name: Deployment Report runs-on: ubuntu-latest if: always() needs: [ build, deploy, post-deploy-check ] steps: - name: Report Status run: | COMMIT="${{ needs.build.outputs.commit-hash }}" ARTIFACT="${{ needs.build.outputs.artifact-name }}" BUILD_STATUS="${{ needs.build.result }}" DEPLOY_STATUS="${{ needs.deploy.result }}" CHECK_STATUS="${{ needs.post-deploy-check.result }}" echo "╔════════════════════════════════════════════╗" echo "║ Deployment Report ║" echo "╚════════════════════════════════════════════╝" echo "" echo "Commit: $COMMIT" echo "Artifact: $ARTIFACT" echo "" echo "【 Status 】" echo "Build: $([ "$BUILD_STATUS" = "success" ] && echo "✓" || echo "✗") $BUILD_STATUS" echo "Deploy: $([ "$DEPLOY_STATUS" = "success" ] && echo "✓" || echo "✗") $DEPLOY_STATUS" echo "Health: $([ "$CHECK_STATUS" = "success" ] && echo "✓" || echo "✗") $CHECK_STATUS" echo "" if [ "$BUILD_STATUS" = "success" ] && [ "$DEPLOY_STATUS" = "success" ] && [ "$CHECK_STATUS" = "success" ]; then echo "✅ Deployment Successful" echo "Server: 178.104.200.7" exit 0 else echo "❌ Deployment Failed" exit 1 fi