feat(ci): P1-4 前端预览环境实施(轻量版) #482
@@ -0,0 +1,118 @@
|
||||
name: Preview Cleanup
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
cleanup-preview:
|
||||
name: Cleanup Preview Environment
|
||||
runs-on: runtime-builder
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Extract PR number
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||')
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
echo "PR number: $PR_NUMBER"
|
||||
echo "Preview dir: /var/www/preview/pr-${PR_NUMBER}"
|
||||
|
||||
- name: Install SSH client
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
apt-get update -qq && apt-get install -y -qq openssh-client >/dev/null 2>&1
|
||||
echo "openssh-client installed"
|
||||
|
||||
- name: Remove preview directory from server
|
||||
shell: sh
|
||||
env:
|
||||
STAGING_SSH_HOST: ${{ secrets.STAGING_SSH_HOST }}
|
||||
STAGING_SSH_USER: ${{ secrets.STAGING_SSH_USER }}
|
||||
STAGING_SSH_PORT: ${{ secrets.STAGING_SSH_PORT }}
|
||||
STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
|
||||
run: |
|
||||
set -eux
|
||||
staging_host="${STAGING_SSH_HOST:-47.98.113.167}"
|
||||
staging_user="${STAGING_SSH_USER:-root}"
|
||||
staging_port="${STAGING_SSH_PORT:-22222}"
|
||||
preview_dir="/var/www/preview/pr-${PR_NUMBER}"
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
|
||||
# 查找可用的SSH密钥
|
||||
key_path=""
|
||||
if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then
|
||||
key_path="/root/.ssh/xiaoxia_runtime_builder"
|
||||
echo "Using key: $key_path (builder key)"
|
||||
elif [ -f "$HOME/.ssh/xiaoxia_runtime_builder" ]; then
|
||||
key_path="$HOME/.ssh/xiaoxia_runtime_builder"
|
||||
echo "Using key: $key_path (home key)"
|
||||
elif [ -n "${STAGING_SSH_KEY:-}" ]; then
|
||||
key_path="$HOME/.ssh/id_ed25519"
|
||||
printf '%s\n' "$STAGING_SSH_KEY" > "$key_path"
|
||||
chmod 600 "$key_path"
|
||||
echo "Using key from STAGING_SSH_KEY secret"
|
||||
else
|
||||
echo "ERROR: No SSH key available"
|
||||
ls -la ~/.ssh/ 2>/dev/null || true
|
||||
ls -la /root/.ssh/ 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ssh-keyscan -p "$staging_port" -H "$staging_host" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
echo "SSH keyscan done"
|
||||
|
||||
# 测试SSH连接
|
||||
ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" "echo SSH_CONNECTION_OK && hostname"
|
||||
echo "SSH connection verified"
|
||||
|
||||
# 检查目录是否存在
|
||||
DIR_EXISTS=$(ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" \
|
||||
"if [ -d '${preview_dir}' ]; then echo 'yes'; else echo 'no'; fi")
|
||||
|
||||
if [ "$DIR_EXISTS" = "yes" ]; then
|
||||
echo "Removing preview directory: ${preview_dir}"
|
||||
ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" \
|
||||
"rm -rf ${preview_dir} && echo 'Preview directory removed successfully'"
|
||||
echo "Cleanup completed: ${preview_dir}"
|
||||
else
|
||||
echo "Preview directory does not exist: ${preview_dir}, nothing to clean up"
|
||||
fi
|
||||
|
||||
- name: Comment cleanup notice on PR
|
||||
if: success()
|
||||
shell: sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -eu
|
||||
PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||')
|
||||
|
||||
COMMENT_BODY=$(python3 -c "
|
||||
import json, os
|
||||
pr_num = os.environ['PR_NUMBER']
|
||||
body = f'''🗑️ **预览环境已清理**
|
||||
|
||||
PR #{pr_num} 已关闭或合并,对应的预览环境已被清理。
|
||||
|
||||
> 如有需要,可以重新打开 PR 来重新生成预览环境。
|
||||
'''
|
||||
print(json.dumps({'body': body}))
|
||||
")
|
||||
|
||||
API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments"
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$COMMENT_BODY" \
|
||||
"$API_URL" \
|
||||
> /dev/null
|
||||
echo "Cleanup comment posted"
|
||||
@@ -0,0 +1,293 @@
|
||||
name: Preview Deploy
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
reason:
|
||||
description: "触发原因"
|
||||
required: false
|
||||
default: "手动触发 - 预览环境补跑"
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
concurrency:
|
||||
group: preview-deploy-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
deploy-preview:
|
||||
name: Deploy Preview Environment
|
||||
runs-on: runtime-builder
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout code
|
||||
shell: sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -eu
|
||||
python3 - <<'PY'
|
||||
import io, os, tarfile, time, urllib.request, urllib.error
|
||||
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
||||
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
||||
last_err = None
|
||||
for attempt in range(5):
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=120) as response:
|
||||
archive = response.read()
|
||||
break
|
||||
except urllib.error.HTTPError as e:
|
||||
last_err = e
|
||||
if e.code >= 500 and attempt < 4:
|
||||
wait = 2 ** attempt
|
||||
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||
time.sleep(wait)
|
||||
continue
|
||||
raise
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
if attempt < 4:
|
||||
wait = 2 ** attempt
|
||||
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||
time.sleep(wait)
|
||||
continue
|
||||
raise
|
||||
else:
|
||||
raise last_err
|
||||
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
||||
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
|
||||
for member in tar.getmembers():
|
||||
name = member.name
|
||||
if name == root_prefix[:-1]:
|
||||
continue
|
||||
if name.startswith(root_prefix):
|
||||
member.name = name[len(root_prefix):]
|
||||
if member.name:
|
||||
tar.extract(member, '.')
|
||||
PY
|
||||
|
||||
- name: Record job start time
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
echo "JOB_START_TIME=$(date +%s)" >> $GITHUB_ENV
|
||||
echo "Job started at $(date)"
|
||||
|
||||
- name: Extract PR number
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||')
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
echo "PR number: $PR_NUMBER"
|
||||
echo "PREVIEW_URL=https://pr-${PR_NUMBER}.preview.xiaoxiajianji.com" >> $GITHUB_ENV
|
||||
echo "Preview URL: https://pr-${PR_NUMBER}.preview.xiaoxiajianji.com"
|
||||
|
||||
- name: Build frontend
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
NPM_CACHE_VOLUME="xiaoxia-npm-cache"
|
||||
if ! docker volume inspect "$NPM_CACHE_VOLUME" >/dev/null 2>&1; then
|
||||
docker volume create "$NPM_CACHE_VOLUME" >/dev/null
|
||||
echo "Created npm cache volume: $NPM_CACHE_VOLUME"
|
||||
fi
|
||||
|
||||
docker run --rm \
|
||||
-v "$PWD:/workspace" \
|
||||
-v "$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules" \
|
||||
-w /workspace/apps/web \
|
||||
-e VITE_API_URL=https://staging-api.xiaoxiajianji.com \
|
||||
docker.m.daocloud.io/library/node:20 \
|
||||
sh -lc '
|
||||
PACKAGE_LOCK_HASH=$(md5sum package-lock.json 2>/dev/null | cut -d" " -f1)
|
||||
CACHE_HASH_FILE="node_modules/.package-lock-hash"
|
||||
CACHE_VALID=false
|
||||
if [ -f "$CACHE_HASH_FILE" ] && [ "$(cat "$CACHE_HASH_FILE")" = "$PACKAGE_LOCK_HASH" ] && [ -x "node_modules/.bin/vite" ] && [ -x "node_modules/.bin/tsc" ]; then
|
||||
CACHE_VALID=true
|
||||
echo "Cache hit: dependencies valid, skipping npm ci"
|
||||
fi
|
||||
if [ "$CACHE_VALID" = "false" ]; then
|
||||
echo "Cache miss or invalid: running npm ci..."
|
||||
if ! npm ci --include=dev; then
|
||||
echo "npm ci failed, cleaning node_modules and retrying..."
|
||||
rm -rf node_modules
|
||||
mkdir -p node_modules
|
||||
npm ci --include=dev
|
||||
fi
|
||||
echo "$PACKAGE_LOCK_HASH" > "$CACHE_HASH_FILE"
|
||||
echo "Dependencies installed, cache updated"
|
||||
fi
|
||||
echo "Running TypeScript check..."
|
||||
npx --no-install tsc
|
||||
echo "Running Vite build..."
|
||||
npx --no-install vite build
|
||||
echo "Build completed successfully"
|
||||
ls -la dist/
|
||||
'
|
||||
|
||||
- name: Install SSH client
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
apt-get update -qq && apt-get install -y -qq openssh-client rsync >/dev/null 2>&1
|
||||
echo "openssh-client and rsync installed"
|
||||
|
||||
- name: Deploy preview to server
|
||||
shell: sh
|
||||
env:
|
||||
STAGING_SSH_HOST: ${{ secrets.STAGING_SSH_HOST }}
|
||||
STAGING_SSH_USER: ${{ secrets.STAGING_SSH_USER }}
|
||||
STAGING_SSH_PORT: ${{ secrets.STAGING_SSH_PORT }}
|
||||
STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
|
||||
run: |
|
||||
set -eux
|
||||
staging_host="${STAGING_SSH_HOST:-47.98.113.167}"
|
||||
staging_user="${STAGING_SSH_USER:-root}"
|
||||
staging_port="${STAGING_SSH_PORT:-22222}"
|
||||
preview_dir="/var/www/preview/pr-${PR_NUMBER}"
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
|
||||
# 查找可用的SSH密钥
|
||||
key_path=""
|
||||
if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then
|
||||
key_path="/root/.ssh/xiaoxia_runtime_builder"
|
||||
echo "Using key: $key_path (builder key)"
|
||||
elif [ -f "$HOME/.ssh/xiaoxia_runtime_builder" ]; then
|
||||
key_path="$HOME/.ssh/xiaoxia_runtime_builder"
|
||||
echo "Using key: $key_path (home key)"
|
||||
elif [ -n "${STAGING_SSH_KEY:-}" ]; then
|
||||
key_path="$HOME/.ssh/id_ed25519"
|
||||
printf '%s\n' "$STAGING_SSH_KEY" > "$key_path"
|
||||
chmod 600 "$key_path"
|
||||
echo "Using key from STAGING_SSH_KEY secret"
|
||||
else
|
||||
echo "ERROR: No SSH key available"
|
||||
ls -la ~/.ssh/ 2>/dev/null || true
|
||||
ls -la /root/.ssh/ 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ssh-keyscan -p "$staging_port" -H "$staging_host" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
echo "SSH keyscan done"
|
||||
|
||||
# 测试SSH连接
|
||||
ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" "echo SSH_CONNECTION_OK && hostname"
|
||||
echo "SSH connection verified"
|
||||
|
||||
# 创建预览目录并上传文件
|
||||
ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" \
|
||||
"mkdir -p ${preview_dir} && echo 'Preview directory created: ${preview_dir}'"
|
||||
|
||||
# 使用rsync上传dist目录内容
|
||||
rsync -avz --delete -e "ssh -p ${staging_port} -i ${key_path} -o StrictHostKeyChecking=no" \
|
||||
apps/web/dist/ \
|
||||
"${staging_user}@${staging_host}:${preview_dir}/"
|
||||
|
||||
echo "Preview deployed to: ${preview_dir}"
|
||||
echo "Preview URL: https://pr-${PR_NUMBER}.preview.xiaoxiajianji.com"
|
||||
|
||||
- name: Comment preview link on PR
|
||||
shell: sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -eu
|
||||
PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||')
|
||||
PREVIEW_URL="https://pr-${PR_NUMBER}.preview.xiaoxiajianji.com"
|
||||
|
||||
# 构建评论内容
|
||||
COMMENT_BODY=$(python3 -c "
|
||||
import json, os
|
||||
pr_num = os.environ['PR_NUMBER']
|
||||
url = os.environ['PREVIEW_URL']
|
||||
body = f'''🚀 **预览环境已部署**
|
||||
|
||||
| 项目 | 详情 |
|
||||
|------|------|
|
||||
| PR号 | #{pr_num} |
|
||||
| 预览链接 | [${url}](${url}) |
|
||||
| API环境 | staging |
|
||||
|
||||
> 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。
|
||||
>
|
||||
> 🔄 每次提交新代码后预览环境会自动更新。
|
||||
>
|
||||
> 🗑️ PR 关闭或合并后,预览环境会自动清理。
|
||||
'''
|
||||
print(json.dumps({'body': body}))
|
||||
")
|
||||
|
||||
# 查找是否已有预览评论
|
||||
API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments"
|
||||
EXISTING_COMMENT_ID=""
|
||||
|
||||
COMMENTS=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL")
|
||||
EXISTING_COMMENT_ID=$(echo "$COMMENTS" | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
comments = json.load(sys.stdin)
|
||||
for c in comments:
|
||||
body = c.get('body', '')
|
||||
if '预览环境已部署' in body and 'Preview URL' not in body:
|
||||
print(c['id'])
|
||||
break
|
||||
except:
|
||||
print('')
|
||||
")
|
||||
|
||||
if [ -n "$EXISTING_COMMENT_ID" ]; then
|
||||
# 更新已有评论
|
||||
echo "Updating existing comment: $EXISTING_COMMENT_ID"
|
||||
curl -s -X PATCH \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$COMMENT_BODY" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/comments/${EXISTING_COMMENT_ID}" \
|
||||
> /dev/null
|
||||
echo "Comment updated"
|
||||
else
|
||||
# 发布新评论
|
||||
echo "Creating new comment"
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$COMMENT_BODY" \
|
||||
"$API_URL" \
|
||||
> /dev/null
|
||||
echo "Comment posted"
|
||||
fi
|
||||
|
||||
- name: Job duration summary
|
||||
if: always()
|
||||
shell: sh
|
||||
run: |
|
||||
set +eu
|
||||
if [ -n "$JOB_START_TIME" ]; then
|
||||
END_TIME=$(date +%s)
|
||||
DURATION=$((END_TIME - JOB_START_TIME))
|
||||
MINS=$((DURATION / 60))
|
||||
SECS=$((DURATION % 60))
|
||||
echo "JOB_DURATION_SECONDS=$DURATION" >> $GITHUB_ENV
|
||||
echo "=== Job Duration: ${MINS}m${SECS}s ==="
|
||||
else
|
||||
echo "JOB_DURATION_SECONDS=0" >> $GITHUB_ENV
|
||||
echo "=== Job Duration: unknown ==="
|
||||
fi
|
||||
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }}
|
||||
run: |
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Deploy Preview Environment" python3 scripts/ci_notify.py
|
||||
@@ -0,0 +1,264 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# 预览环境服务器初始化脚本
|
||||
# 用途:在业务服务器上创建预览环境所需的目录和配置
|
||||
# 使用方式:bash scripts/ci/preview_init_server.sh
|
||||
# ============================================================
|
||||
|
||||
set -eu
|
||||
|
||||
PREVIEW_ROOT="/var/www/preview"
|
||||
NGINX_CONF_PATH="/etc/nginx/conf.d/preview.conf"
|
||||
DOMAIN="xiaoxiajianji.com"
|
||||
STAGING_API="https://staging-api.xiaoxiajianji.com"
|
||||
|
||||
echo "=========================================="
|
||||
echo " 预览环境服务器初始化"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 1. 创建预览根目录
|
||||
echo "[1/4] 创建预览根目录..."
|
||||
if [ -d "$PREVIEW_ROOT" ]; then
|
||||
echo " 目录已存在: $PREVIEW_ROOT"
|
||||
else
|
||||
mkdir -p "$PREVIEW_ROOT"
|
||||
echo " 已创建: $PREVIEW_ROOT"
|
||||
fi
|
||||
chown -R root:root "$PREVIEW_ROOT"
|
||||
chmod -R 755 "$PREVIEW_ROOT"
|
||||
echo ""
|
||||
|
||||
# 2. 创建测试页面(验证Nginx配置用)
|
||||
echo "[2/4] 创建测试页面..."
|
||||
TEST_DIR="${PREVIEW_ROOT}/pr-demo"
|
||||
mkdir -p "$TEST_DIR"
|
||||
cat > "$TEST_DIR/index.html" <<'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>预览环境测试页</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
min-height: 100vh; margin: 0; background: #f5f5f5; }
|
||||
.card { background: white; padding: 40px; border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1); text-align: center; max-width: 400px; }
|
||||
h1 { color: #2d3748; margin-top: 0; }
|
||||
.success { color: #38a169; font-size: 48px; margin: 20px 0; }
|
||||
p { color: #718096; line-height: 1.6; }
|
||||
code { background: #edf2f7; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="success">✅</div>
|
||||
<h1>预览环境配置成功!</h1>
|
||||
<p>如果你能看到这个页面,说明 Nginx 预览环境配置正确。</p>
|
||||
<p>当前站点通过子域名 <code>pr-demo.preview</code> 路由到 <code>/var/www/preview/pr-demo/</code> 目录。</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
echo " 测试页面已创建: $TEST_DIR/index.html"
|
||||
echo ""
|
||||
|
||||
# 3. 检查Nginx是否安装
|
||||
echo "[3/4] 检查Nginx环境..."
|
||||
if command -v nginx > /dev/null 2>&1; then
|
||||
echo " Nginx 已安装: $(nginx -v 2>&1)"
|
||||
NGINX_INSTALLED=true
|
||||
else
|
||||
echo " ⚠️ Nginx 未安装,请先安装 Nginx"
|
||||
NGINX_INSTALLED=false
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 4. 输出Nginx配置建议
|
||||
echo "[4/4] Nginx 配置建议"
|
||||
echo ""
|
||||
echo "----------------------------------------"
|
||||
echo " 请将以下配置保存到: $NGINX_CONF_PATH"
|
||||
echo " 或复制到 Nginx 配置目录中"
|
||||
echo "----------------------------------------"
|
||||
echo ""
|
||||
|
||||
cat <<'NGINX_CONF'
|
||||
# ============================================================
|
||||
# 预览环境 Nginx 配置
|
||||
# 支持 *.preview.xiaoxiajianji.com 通配符子域名
|
||||
# ============================================================
|
||||
|
||||
# 从子域名中提取 PR 号(如 pr-123.preview -> pr-123)
|
||||
map $host $preview_pr {
|
||||
default "";
|
||||
~^(?<pr>pr-\d+)\.preview\.xiaoxiajianji\.com$ $pr;
|
||||
}
|
||||
|
||||
# HTTP 服务器(80端口)
|
||||
server {
|
||||
listen 80;
|
||||
server_name *.preview.xiaoxiajianji.com;
|
||||
|
||||
# 根目录根据子域名动态映射
|
||||
root /var/www/preview/$preview_pr;
|
||||
|
||||
# 索引文件
|
||||
index index.html;
|
||||
|
||||
# 字符集
|
||||
charset utf-8;
|
||||
|
||||
# 访问日志
|
||||
access_log /var/log/nginx/preview_access.log;
|
||||
error_log /var/log/nginx/preview_error.log warn;
|
||||
|
||||
# 如果子域名格式不正确,返回404
|
||||
if ($preview_pr = "") {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# 如果预览目录不存在,返回404
|
||||
if (!-d $document_root) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# API 反向代理到 staging 环境
|
||||
location /api/ {
|
||||
proxy_pass https://staging-api.xiaoxiajianji.com/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host staging-api.xiaoxiajianji.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
# 超时设置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# 缓冲设置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
|
||||
# WebSocket 支持(如需要)
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
location /assets/ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# SPA 路由支持
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 安全相关响应头
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# 禁止隐藏文件访问
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS 服务器(443端口)
|
||||
# 注意:需要先配置 SSL 证书
|
||||
# 建议使用 certbot 或手动配置证书
|
||||
#
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name *.preview.xiaoxiajianji.com;
|
||||
#
|
||||
# # SSL 证书配置(请替换为实际证书路径)
|
||||
# ssl_certificate /path/to/fullchain.pem;
|
||||
# ssl_certificate_key /path/to/privkey.pem;
|
||||
#
|
||||
# # SSL 安全配置
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
# ssl_prefer_server_ciphers on;
|
||||
# ssl_session_cache shared:SSL:10m;
|
||||
# ssl_session_timeout 10m;
|
||||
#
|
||||
# # 其余配置与 HTTP 相同
|
||||
# root /var/www/preview/$preview_pr;
|
||||
# index index.html;
|
||||
# charset utf-8;
|
||||
#
|
||||
# access_log /var/log/nginx/preview_ssl_access.log;
|
||||
# error_log /var/log/nginx/preview_ssl_error.log warn;
|
||||
#
|
||||
# if ($preview_pr = "") {
|
||||
# return 404;
|
||||
# }
|
||||
#
|
||||
# if (!-d $document_root) {
|
||||
# return 404;
|
||||
# }
|
||||
#
|
||||
# location /api/ {
|
||||
# proxy_pass https://staging-api.xiaoxiajianji.com/api/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host staging-api.xiaoxiajianji.com;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_set_header X-Forwarded-Host $host;
|
||||
# proxy_connect_timeout 30s;
|
||||
# proxy_send_timeout 60s;
|
||||
# proxy_read_timeout 60s;
|
||||
# }
|
||||
#
|
||||
# location /assets/ {
|
||||
# expires 7d;
|
||||
# add_header Cache-Control "public, max-age=604800, immutable";
|
||||
# try_files $uri =404;
|
||||
# }
|
||||
#
|
||||
# location / {
|
||||
# try_files $uri $uri/ /index.html;
|
||||
# }
|
||||
#
|
||||
# add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
# add_header X-Content-Type-Options "nosniff" always;
|
||||
# add_header X-XSS-Protection "1; mode=block" always;
|
||||
#
|
||||
# location ~ /\. {
|
||||
# deny all;
|
||||
# access_log off;
|
||||
# log_not_found off;
|
||||
# }
|
||||
# }
|
||||
NGINX_CONF
|
||||
|
||||
echo ""
|
||||
echo "----------------------------------------"
|
||||
echo " 配置完成后的操作步骤:"
|
||||
echo "----------------------------------------"
|
||||
echo ""
|
||||
echo "1. 将上面的 Nginx 配置保存到合适的位置(如 /etc/nginx/conf.d/preview.conf)"
|
||||
echo "2. 测试配置: nginx -t"
|
||||
echo "3. 重载配置: nginx -s reload"
|
||||
echo "4. 配置 DNS 解析: 将 *.preview.xiaoxiajianji.com 指向服务器 IP"
|
||||
echo "5. 配置 SSL 证书(推荐使用 Let's Encrypt 通配符证书)"
|
||||
echo ""
|
||||
echo "测试方式:"
|
||||
echo " 访问 http://pr-demo.preview.xiaoxiajianji.com 验证配置"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 初始化完成"
|
||||
echo "=========================================="
|
||||
@@ -0,0 +1,226 @@
|
||||
# ============================================================
|
||||
# 预览环境 Nginx 配置模板
|
||||
# 支持 *.preview.xiaoxiajianji.com 通配符子域名
|
||||
#
|
||||
# 使用方法:
|
||||
# 1. 将本文件复制到 Nginx 配置目录(如 /etc/nginx/conf.d/preview.conf)
|
||||
# 2. 根据实际情况修改域名和 API 地址
|
||||
# 3. 运行 nginx -t 测试配置
|
||||
# 4. 运行 nginx -s reload 重载配置
|
||||
#
|
||||
# 前置条件:
|
||||
# - DNS 已配置 *.preview.xiaoxiajianji.com 指向本服务器
|
||||
# - 预览根目录已创建:/var/www/preview/
|
||||
# - 每个 PR 的静态文件放在 /var/www/preview/pr-{N}/ 下
|
||||
# ============================================================
|
||||
|
||||
# ---- 变量定义 ----
|
||||
# 从子域名中提取 PR 号(如 pr-123.preview -> pr-123)
|
||||
map $host $preview_pr {
|
||||
default "";
|
||||
~^(?<pr>pr-\d+)\.preview\.xiaoxiajianji\.com$ $pr;
|
||||
}
|
||||
|
||||
# ---- HTTP 服务器(80端口) ----
|
||||
server {
|
||||
listen 80;
|
||||
server_name *.preview.xiaoxiajianji.com;
|
||||
|
||||
# 根目录根据子域名动态映射
|
||||
root /var/www/preview/$preview_pr;
|
||||
|
||||
# 索引文件
|
||||
index index.html;
|
||||
|
||||
# 字符集
|
||||
charset utf-8;
|
||||
|
||||
# 访问日志
|
||||
access_log /var/log/nginx/preview_access.log;
|
||||
error_log /var/log/nginx/preview_error.log warn;
|
||||
|
||||
# 如果子域名格式不正确,返回404
|
||||
if ($preview_pr = "") {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# 如果预览目录不存在,返回404
|
||||
if (!-d $document_root) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# ---- API 反向代理到 staging 环境 ----
|
||||
location /api/ {
|
||||
proxy_pass https://staging-api.xiaoxiajianji.com/api/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# 请求头设置
|
||||
proxy_set_header Host staging-api.xiaoxiajianji.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
# 超时设置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# 缓冲设置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
|
||||
# 重定向跟随
|
||||
proxy_redirect off;
|
||||
|
||||
# WebSocket 支持(如需要,取消注释)
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# ---- 生成文件代理(如需要) ----
|
||||
# location /generated-files/ {
|
||||
# proxy_pass https://staging-api.xiaoxiajianji.com/generated-files/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host staging-api.xiaoxiajianji.com;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# }
|
||||
|
||||
# ---- 静态资源缓存 ----
|
||||
location /assets/ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# ---- SPA 路由支持 ----
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# ---- 安全相关响应头 ----
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# ---- 禁止隐藏文件访问 ----
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# ---- 禁止敏感文件访问 ----
|
||||
location ~* \.(env|log|sql|bak|swp|tmp|zip|tar|gz)$ {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# HTTPS 服务器配置(可选,需要 SSL 证书)
|
||||
#
|
||||
# 推荐使用 Let's Encrypt 通配符证书:
|
||||
# certbot certonly --dns-xxx -d "*.preview.xiaoxiajianji.com"
|
||||
#
|
||||
# 启用方法:取消下方注释,并修改证书路径
|
||||
# ============================================================
|
||||
#
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name *.preview.xiaoxiajianji.com;
|
||||
#
|
||||
# # SSL 证书配置
|
||||
# ssl_certificate /etc/letsencrypt/live/preview.xiaoxiajianji.com/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/preview.xiaoxiajianji.com/privkey.pem;
|
||||
#
|
||||
# # SSL 安全配置
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
# ssl_prefer_server_ciphers off;
|
||||
# ssl_session_cache shared:SSL:10m;
|
||||
# ssl_session_timeout 10m;
|
||||
# ssl_session_tickets off;
|
||||
#
|
||||
# # OCSP Stapling
|
||||
# ssl_stapling on;
|
||||
# ssl_stapling_verify on;
|
||||
#
|
||||
# # 根目录根据子域名动态映射
|
||||
# root /var/www/preview/$preview_pr;
|
||||
#
|
||||
# # 索引文件
|
||||
# index index.html;
|
||||
#
|
||||
# # 字符集
|
||||
# charset utf-8;
|
||||
#
|
||||
# # 访问日志
|
||||
# access_log /var/log/nginx/preview_ssl_access.log;
|
||||
# error_log /var/log/nginx/preview_ssl_error.log warn;
|
||||
#
|
||||
# # 如果子域名格式不正确,返回404
|
||||
# if ($preview_pr = "") {
|
||||
# return 404;
|
||||
# }
|
||||
#
|
||||
# # 如果预览目录不存在,返回404
|
||||
# if (!-d $document_root) {
|
||||
# return 404;
|
||||
# }
|
||||
#
|
||||
# # API 反向代理到 staging 环境
|
||||
# location /api/ {
|
||||
# proxy_pass https://staging-api.xiaoxiajianji.com/api/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host staging-api.xiaoxiajianji.com;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_set_header X-Forwarded-Host $host;
|
||||
# proxy_connect_timeout 30s;
|
||||
# proxy_send_timeout 60s;
|
||||
# proxy_read_timeout 60s;
|
||||
# proxy_buffering on;
|
||||
# proxy_buffer_size 4k;
|
||||
# proxy_buffers 8 4k;
|
||||
# }
|
||||
#
|
||||
# # 静态资源缓存
|
||||
# location /assets/ {
|
||||
# expires 7d;
|
||||
# add_header Cache-Control "public, max-age=604800, immutable";
|
||||
# try_files $uri =404;
|
||||
# }
|
||||
#
|
||||
# # SPA 路由支持
|
||||
# location / {
|
||||
# try_files $uri $uri/ /index.html;
|
||||
# }
|
||||
#
|
||||
# # 安全相关响应头
|
||||
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
# add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
# add_header X-Content-Type-Options "nosniff" always;
|
||||
# add_header X-XSS-Protection "1; mode=block" always;
|
||||
# add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
#
|
||||
# # 禁止隐藏文件访问
|
||||
# location ~ /\. {
|
||||
# deny all;
|
||||
# access_log off;
|
||||
# log_not_found off;
|
||||
# }
|
||||
#
|
||||
# # 禁止敏感文件访问
|
||||
# location ~* \.(env|log|sql|bak|swp|tmp|zip|tar|gz)$ {
|
||||
# deny all;
|
||||
# access_log off;
|
||||
# log_not_found off;
|
||||
# }
|
||||
# }
|
||||
Reference in New Issue
Block a user