cc94d5aeae
Found via SSH log analysis (actions_log/.../2332.log, Run #2002): 1. This Gitea Actions instance's runner explicitly rejects the actions/upload-artifact@v4 / download-artifact@v4 protocol: "GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+ are not currently supported on GHES." The old 3-job split (fetch-release -> pre-deploy-check -> deploy) relied on upload-artifact/download-artifact to hand the .tar.gz from the fetch job to the deploy job, so it could never succeed on this server regardless of any other fix. 2. Independently, the guessed download URL pattern /releases/download/{tag}/{filename} doesn't exist on this Gitea instance -- it silently downloaded a 19-byte "404 page not found" body as if it were the artifact (curl exited 0, file "existed"). Fixes: - Merge fetch-release + pre-deploy-check + deploy into a single `deploy` job so the downloaded artifact never needs to cross a job boundary -- it's downloaded and scp'd from the same runner filesystem in one shot. - Fetch the real `browser_download_url` from the release JSON instead of constructing the URL by convention. - Add a `file "$ARTIFACT" | grep -q "gzip compressed"` guard right after download so a wrong-URL / error-page download fails loudly instead of silently proceeding with garbage bytes. - Update post-deploy-check / post-deploy-report to read from `needs.deploy.outputs.*` now that fetch-release no longer exists as a separate job.
340 lines
13 KiB
YAML
340 lines
13 KiB
YAML
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:
|
|
deploy:
|
|
name: Deploy to Production
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
outputs:
|
|
release-tag: ${{ steps.fetch.outputs.tag }}
|
|
artifact-name: ${{ steps.fetch.outputs.artifact }}
|
|
commit-hash: ${{ steps.fetch.outputs.commit }}
|
|
|
|
steps:
|
|
- name: Verify SSH Key and Secrets
|
|
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
|
|
[ -z "${{ secrets.GITEA_TOKEN }}" ] && { echo "ERROR: GITEA_TOKEN not configured"; exit 1; }
|
|
echo "✓ SSH key and GITEA_TOKEN configured"
|
|
|
|
- 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
|
|
RELEASE_URL="https://gitea.taxbaik.com/api/v1/repos/$REPO/releases/latest"
|
|
else
|
|
RELEASE_URL="https://gitea.taxbaik.com/api/v1/repos/$REPO/releases/tags/$RELEASE_INPUT"
|
|
fi
|
|
|
|
RELEASE=$(curl -sf -H "Authorization: token $TOKEN" "$RELEASE_URL")
|
|
TAG=$(echo "$RELEASE" | jq -r '.tag_name')
|
|
COMMIT=$(echo "$RELEASE" | jq -r '.target_commitish' | cut -c1-7)
|
|
ARTIFACT=$(echo "$RELEASE" | jq -r '.assets[0].name')
|
|
DOWNLOAD_URL=$(echo "$RELEASE" | jq -r '.assets[0].browser_download_url')
|
|
|
|
if [ "$TAG" = "null" ] || [ -z "$TAG" ]; then
|
|
echo "ERROR: Release not found"; exit 1
|
|
fi
|
|
if [ "$ARTIFACT" = "null" ] || [ -z "$ARTIFACT" ]; then
|
|
echo "ERROR: No artifacts found in release $TAG"; exit 1
|
|
fi
|
|
if [ "$DOWNLOAD_URL" = "null" ] || [ -z "$DOWNLOAD_URL" ]; then
|
|
echo "ERROR: No browser_download_url found for asset"; exit 1
|
|
fi
|
|
|
|
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
|
echo "artifact=${ARTIFACT}" >> $GITHUB_OUTPUT
|
|
echo "download_url=${DOWNLOAD_URL}" >> $GITHUB_OUTPUT
|
|
echo "commit=${COMMIT}" >> $GITHUB_OUTPUT
|
|
|
|
echo "✓ Release: $TAG"
|
|
echo "✓ Artifact: $ARTIFACT"
|
|
echo "✓ Download URL: $DOWNLOAD_URL"
|
|
|
|
- name: Download Release Artifact
|
|
run: |
|
|
ARTIFACT="${{ steps.fetch.outputs.artifact }}"
|
|
TOKEN="${{ secrets.GITEA_TOKEN }}"
|
|
DOWNLOAD_URL="${{ steps.fetch.outputs.download_url }}"
|
|
|
|
echo "Downloading: $DOWNLOAD_URL"
|
|
curl -sfL -H "Authorization: token $TOKEN" -o "$ARTIFACT" "$DOWNLOAD_URL"
|
|
|
|
# A 404/error page would still create a small file -- verify it's a
|
|
# real gzip archive, not an HTML/JSON error body (this is exactly
|
|
# how the old /releases/download/{tag}/{file} guessed URL failed
|
|
# silently: curl exited 0 but wrote a 19-byte "404 page not found").
|
|
file "$ARTIFACT" | grep -q "gzip compressed" || {
|
|
echo "ERROR: Downloaded file is not a valid gzip archive:"
|
|
file "$ARTIFACT"
|
|
cat "$ARTIFACT"
|
|
exit 1
|
|
}
|
|
|
|
echo "✓ Downloaded: $(du -sh $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="${{ steps.fetch.outputs.artifact }}"
|
|
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="${{ steps.fetch.outputs.artifact }}"
|
|
RELEASE_TAG="${{ steps.fetch.outputs.tag }}"
|
|
COMMIT="${{ steps.fetch.outputs.commit }}"
|
|
|
|
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: 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.deploy.outputs.release-tag }} (commit: ${{ needs.deploy.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: [ deploy, post-deploy-check ]
|
|
|
|
steps:
|
|
- name: Report Status
|
|
run: |
|
|
RELEASE="${{ needs.deploy.outputs.release-tag }}"
|
|
COMMIT="${{ needs.deploy.outputs.commit-hash }}"
|
|
ARTIFACT="${{ needs.deploy.outputs.artifact-name }}"
|
|
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 "Deploy: $([ "$DEPLOY_STATUS" = "success" ] && echo "✓" || echo "✗") $DEPLOY_STATUS"
|
|
echo "Health: $([ "$CHECK_STATUS" = "success" ] && echo "✓" || echo "✗") $CHECK_STATUS"
|
|
echo ""
|
|
|
|
if [ "$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
|