Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6c44c9bf8 | |||
| 77269802d0 | |||
| e2baee1173 | |||
| b8d3f3aa16 | |||
| 459cf61495 |
@@ -1169,6 +1169,31 @@ jobs:
|
||||
run: |
|
||||
set +e
|
||||
NOTIFY_MODE=start JOB_NAME="Deploy Staging" python3 scripts/ci_notify.py
|
||||
- name: Render .env from template
|
||||
shell: sh
|
||||
env:
|
||||
STAGING_DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
|
||||
STAGING_REDIS_URL: ${{ secrets.STAGING_REDIS_URL }}
|
||||
STAGING_CELERY_BROKER_URL: ${{ secrets.STAGING_CELERY_BROKER_URL }}
|
||||
STAGING_CELERY_RESULT_BACKEND: ${{ secrets.STAGING_CELERY_RESULT_BACKEND }}
|
||||
STAGING_JWT_SECRET_KEY: ${{ secrets.STAGING_JWT_SECRET_KEY }}
|
||||
STAGING_MINIO_ENDPOINT: ${{ secrets.STAGING_MINIO_ENDPOINT }}
|
||||
STAGING_MINIO_ACCESS_KEY: ${{ secrets.STAGING_MINIO_ACCESS_KEY }}
|
||||
STAGING_MINIO_SECRET_KEY: ${{ secrets.STAGING_MINIO_SECRET_KEY }}
|
||||
STAGING_MINIO_BUCKET: ${{ secrets.STAGING_MINIO_BUCKET }}
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||||
COSYVOICE_API_KEY: ${{ secrets.COSYVOICE_API_KEY }}
|
||||
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
|
||||
MEDIAKIT_API_KEY: ${{ secrets.MEDIAKIT_API_KEY }}
|
||||
run: |
|
||||
set -eu
|
||||
echo "Rendering .env from template + secrets..."
|
||||
bash scripts/render_env.sh staging
|
||||
echo "✅ .env rendered (file contains secrets, not printed to log)"
|
||||
# 验证文件存在且非空
|
||||
test -s .env.rendered
|
||||
echo "✅ .env.rendered validated ($(wc -l < .env.rendered) lines)"
|
||||
- name: Docker login to Registry
|
||||
shell: sh
|
||||
env:
|
||||
@@ -1241,9 +1266,31 @@ jobs:
|
||||
ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" "echo SSH_CONNECTION_OK && hostname"
|
||||
echo "SSH connection verified"
|
||||
|
||||
# 配置 Diff 检查:下载服务器当前 .env,对比渲染结果,检测漂移
|
||||
echo "Running config diff check..."
|
||||
scp -P "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no \
|
||||
"${staging_user}@${staging_host}:/var/lib/xiaoxia-saas-staging/.env" .env.current 2>/dev/null \
|
||||
|| touch .env.current # 首次部署时文件不存在,创建空文件
|
||||
bash scripts/config_diff_check.sh .env.rendered .env.current
|
||||
rm -f .env.current
|
||||
echo "Config diff check done"
|
||||
|
||||
# 上传渲染后的 .env 到服务器(替代服务器上旧的 .env)
|
||||
echo "Uploading rendered .env to staging server..."
|
||||
# 备份旧 .env
|
||||
ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" \
|
||||
"cp -f /var/lib/xiaoxia-saas-staging/.env /var/lib/xiaoxia-saas-staging/.env.bak.\$(date +%Y%m%d%H%M%S) 2>/dev/null || true"
|
||||
# 上传新 .env
|
||||
scp -P "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no .env.rendered \
|
||||
"${staging_user}@${staging_host}:/var/lib/xiaoxia-saas-staging/.env"
|
||||
echo "✅ .env uploaded to staging server"
|
||||
|
||||
# 通过环境变量传递凭证,避免命令行引号转义问题
|
||||
cat scripts/ci_staging_deploy.sh | ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" "IMAGE_TAG=${GITHUB_SHA} ACR_USERNAME=${ACR_USERNAME} ACR_PASSWORD=${ACR_PASSWORD} sh"
|
||||
|
||||
# 清理 CI runner 上的渲染文件
|
||||
rm -f .env.rendered
|
||||
|
||||
- name: Staging health check + auto rollback
|
||||
if: success()
|
||||
shell: sh
|
||||
|
||||
@@ -27,6 +27,8 @@ ruff_cache/
|
||||
# 配置模板不受忽略规则限制
|
||||
!deploy/configs/.env.staging
|
||||
!deploy/configs/.env.production
|
||||
# 渲染后的 env 文件包含真实密钥,绝不能提交
|
||||
.env.rendered
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
@@ -57,3 +59,4 @@ frontend-v21-ui-prototype-final.html
|
||||
!.vscode/settings.json
|
||||
.vscode/extensions.json
|
||||
.coverage
|
||||
.env.current
|
||||
|
||||
@@ -104,25 +104,23 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||
|
||||
# ==================== 邮件配置 ====================
|
||||
|
||||
# 启用邮件投递
|
||||
ENABLE_EMAIL_DELIVERY=true
|
||||
# 邮件功能尚未上线,暂时关闭
|
||||
ENABLE_EMAIL_DELIVERY=false
|
||||
|
||||
# SMTP 服务器地址
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_HOST=
|
||||
|
||||
# SMTP 端口
|
||||
SMTP_PORT=587
|
||||
|
||||
# SMTP 用户名
|
||||
# ${SMTP_USER} — 替换为实际的 SMTP 用户名
|
||||
SMTP_USER=${SMTP_USER}
|
||||
# SMTP 用户名(邮件功能上线后配置)
|
||||
SMTP_USER=
|
||||
|
||||
# SMTP 密码 / 应用专用密码
|
||||
# ${SMTP_PASSWORD} — 替换为实际的 SMTP 密码
|
||||
SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
# SMTP 密码(邮件功能上线后配置)
|
||||
SMTP_PASSWORD=
|
||||
|
||||
# 发件人邮箱
|
||||
SMTP_FROM_EMAIL=noreply@xiaoxia-saas.com
|
||||
# 发件人邮箱(邮件功能上线后配置)
|
||||
SMTP_FROM_EMAIL=
|
||||
|
||||
# 发件人显示名称
|
||||
SMTP_FROM_NAME=小虾 SaaS
|
||||
@@ -157,7 +155,7 @@ OSS_DIRECT_UPLOAD_EXPIRE_SECONDS=900
|
||||
# ==================== CORS 配置 ====================
|
||||
|
||||
# 允许跨域的前端域名列表,逗号分隔
|
||||
CORS_ORIGINS_RAW=https://xiaoxiajianji.com,https://www.xiaoxiajianji.com
|
||||
CORS_ORIGINS_RAW=https://xiaoxiajianji.com,https://api.xiaoxiajianji.com
|
||||
|
||||
|
||||
# ==================== 生成文件路径 ====================
|
||||
@@ -191,35 +189,26 @@ COSYVOICE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
||||
COSYVOICE_MODEL=cosyvoice-v3-flash
|
||||
|
||||
# 音色:v3 系列系统音色带 _v3 后缀
|
||||
COSYVOICE_VOICE=longxiaochun_v3
|
||||
COSYVOICE_VOICE=longxiaoxia_v3
|
||||
|
||||
# 采样率
|
||||
COSYVOICE_SAMPLE_RATE=22050
|
||||
|
||||
# 输出格式
|
||||
COSYVOICE_FORMAT=mp3
|
||||
COSYVOICE_FORMAT=wav
|
||||
|
||||
# 音色克隆模型名(固定值)
|
||||
COSYVOICE_CLONE_MODEL=voice-enrollment
|
||||
|
||||
# DashScope 通用 API Key(与 CosyVoice 共用)
|
||||
DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
|
||||
|
||||
# ==================== 豆包大模型(火山引擎方舟)====================
|
||||
|
||||
# 豆包 API Key
|
||||
# ${DOUBAO_API_KEY} — 替换为实际的豆包 API Key
|
||||
DOUBAO_API_KEY=${DOUBAO_API_KEY}
|
||||
# ==================== MediaKit 视频理解(火山引擎)====================
|
||||
|
||||
# 模型名称
|
||||
DOUBAO_MODEL=doubao-seed-1-6-250615
|
||||
|
||||
# API 基础 URL
|
||||
DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||
|
||||
# 请求超时时间(秒)
|
||||
DOUBAO_TIMEOUT=30
|
||||
|
||||
# 最大重试次数
|
||||
DOUBAO_MAX_RETRIES=2
|
||||
MEDIAKIT_API_KEY=${MEDIAKIT_API_KEY}
|
||||
MEDIAKIT_BASE_URL=https://mediakit.cn-beijing.volces.com/api/v1
|
||||
MEDIAKIT_TIMEOUT=60
|
||||
|
||||
|
||||
# ==================== 监控(可选)====================
|
||||
|
||||
+21
-32
@@ -14,8 +14,8 @@ APP_NAME=xiaoxia-saas
|
||||
# 环境标识
|
||||
APP_ENV=staging
|
||||
|
||||
# 关闭 Debug 模式
|
||||
DEBUG=false
|
||||
# Staging 开启 Debug 模式便于排查问题
|
||||
DEBUG=true
|
||||
|
||||
# 应用基础 URL(前端页面地址)
|
||||
APP_BASE_URL=https://staging.xiaoxiajianji.com
|
||||
@@ -96,7 +96,7 @@ JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
||||
JWT_ALGORITHM=HS256
|
||||
|
||||
# Access Token 过期时间(分钟)
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=1440
|
||||
|
||||
# Refresh Token 过期时间(天)
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||
@@ -104,8 +104,8 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||
|
||||
# ==================== 邮件配置 ====================
|
||||
|
||||
# 启用邮件投递
|
||||
ENABLE_EMAIL_DELIVERY=true
|
||||
# 邮件功能尚未上线,暂时关闭
|
||||
ENABLE_EMAIL_DELIVERY=false
|
||||
|
||||
# SMTP 服务器地址
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
@@ -113,16 +113,14 @@ SMTP_HOST=smtp.gmail.com
|
||||
# SMTP 端口
|
||||
SMTP_PORT=587
|
||||
|
||||
# SMTP 用户名
|
||||
# ${SMTP_USER} — 替换为实际的 SMTP 用户名
|
||||
SMTP_USER=${SMTP_USER}
|
||||
# SMTP 用户名(邮件功能上线后配置)
|
||||
SMTP_USER=
|
||||
|
||||
# SMTP 密码 / 应用专用密码
|
||||
# ${SMTP_PASSWORD} — 替换为实际的 SMTP 密码
|
||||
SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
# SMTP 密码(邮件功能上线后配置)
|
||||
SMTP_PASSWORD=
|
||||
|
||||
# 发件人邮箱
|
||||
SMTP_FROM_EMAIL=noreply@xiaoxia-saas.com
|
||||
# 发件人邮箱(邮件功能上线后配置)
|
||||
SMTP_FROM_EMAIL=
|
||||
|
||||
# 发件人显示名称
|
||||
SMTP_FROM_NAME=小虾 SaaS
|
||||
@@ -170,7 +168,7 @@ MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
||||
|
||||
# MinIO Bucket 名称
|
||||
MINIO_BUCKET=${MINIO_BUCKET}
|
||||
MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME}
|
||||
|
||||
# 是否使用 SSL 连接 MinIO
|
||||
MINIO_USE_SSL=false
|
||||
@@ -179,7 +177,7 @@ MINIO_USE_SSL=false
|
||||
# ==================== CORS 配置 ====================
|
||||
|
||||
# 允许跨域的前端域名列表,逗号分隔
|
||||
CORS_ORIGINS_RAW=https://staging.xiaoxiajianji.com,https://staging-web.xiaoxiajianji.com
|
||||
CORS_ORIGINS_RAW=https://staging.xiaoxiajianji.com,https://staging-api.xiaoxiajianji.com
|
||||
|
||||
|
||||
# ==================== 生成文件路径 ====================
|
||||
@@ -213,32 +211,23 @@ COSYVOICE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
||||
COSYVOICE_MODEL=cosyvoice-v3-flash
|
||||
|
||||
# 音色:v3 系列系统音色带 _v3 后缀
|
||||
COSYVOICE_VOICE=longxiaochun_v3
|
||||
COSYVOICE_VOICE=longxiaoxia_v3
|
||||
|
||||
# 采样率
|
||||
COSYVOICE_SAMPLE_RATE=22050
|
||||
|
||||
# 输出格式
|
||||
COSYVOICE_FORMAT=mp3
|
||||
COSYVOICE_FORMAT=wav
|
||||
|
||||
# 音色克隆模型名(固定值)
|
||||
COSYVOICE_CLONE_MODEL=voice-enrollment
|
||||
|
||||
# DashScope 通用 API Key(与 CosyVoice 共用)
|
||||
DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
|
||||
|
||||
# ==================== 豆包大模型(火山引擎方舟)====================
|
||||
|
||||
# 豆包 API Key
|
||||
# ${DOUBAO_API_KEY} — 替换为实际的豆包 API Key
|
||||
DOUBAO_API_KEY=${DOUBAO_API_KEY}
|
||||
# ==================== MediaKit 视频理解(火山引擎)====================
|
||||
|
||||
# 模型名称
|
||||
DOUBAO_MODEL=doubao-seed-1-6-250615
|
||||
|
||||
# API 基础 URL
|
||||
DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||
|
||||
# 请求超时时间(秒)
|
||||
DOUBAO_TIMEOUT=30
|
||||
|
||||
# 最大重试次数
|
||||
DOUBAO_MAX_RETRIES=2
|
||||
MEDIAKIT_API_KEY=${MEDIAKIT_API_KEY}
|
||||
MEDIAKIT_BASE_URL=https://mediakit.cn-beijing.volces.com/api/v1
|
||||
MEDIAKIT_TIMEOUT=60
|
||||
|
||||
@@ -55,7 +55,13 @@ if [ -z "$IMAGE_TAG" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
test -f "$ENV_FILE"
|
||||
# .env 文件由 CI 从模板 + Secrets 渲染后通过 SCP 上传到服务器
|
||||
# 如果文件不存在,说明 CI 渲染步骤失败或未执行
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "ERROR: $ENV_FILE 不存在。CI 应先在 render_env 步骤渲染并上传此文件"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ .env file found: $ENV_FILE ($(wc -l < "$ENV_FILE") lines)"
|
||||
mkdir -p "$GENERATED_DIR"
|
||||
mkdir -p "$LEGACY_ASSETS_DIR"
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env bash
|
||||
# ===========================================================
|
||||
# config_diff_check.sh — 对比渲染 .env 与服务器当前 .env
|
||||
# ===========================================================
|
||||
# 用法: scripts/config_diff_check.sh <rendered_file> <current_file>
|
||||
#
|
||||
# 输出:
|
||||
# + ADDED 渲染文件有、当前文件没有(新增配置)
|
||||
# - REMOVED 当前文件有、渲染文件没有(将被删除)
|
||||
# ~ CHANGED 两边都有但值不同(将被覆盖)
|
||||
#
|
||||
# 敏感值脱敏:KEY/SECRET/PASSWORD/TOKEN/URL 类变量只显示前4字符+***
|
||||
# 退出码: 始终返回 0(仅告警,不阻塞部署)
|
||||
# ===========================================================
|
||||
set -u
|
||||
|
||||
RENDERED_FILE="${1:-}"
|
||||
CURRENT_FILE="${2:-}"
|
||||
|
||||
if [ -z "$RENDERED_FILE" ] || [ -z "$CURRENT_FILE" ]; then
|
||||
echo "ERROR: 用法: $0 <rendered_file> <current_file>" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$RENDERED_FILE" ]; then
|
||||
echo "ERROR: 渲染文件不存在: $RENDERED_FILE" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 判断是否为敏感变量(键名包含以下关键词)
|
||||
is_sensitive() {
|
||||
local key="$1"
|
||||
case "$key" in
|
||||
*KEY*|*SECRET*|*PASSWORD*|*TOKEN*|*URL*|*BROKER*|*BACKEND*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 脱敏:敏感值只显示前4字符+***
|
||||
mask_value() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
if is_sensitive "$key"; then
|
||||
if [ ${#value} -le 4 ]; then
|
||||
echo "****"
|
||||
else
|
||||
echo "${value:0:4}***"
|
||||
fi
|
||||
else
|
||||
echo "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
# 解析文件为 KEY=VALUE(忽略注释和空行)
|
||||
parse_env() {
|
||||
local file="$1"
|
||||
grep -vE '^\s*#|^\s*$' "$file" 2>/dev/null | while IFS= read -r line; do
|
||||
# 只取第一个 = 之前的部分作为 key
|
||||
key="${line%%=*}"
|
||||
value="${line#*=}"
|
||||
# 跳过无效行
|
||||
if [ -n "$key" ] && [ "$key" != "$line" ]; then
|
||||
echo "${key}=${value}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
echo "=========================================="
|
||||
echo " 配置 Diff 检查(检测配置漂移)"
|
||||
echo "=========================================="
|
||||
echo "渲染文件: $RENDERED_FILE"
|
||||
echo "当前文件: $CURRENT_FILE"
|
||||
echo ""
|
||||
|
||||
# 解析两个文件
|
||||
if [ ! -f "$CURRENT_FILE" ] || [ ! -s "$CURRENT_FILE" ]; then
|
||||
# 服务器 .env 不存在或为空(首次部署)
|
||||
echo "⚠️ 服务器 .env 不存在或为空(可能是首次部署)"
|
||||
echo " 所有配置项将标记为 ADDED"
|
||||
echo ""
|
||||
|
||||
added=0
|
||||
while IFS='=' read -r key value; do
|
||||
[ -z "$key" ] && continue
|
||||
masked=$(mask_value "$key" "$value")
|
||||
echo " + ADDED ${key}=${masked}"
|
||||
added=$((added + 1))
|
||||
done < <(parse_env "$RENDERED_FILE")
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 汇总: 新增 ${added} 项 | 删除 0 项 | 变更 0 项 | 无变化 0 项"
|
||||
echo "=========================================="
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 用临时文件存储解析结果
|
||||
tmp_rendered=$(mktemp)
|
||||
tmp_current=$(mktemp)
|
||||
trap "rm -f $tmp_rendered $tmp_current" EXIT
|
||||
|
||||
parse_env "$RENDERED_FILE" | sort > "$tmp_rendered"
|
||||
parse_env "$CURRENT_FILE" | sort > "$tmp_current"
|
||||
|
||||
added=0
|
||||
removed=0
|
||||
changed=0
|
||||
unchanged=0
|
||||
|
||||
echo "--- 新增配置(渲染文件有、当前文件无)---"
|
||||
# 找 ADDED:渲染文件有但当前文件没有的 key
|
||||
while IFS='=' read -r key value; do
|
||||
[ -z "$key" ] && continue
|
||||
current_line=$(grep -m1 "^${key}=" "$tmp_current" 2>/dev/null || true)
|
||||
if [ -z "$current_line" ]; then
|
||||
masked=$(mask_value "$key" "$value")
|
||||
echo " + ADDED ${key}=${masked}"
|
||||
added=$((added + 1))
|
||||
fi
|
||||
done < "$tmp_rendered"
|
||||
|
||||
if [ "$added" -eq 0 ]; then
|
||||
echo " (无)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- 删除配置(当前文件有、渲染文件无)---"
|
||||
# 找 REMOVED:当前文件有但渲染文件没有的 key
|
||||
while IFS='=' read -r key value; do
|
||||
[ -z "$key" ] && continue
|
||||
rendered_line=$(grep -m1 "^${key}=" "$tmp_rendered" 2>/dev/null || true)
|
||||
if [ -z "$rendered_line" ]; then
|
||||
masked=$(mask_value "$key" "$value")
|
||||
echo " - REMOVED ${key}=${masked}"
|
||||
removed=$((removed + 1))
|
||||
fi
|
||||
done < "$tmp_current"
|
||||
|
||||
if [ "$removed" -eq 0 ]; then
|
||||
echo " (无)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- 变更配置(两边都有但值不同)---"
|
||||
# 找 CHANGED:两边都有但值不同
|
||||
while IFS='=' read -r key value; do
|
||||
[ -z "$key" ] && continue
|
||||
current_line=$(grep -m1 "^${key}=" "$tmp_current" 2>/dev/null || true)
|
||||
if [ -n "$current_line" ]; then
|
||||
current_value="${current_line#*=}"
|
||||
if [ "$value" != "$current_value" ]; then
|
||||
masked_new=$(mask_value "$key" "$value")
|
||||
masked_old=$(mask_value "$key" "$current_value")
|
||||
echo " ~ CHANGED ${key}: ${masked_old} → ${masked_new}"
|
||||
changed=$((changed + 1))
|
||||
else
|
||||
unchanged=$((unchanged + 1))
|
||||
fi
|
||||
fi
|
||||
done < "$tmp_rendered"
|
||||
|
||||
if [ "$changed" -eq 0 ]; then
|
||||
echo " (无)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 汇总: 新增 ${added} 项 | 删除 ${removed} 项 | 变更 ${changed} 项 | 无变化 ${unchanged} 项"
|
||||
echo "=========================================="
|
||||
|
||||
if [ "$added" -gt 0 ] || [ "$removed" -gt 0 ] || [ "$changed" -gt 0 ]; then
|
||||
echo "⚠️ 检测到配置漂移,请确认以上变更是否符合预期"
|
||||
else
|
||||
echo "✅ 配置无漂移,与服务器当前配置一致"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
# ===========================================================
|
||||
# render_env.sh — 从模板 + Secrets 渲染 .env 文件
|
||||
# ===========================================================
|
||||
# 用法: scripts/render_env.sh <staging|production>
|
||||
#
|
||||
# 输入: deploy/configs/.env.staging 或 .env.production 模板
|
||||
# 输出: .env.rendered(包含真实密钥,切勿提交或打印)
|
||||
#
|
||||
# 环境变量映射规则:
|
||||
# STAGING_xxx / PRODUCTION_xxx → xxx(去掉环境前缀)
|
||||
# 共用 secrets 直接使用(如 OSS_ACCESS_KEY_ID)
|
||||
# ===========================================================
|
||||
set -eu
|
||||
|
||||
TARGET_ENV="${1:-}"
|
||||
|
||||
if [ -z "$TARGET_ENV" ] || { [ "$TARGET_ENV" != "staging" ] && [ "$TARGET_ENV" != "production" ]; }; then
|
||||
echo "ERROR: 用法: $0 <staging|production>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TEMPLATE_FILE="deploy/configs/.env.${TARGET_ENV}"
|
||||
OUTPUT_FILE=".env.rendered"
|
||||
|
||||
if [ ! -f "$TEMPLATE_FILE" ]; then
|
||||
echo "ERROR: 模板文件不存在: $TEMPLATE_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 构建环境变量映射(带环境前缀的 secrets → 模板变量名)
|
||||
ENV_PREFIX=$(echo "$TARGET_ENV" | tr '[:lower:]' '[:upper:]')
|
||||
|
||||
# 需要映射的带环境前缀变量
|
||||
MAPPED_VARS="DATABASE_URL REDIS_URL CELERY_BROKER_URL CELERY_RESULT_BACKEND JWT_SECRET_KEY"
|
||||
|
||||
# Staging 独有的 MinIO 变量
|
||||
if [ "$TARGET_ENV" = "staging" ]; then
|
||||
MAPPED_VARS="$MAPPED_VARS MINIO_ENDPOINT MINIO_ACCESS_KEY MINIO_SECRET_KEY"
|
||||
fi
|
||||
|
||||
# 将带前缀的 secrets 导出为无前缀的环境变量
|
||||
for var in $MAPPED_VARS; do
|
||||
prefixed_var="${ENV_PREFIX}_${var}"
|
||||
value="${!prefixed_var:-}"
|
||||
if [ -n "$value" ]; then
|
||||
export "$var=$value"
|
||||
fi
|
||||
done
|
||||
|
||||
# 特殊映射:CI secret 名称与模板占位符不一致的变量
|
||||
# STAGING_MINIO_BUCKET → MINIO_BUCKET_NAME
|
||||
if [ "$TARGET_ENV" = "staging" ]; then
|
||||
if [ -n "${STAGING_MINIO_BUCKET:-}" ]; then
|
||||
export "MINIO_BUCKET_NAME=$STAGING_MINIO_BUCKET"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 共用 secrets 直接导出(如果存在)
|
||||
SHARED_SECRETS="OSS_ACCESS_KEY_ID OSS_ACCESS_KEY_SECRET COSYVOICE_API_KEY DASHSCOPE_API_KEY MEDIAKIT_API_KEY"
|
||||
for var in $SHARED_SECRETS; do
|
||||
value="${!var:-}"
|
||||
# 已经在环境中了,无需额外操作
|
||||
done
|
||||
|
||||
# 使用 Python 进行变量替换(Python 在 CI runner 中一定存在)
|
||||
python3 - "$TEMPLATE_FILE" "$OUTPUT_FILE" "$ENV_PREFIX" "$MAPPED_VARS" "$SHARED_SECRETS" <<'PYTHON_SCRIPT'
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
template_file = sys.argv[1]
|
||||
output_file = sys.argv[2]
|
||||
env_prefix = sys.argv[3]
|
||||
mapped_vars_str = sys.argv[4]
|
||||
shared_secrets_str = sys.argv[5]
|
||||
|
||||
# 收集所有可用的替换变量
|
||||
all_vars = set()
|
||||
for v in mapped_vars_str.split():
|
||||
all_vars.add(v)
|
||||
for v in shared_secrets_str.split():
|
||||
all_vars.add(v)
|
||||
|
||||
# 读取模板
|
||||
with open(template_file, 'r') as f:
|
||||
template = f.read()
|
||||
|
||||
# 找出模板中所有的 ${VAR} 占位符(仅检查非注释行)
|
||||
pattern = re.compile(r'\$\{(\w+)\}')
|
||||
placeholders = set()
|
||||
for line in template.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('#'):
|
||||
continue
|
||||
placeholders.update(pattern.findall(line))
|
||||
|
||||
# 检查必需变量是否已设置
|
||||
missing = []
|
||||
for var in placeholders:
|
||||
value = os.environ.get(var, '')
|
||||
if not value:
|
||||
missing.append(var)
|
||||
|
||||
if missing:
|
||||
print(f"ERROR: 以下变量未设置或为空: {', '.join(sorted(missing))}", file=sys.stderr)
|
||||
print(f"请确认对应的 {env_prefix}_xxx 或共用 secrets 已在 Gitea Secrets 中配置", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 执行替换
|
||||
def replace_var(match):
|
||||
var_name = match.group(1)
|
||||
return os.environ.get(var_name, match.group(0))
|
||||
|
||||
rendered = pattern.sub(replace_var, template)
|
||||
|
||||
# 写入输出文件
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(rendered)
|
||||
|
||||
# 设置文件权限为仅 owner 可读写
|
||||
os.chmod(output_file, 0o600)
|
||||
|
||||
print(f"✅ .env 渲染完成: {template_file} → {output_file}")
|
||||
print(f" 替换了 {len(placeholders)} 个变量")
|
||||
PYTHON_SCRIPT
|
||||
|
||||
# 验证输出文件
|
||||
if [ ! -f "$OUTPUT_FILE" ]; then
|
||||
echo "ERROR: 渲染失败,输出文件不存在" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查输出文件中是否还有未替换的占位符(仅检查非注释行)
|
||||
if grep -vE '^\s*#' "$OUTPUT_FILE" | grep -qE '\$\{[A-Z_]+\}'; then
|
||||
echo "ERROR: 输出文件中仍有未替换的占位符:" >&2
|
||||
grep -nE '\$\{[A-Z_]+\}' "$OUTPUT_FILE" | grep -v '^\s*#' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ 渲染文件校验通过,无残留占位符"
|
||||
echo "⚠️ $OUTPUT_FILE 包含敏感信息,请勿提交或打印到日志"
|
||||
Reference in New Issue
Block a user