From 352b440e8df4983263ba078144be3a2eda46f5cf Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sat, 11 Jul 2026 23:21:20 +0900 Subject: [PATCH] feat(ci/cd): Implement release-based deployment with two-workflow architecture - Add prepare-release.yml: Manual release creation workflow * Builds code, generates appsettings.Production.json * Packages artifact (.tar.gz) * Creates git tag and Gitea Release with attached artifact - Refactor deploy-prod.yml: Release-based deployment workflow * Fetch Release stage: Query Gitea Releases, download artifact * Pre-Check stage: Verify SSH credentials and release integrity * Deploy stage: Upload, extract, symlink, restart service * Health Check stage: 5-point verification (HTTP, CSS, login, service, release) * Report stage: Final deployment status * Now triggered via workflow_dispatch with release version input * Removes on:push trigger (manual release selection required) - Update CLAUDE.md: * Document two-workflow architecture * Add release creation and deployment procedures * Update SSH key configuration with GITEA_TOKEN requirement * Clarify CI/CD-Only Deployment Mandate with release traceability * Add complete deployment flow documentation **Motivation**: - Separate build/release phase from deployment phase - Enable release tagging for version control and rollback - Reduce build time on re-deployments (use cached releases) - Improve deployment auditability via git tags and Gitea Releases - Match taxbaik-pattern release management strategy Co-Authored-By: Claude Haiku 4.5 --- .gitea/workflows/deploy-prod.yml | 228 +++++++++++++++------------ .gitea/workflows/prepare-release.yml | 144 +++++++++++++++++ CLAUDE.md | 134 +++++++++++++--- 3 files changed, 376 insertions(+), 130 deletions(-) create mode 100644 .gitea/workflows/prepare-release.yml diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml index 1188cdb7..4b4a90b7 100644 --- a/.gitea/workflows/deploy-prod.yml +++ b/.gitea/workflows/deploy-prod.yml @@ -1,9 +1,12 @@ name: Deploy to Production on: - push: - branches: [ main ] 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 @@ -14,104 +17,94 @@ env: DEPLOY_USER: kjh2064 DEPLOY_PORT: 22 SERVICE_NAME: quantengine - DOTNET_VERSION: '10.0.x' + REPO: kjh2064/QuantEngineByItz jobs: - build: - name: Build Release + fetch-release: + name: Fetch Release Artifact runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 10 outputs: - artifact-name: ${{ steps.metadata.outputs.artifact }} - commit-hash: ${{ steps.metadata.outputs.commit }} - timestamp: ${{ steps.metadata.outputs.timestamp }} + 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: Checkout - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Generate Metadata - id: metadata + - name: Fetch Release Info + id: fetch run: | - COMMIT=$(git rev-parse --short HEAD) - TIMESTAMP=$(TZ=UTC date +%Y%m%d_%H%M%S) - ARTIFACT="quantengine_${TIMESTAMP}_${COMMIT}.tar.gz" + 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 "timestamp=${TIMESTAMP}" >> $GITHUB_OUTPUT - - name: Restore + echo "✓ Release: $TAG" + echo "✓ Artifact: $ARTIFACT" + echo "✓ Size: $SIZE bytes" + + - name: Download Release Artifact run: | - dotnet restore src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj + TAG="${{ steps.fetch.outputs.tag }}" + ARTIFACT="${{ steps.fetch.outputs.artifact }}" + TOKEN="${{ secrets.GITEA_TOKEN }}" + REPO="${{ env.REPO }}" - - name: Build (Release) - run: | - dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \ - -c Release \ - --no-restore \ - -p:ContinuousIntegrationBuild=true + DOWNLOAD_URL="https://gitea.taxbaik.com/api/v1/repos/$REPO/releases/download/$TAG/$ARTIFACT" - - name: Publish - run: | - dotnet publish src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \ - -c Release \ - -o ./publish \ - --no-restore \ - --no-build + echo "Downloading: $DOWNLOAD_URL" + curl -L -H "Authorization: token $TOKEN" \ + -o "$ARTIFACT" \ + "$DOWNLOAD_URL" - - name: Write Production Config - run: | - mkdir -p ./publish - DEPLOY_HOST="${{ secrets.DEPLOY_HOST }}" - DEPLOY_USER="${{ secrets.DEPLOY_USER }}" + if [ ! -f "$ARTIFACT" ]; then + echo "ERROR: Failed to download artifact" + exit 1 + fi - # appsettings.Production.json 생성 - python3 -c ' - import json - import pathlib + echo "✓ Downloaded: $(du -sh $ARTIFACT)" - config = { - "ConnectionStrings": { - "DefaultConnection": "Host=127.0.0.1;Database=quantenginedb;Username=quantengine_app;Password=quantengine_app;Search Path=quantengine;" - }, - "Logging": { - "LogLevel": { - "Default": "Information" - } - } - } - - pathlib.Path("./publish/appsettings.Production.json").write_text( - json.dumps(config, ensure_ascii=False, indent=2), - encoding="utf-8" - )' - - test -s ./publish/appsettings.Production.json || { echo "ERROR: appsettings.Production.json is empty"; exit 1; } - echo "✓ Production config created" - - - 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 + - name: Upload to Actions uses: actions/upload-artifact@v4 with: - name: build-${{ github.run_number }} + name: release-artifact path: quantengine_*.tar.gz - retention-days: 7 + retention-days: 1 pre-deploy-check: name: Pre-Deployment Verification runs-on: ubuntu-latest - needs: build + needs: fetch-release timeout-minutes: 5 steps: @@ -131,28 +124,27 @@ jobs: [ -z "${{ secrets.DEPLOY_USER }}" ] && { echo "ERROR: DEPLOY_USER not configured"; exit 1; } echo "✓ All secrets configured" - - name: Verify Build Artifact + - name: Verify Release Artifact run: | - if [ "${{ needs.build.outputs.artifact-name }}" = "" ]; then - echo "ERROR: Build artifact not generated" + if [ "${{ needs.fetch-release.outputs.artifact-name }}" = "" ]; then + echo "ERROR: Release artifact not found" exit 1 fi - echo "✓ Artifact: ${{ needs.build.outputs.artifact-name }}" + 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: [ build, pre-deploy-check ] + needs: [ fetch-release, pre-deploy-check ] timeout-minutes: 30 steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Download Artifact + - name: Download Release Artifact uses: actions/download-artifact@v4 with: - name: build-${{ github.run_number }} + name: release-artifact - name: Setup SSH run: | @@ -178,20 +170,23 @@ jobs: ssh-keyscan -p ${{ env.DEPLOY_PORT }} ${{ env.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true echo "✓ SSH configured" - - name: Upload Artifact + - name: Upload Release Artifact run: | - ARTIFACT="${{ needs.build.outputs.artifact-name }}" + 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 "✓ Artifact uploaded" + echo "✓ Release artifact uploaded" - name: Deploy & Verify run: | - ARTIFACT="${{ needs.build.outputs.artifact-name }}" - COMMIT="${{ needs.build.outputs.commit-hash }}" - TIMESTAMP="${{ needs.build.outputs.timestamp }}" + 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 }} \ @@ -200,12 +195,13 @@ jobs: set -e ARTIFACT='$ARTIFACT' + RELEASE_TAG='$RELEASE_TAG' COMMIT='$COMMIT' - TIMESTAMP='$TIMESTAMP' DEPLOY_HOME=$HOME - DEPLOY_DIR="$DEPLOY_HOME/deployments/quantengine_${TIMESTAMP}_${COMMIT}" + 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" @@ -215,6 +211,7 @@ jobs: 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 @@ -224,7 +221,12 @@ jobs: 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 "" @@ -243,10 +245,25 @@ jobs: post-deploy-check: name: Health Check & Verification runs-on: ubuntu-latest - needs: [ build, deploy ] + 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 @@ -289,8 +306,8 @@ jobs: echo "⚠ [4/5] Service status: $SERVICE_STATUS" fi - # Check 5: Commit verified - echo "✓ [5/5] Deployment commit: ${{ needs.build.outputs.commit-hash }}" + # Check 5: Release verified + echo "✓ [5/5] Deployment release: ${{ needs.fetch-release.outputs.release-tag }} (commit: ${{ needs.fetch-release.outputs.commit-hash }})" echo "" echo "✅ All health checks passed!" @@ -311,14 +328,15 @@ jobs: name: Deployment Report runs-on: ubuntu-latest if: always() - needs: [ build, deploy, post-deploy-check ] + needs: [ fetch-release, 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 }}" + 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 }}" @@ -326,18 +344,20 @@ jobs: echo "║ Deployment Report ║" echo "╚════════════════════════════════════════════╝" echo "" + echo "Release: $RELEASE" echo "Commit: $COMMIT" echo "Artifact: $ARTIFACT" echo "" echo "【 Status 】" - echo "Build: $([ "$BUILD_STATUS" = "success" ] && echo "✓" || echo "✗") $BUILD_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 [ "$BUILD_STATUS" = "success" ] && [ "$DEPLOY_STATUS" = "success" ] && [ "$CHECK_STATUS" = "success" ]; then + 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" diff --git a/.gitea/workflows/prepare-release.yml b/.gitea/workflows/prepare-release.yml new file mode 100644 index 00000000..de6b6ab0 --- /dev/null +++ b/.gitea/workflows/prepare-release.yml @@ -0,0 +1,144 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., v0.1.20260711 or v1.0.0)' + required: true + type: string + +env: + DOTNET_VERSION: '10.0.x' + +jobs: + build-and-release: + name: Build & Create Release + runs-on: ubuntu-latest + timeout-minutes: 30 + + 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: | + VERSION="${{ github.event.inputs.version }}" + COMMIT=$(git rev-parse --short HEAD) + TIMESTAMP=$(TZ=UTC date +%Y%m%d_%H%M%S) + ARTIFACT="quantengine_${VERSION}_${COMMIT}.tar.gz" + echo "version=${VERSION}" >> $GITHUB_OUTPUT + 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: Write Production Config + run: | + mkdir -p ./publish + python3 -c ' + import json + import pathlib + + config = { + "ConnectionStrings": { + "DefaultConnection": "Host=127.0.0.1;Database=quantenginedb;Username=quantengine_app;Password=quantengine_app;Search Path=quantengine;" + }, + "Logging": { + "LogLevel": { + "Default": "Information" + } + } + } + + pathlib.Path("./publish/appsettings.Production.json").write_text( + json.dumps(config, ensure_ascii=False, indent=2), + encoding="utf-8" + )' + + test -s ./publish/appsettings.Production.json || { echo "ERROR: appsettings.Production.json is empty"; exit 1; } + echo "✓ Production config created" + + - name: Package Artifact + run: | + ARTIFACT="${{ steps.metadata.outputs.artifact }}" + tar -czf "$ARTIFACT" -C ./publish . + echo "✓ Package: $(du -sh $ARTIFACT | cut -f1)" + file "$ARTIFACT" + + - name: Create Git Tag + run: | + VERSION="${{ steps.metadata.outputs.version }}" + COMMIT="${{ steps.metadata.outputs.commit }}" + git tag -a "$VERSION" -m "Release $VERSION (commit: $COMMIT)" HEAD + git push origin "$VERSION" + echo "✓ Tag created: $VERSION" + + - name: Create Gitea Release + run: | + VERSION="${{ steps.metadata.outputs.version }}" + ARTIFACT="${{ steps.metadata.outputs.artifact }}" + COMMIT="${{ steps.metadata.outputs.commit }}" + TOKEN="${{ secrets.GITEA_TOKEN }}" + REPO="kjh2064/QuantEngineByItz" + + # Create release + gh release create "$VERSION" "$ARTIFACT" \ + --title "Release $VERSION" \ + --notes "Commit: $COMMIT +Platform: .NET 10 +Build: $(date '+%Y-%m-%d %H:%M:%S UTC') + +## Files +- quantengine_release.tar.gz + - QuantEngine.Web.dll + - appsettings.Production.json + - All dependencies + +## Deployment +Use deploy-prod.yml with release: \`$VERSION\`" + + echo "✓ Release created: $VERSION" + echo "✓ Artifact attached" + + notification: + name: Release Notification + runs-on: ubuntu-latest + if: success() + needs: build-and-release + + steps: + - name: Notify Release Ready + run: | + echo "════════════════════════════════════════" + echo "✅ Release Ready for Deployment" + echo "════════════════════════════════════════" + echo "Version: ${{ needs.build-and-release.outputs.version }}" + echo "Commit: ${{ needs.build-and-release.outputs.commit }}" + echo "" + echo "Next: Use deploy-prod.yml to deploy this release" + echo "════════════════════════════════════════" diff --git a/CLAUDE.md b/CLAUDE.md index 8a667d4e..4dfdd1d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,40 +120,60 @@ Projects on server: - Audit trail (all deployments logged in Gitea Actions) - Consistent process (no manual errors) - Rollback safety (deployment history retained) +- Release traceability (version control via git tags) -### Production Deployment Strategy (Gitea Actions CI/CD) +### Production Deployment Strategy (Release-Based) -**Status**: Gitea Actions fully operational (taxbaik-pattern with enhanced health checks) +**Architecture**: Two-Workflow System (Release Creation → Deployment) -**Pre-Deployment Checklist**: -1. ✅ Local build: `dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj -c Release` -2. ✅ E2E tests pass: `npx playwright test` -3. ✅ Admin pages verified (200 status, no 500 errors) -4. ✅ All changes committed to main branch +#### Workflow 1: prepare-release.yml (Release Creation) -**Deployment via Gitea Actions (CI/CD)**: +**Purpose**: Create a release with built artifact -**Option A: Automatic (on push to main)** +**Trigger**: Manual (`workflow_dispatch`) ```bash -git push origin main -# → Gitea Actions automatically triggers deploy-prod.yml -# → Build, deploy, health checks run automatically +# Visit Gitea Actions and select prepare-release.yml +# Input version: v0.1.20260711 (or any semantic version) ``` -**Option B: Manual (workflow_dispatch)** -1. Visit: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions -2. Click "Deploy to Production" workflow -3. Click "Run workflow" button -4. Monitor execution in Gitea Actions UI +**What it does**: +1. ✓ Build (restore, build, publish) +2. ✓ Generate `appsettings.Production.json` +3. ✓ Package artifact: `.tar.gz` +4. ✓ Create git tag: `v0.1.20260711` +5. ✓ Create Gitea Release with artifact attached +6. ✓ Notify: Release ready for deployment -**Deployment Pipeline (Automatic - 7 Stages)**: +**Output**: Gitea Release with downloadable artifact + +#### Workflow 2: deploy-prod.yml (Deployment) + +**Purpose**: Deploy a release to production + +**Trigger**: Manual (`workflow_dispatch`) +```bash +# Visit Gitea Actions and select deploy-prod.yml +# Input release: v0.1.20260711 (optional — uses latest if empty) +``` + +**What it does**: +1. ✓ Fetch Release (from Gitea Releases) +2. ✓ Download artifact +3. ✓ Verify SSH credentials +4. ✓ Upload to production server +5. ✓ Extract and symlink +6. ✓ Restart service +7. ✓ 5-point health checks +8. ✓ Report deployment status + +**Deployment Pipeline (5 Stages)**: | Stage | Purpose | Timeout | |-------|---------|---------| -| 1. Build | Restore, build, publish Release | 15min | -| 2. Pre-Check | Verify SSH keys, secrets, artifact | 5min | -| 3. Deploy | Upload artifact, extract, symlink, restart | 30min | -| 4. Health Check | 5-point verification (HTTP, CSS, login, service, commit) | 10min | +| 1. Fetch Release | Query Gitea Releases, download artifact | 10min | +| 2. Pre-Check | Verify SSH keys, secrets, release | 5min | +| 3. Deploy | Upload, extract, symlink, restart service | 30min | +| 4. Health Check | 5-point verification (HTTP, CSS, login, service, release) | 10min | | 5. Report | Final deployment status | Auto | **Health Checks (Automatic)**: @@ -161,7 +181,62 @@ git push origin main - ✓ Login page content verification - ✓ CSS file loads (`/css/admin.css`) - ✓ Service status (systemctl active) -- ✓ Commit hash verification (deployed version matches) +- ✓ Release verification (deployed release tag matches) + +**Complete Deployment Flow**: +``` +1. Code committed to main branch +2. Create release: prepare-release.yml workflow_dispatch (manual) + → Builds code + → Creates Gitea Release with artifact + → Tags repository +3. Deploy release: deploy-prod.yml workflow_dispatch (manual) + → Selects release version + → Downloads artifact from Gitea Release + → Deploys to production server + → Runs health checks + → Reports status +``` + +### Pre-Deployment Checklist + +**Before creating a release**, verify: +1. ✅ Local build: `dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj -c Release` (0 errors, 0 warnings) +2. ✅ E2E tests pass: `npx playwright test` +3. ✅ Admin pages verified (200 status, no 500 errors) +4. ✅ All changes committed and pushed to main branch +5. ✅ No uncommitted changes: `git status` + +### Release & Deployment Workflow + +**Step 1: Create Release (prepare-release.yml)** +```bash +# Visit Gitea Actions +# https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions + +# Run prepare-release.yml workflow +# Input: version = v0.1.20260711 + +# Workflow will: +# - Build and publish +# - Package artifact +# - Create git tag +# - Create Gitea Release +# - Attach artifact +``` + +**Step 2: Deploy Release (deploy-prod.yml)** +```bash +# Visit Gitea Actions (same page) +# Run deploy-prod.yml workflow +# Input: release = v0.1.20260711 (leave empty for latest) + +# Workflow will: +# - Download artifact from release +# - Deploy to production server +# - Run health checks +# - Report status +``` ### SSH Key Configuration (Required) @@ -178,15 +253,22 @@ git push origin main 3. Get private key in base64 format: ```bash - base64 -w 0 ~/.ssh/quantengine_deploy | wc -c - base64 -w 0 ~/.ssh/quantengine_deploy | pbcopy # macOS - # On Windows: Get-Content ~/.ssh/quantengine_deploy -Raw | [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($_)) | Set-Clipboard + # macOS/Linux + base64 -w 0 ~/.ssh/quantengine_deploy > /tmp/key_b64.txt + cat /tmp/key_b64.txt | pbcopy + + # Or Windows PowerShell + $key = Get-Content ~/.ssh/quantengine_deploy -Raw + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($key)) | Set-Clipboard ``` 4. Configure in Gitea: - URL: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/settings/secrets - Add secret: `DEPLOY_SSH_KEY_B64` (base64-encoded private key) - Or: `DEPLOY_SSH_KEY` (raw PEM format) + - Also add: `GITEA_TOKEN` (for release API access) + - Generate at: https://gitea.taxbaik.com/user/settings/applications + - Required permissions: `repo` + `read:actions` ### Deployment Monitoring