name: Deploy to Production on: workflow_dispatch: inputs: release: description: 'Release version to deploy (e.g., v0.1.20260711, or leave empty for latest)' required: false type: string 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 REPO: kjh2064/QuantEngineByItz jobs: fetch-release: name: Fetch Release Artifact runs-on: ubuntu-latest timeout-minutes: 10 outputs: release-tag: ${{ steps.fetch.outputs.tag }} artifact-name: ${{ steps.fetch.outputs.artifact }} artifact-size: ${{ steps.fetch.outputs.size }} commit-hash: ${{ steps.fetch.outputs.commit }} steps: - name: Fetch Release Info id: fetch run: | RELEASE_INPUT="${{ github.event.inputs.release }}" TOKEN="${{ secrets.GITEA_TOKEN }}" REPO="${{ env.REPO }}" if [ -z "$RELEASE_INPUT" ]; then # Fetch latest release RELEASE_URL="https://gitea.taxbaik.com/api/v1/repos/$REPO/releases/latest" else # Fetch specific release RELEASE_URL="https://gitea.taxbaik.com/api/v1/repos/$REPO/releases/tags/$RELEASE_INPUT" fi RELEASE=$(curl -s -H "Authorization: token $TOKEN" "$RELEASE_URL") TAG=$(echo "$RELEASE" | jq -r '.tag_name') COMMIT=$(echo "$RELEASE" | jq -r '.target_commitish' | cut -c1-7) if [ "$TAG" = "null" ] || [ -z "$TAG" ]; then echo "ERROR: Release not found" exit 1 fi # Find artifact in assets ARTIFACT=$(echo "$RELEASE" | jq -r '.assets[0].name') SIZE=$(echo "$RELEASE" | jq -r '.assets[0].size') if [ "$ARTIFACT" = "null" ] || [ -z "$ARTIFACT" ]; then echo "ERROR: No artifacts found in release $TAG" exit 1 fi echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "artifact=${ARTIFACT}" >> $GITHUB_OUTPUT echo "size=${SIZE}" >> $GITHUB_OUTPUT echo "commit=${COMMIT}" >> $GITHUB_OUTPUT echo "✓ Release: $TAG" echo "✓ Artifact: $ARTIFACT" echo "✓ Size: $SIZE bytes" - name: Download Release Artifact run: | TAG="${{ steps.fetch.outputs.tag }}" ARTIFACT="${{ steps.fetch.outputs.artifact }}" TOKEN="${{ secrets.GITEA_TOKEN }}" REPO="${{ env.REPO }}" DOWNLOAD_URL="https://gitea.taxbaik.com/api/v1/repos/$REPO/releases/download/$TAG/$ARTIFACT" echo "Downloading: $DOWNLOAD_URL" curl -L -H "Authorization: token $TOKEN" \ -o "$ARTIFACT" \ "$DOWNLOAD_URL" if [ ! -f "$ARTIFACT" ]; then echo "ERROR: Failed to download artifact" exit 1 fi echo "✓ Downloaded: $(du -sh $ARTIFACT)" - name: Upload to Actions uses: actions/upload-artifact@v4 with: name: release-artifact path: quantengine_*.tar.gz retention-days: 1 pre-deploy-check: name: Pre-Deployment Verification runs-on: ubuntu-latest needs: fetch-release 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 Release Artifact run: | if [ "${{ needs.fetch-release.outputs.artifact-name }}" = "" ]; then echo "ERROR: Release artifact not found" exit 1 fi echo "✓ Release: ${{ needs.fetch-release.outputs.release-tag }}" echo "✓ Artifact: ${{ needs.fetch-release.outputs.artifact-name }}" echo "✓ Commit: ${{ needs.fetch-release.outputs.commit-hash }}" deploy: name: Deploy to Production runs-on: ubuntu-latest needs: [ fetch-release, pre-deploy-check ] timeout-minutes: 30 steps: - name: Download Release Artifact uses: actions/download-artifact@v4 with: name: release-artifact - 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 Release Artifact run: | ARTIFACT="${{ needs.fetch-release.outputs.artifact-name }}" echo "Uploading: $ARTIFACT" ls -lh "$ARTIFACT" scp -i ~/.ssh/deploy_key \ -P ${{ env.DEPLOY_PORT }} \ -o StrictHostKeyChecking=accept-new \ "$ARTIFACT" ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}:/tmp/ echo "✓ Release artifact uploaded" - name: Deploy & Verify run: | ARTIFACT="${{ needs.fetch-release.outputs.artifact-name }}" RELEASE_TAG="${{ needs.fetch-release.outputs.release-tag }}" COMMIT="${{ needs.fetch-release.outputs.commit-hash }}" 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' RELEASE_TAG='$RELEASE_TAG' COMMIT='$COMMIT' DEPLOY_HOME=$HOME DEPLOY_DIR="$DEPLOY_HOME/deployments/quantengine_${RELEASE_TAG}_${COMMIT}" echo "=== Deployment Start ===" echo "Release: $RELEASE_TAG" 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" rm -f "/tmp/$ARTIFACT" 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 if [ ! -f "$DEPLOY_DIR/appsettings.Production.json" ]; then echo "ERROR: appsettings.Production.json not found" exit 1 fi echo "✓ DLL verified" echo "✓ Config 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: [ fetch-release, deploy ] timeout-minutes: 10 steps: - name: Setup SSH (for service check) 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 printf '%s' "$SSH_KEY_RAW" | base64 -d > ~/.ssh/deploy_key fi chmod 600 ~/.ssh/deploy_key 2>/dev/null || true ssh-keyscan -p 22 ${{ env.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true - 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: Release verified echo "✓ [5/5] Deployment release: ${{ needs.fetch-release.outputs.release-tag }} (commit: ${{ needs.fetch-release.outputs.commit-hash }})" # Check 6: DB connectivity (GET /Account/Login returns 200 even when # the DB password is stale -- the page itself has no DB dependency. # Only an actual login POST, or the app logs, reveal a broken # connection string. See CLAUDE.md "DB Secret Management" incident # 2026-07-12: this check would have caught it, the HTTP check alone # did not.) sleep 2 DB_ERRORS=$(ssh -i ~/.ssh/deploy_key \ -p 22 \ -o StrictHostKeyChecking=accept-new \ kjh2064@$DEPLOY_HOST \ "journalctl -u quantengine --since '1 minute ago' --no-pager 2>/dev/null | grep -c '28P01\|password authentication failed'" || echo "0") if [ "$DB_ERRORS" = "0" ]; then echo "✓ [6/6] No DB authentication errors in recent logs" else echo "❌ [6/6] DB authentication errors found in logs ($DB_ERRORS occurrences)" echo "" echo "❌ FAILED: Deployment reachable over HTTP but DB connection is broken" exit 1 fi 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: [ fetch-release, deploy, post-deploy-check ] steps: - name: Report Status run: | RELEASE="${{ needs.fetch-release.outputs.release-tag }}" COMMIT="${{ needs.fetch-release.outputs.commit-hash }}" ARTIFACT="${{ needs.fetch-release.outputs.artifact-name }}" FETCH_STATUS="${{ needs.fetch-release.result }}" DEPLOY_STATUS="${{ needs.deploy.result }}" CHECK_STATUS="${{ needs.post-deploy-check.result }}" echo "╔════════════════════════════════════════════╗" echo "║ Deployment Report ║" echo "╚════════════════════════════════════════════╝" echo "" echo "Release: $RELEASE" echo "Commit: $COMMIT" echo "Artifact: $ARTIFACT" echo "" echo "【 Status 】" echo "Fetch: $([ "$FETCH_STATUS" = "success" ] && echo "✓" || echo "✗") $FETCH_STATUS" echo "Deploy: $([ "$DEPLOY_STATUS" = "success" ] && echo "✓" || echo "✗") $DEPLOY_STATUS" echo "Health: $([ "$CHECK_STATUS" = "success" ] && echo "✓" || echo "✗") $CHECK_STATUS" echo "" if [ "$FETCH_STATUS" = "success" ] && [ "$DEPLOY_STATUS" = "success" ] && [ "$CHECK_STATUS" = "success" ]; then echo "✅ Deployment Successful" echo "Server: 178.104.200.7" echo "Release: $RELEASE" exit 0 else echo "❌ Deployment Failed" exit 1 fi