459cf61495
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m47s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m36s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m40s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 5m4s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m59s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m12s
AI Code Review / AI Code Review (pull_request) Successful in 6m38s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m43s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 9m22s
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
第一部分:修正配置模板(7 处差异) - MINIO_BUCKET → MINIO_BUCKET_NAME(统一变量名) - 删除 DOUBAO_API_KEY 区块(两个环境都没在用) - 新增 DASHSCOPE_API_KEY(实际在用,模板漏了) - 新增 MediaKit 区块(3 个变量) - staging DEBUG=true(实际在用 true) - ENABLE_EMAIL_DELIVERY=false(邮件功能未上线) - SMTP 相关默认值改为空 第二部分:创建 env 渲染脚本 - scripts/render_env.sh:从模板 + Secrets 渲染 .env - 使用 Python 实现变量替换(CI runner 无 envsubst) - 支持 STAGING_xxx/PRODUCTION_xxx 前缀映射 - 自动校验所有必需变量已设置 - 输出文件权限 600,不打印到日志 第三部分:改造 CI 部署流程 - deploy-staging 新增 'Render .env from template' 步骤 - SSH deploy 前先 SCP 渲染后的 .env 到服务器 - 自动备份旧 .env(带时间戳) - 部署完成后清理 CI runner 上的渲染文件 - ci_staging_deploy.sh 改为显式检查 .env 来源 安全: .env.rendered 加入 .gitignore
135 lines
4.1 KiB
Bash
135 lines
4.1 KiB
Bash
#!/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 MINIO_BUCKET_NAME"
|
||
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
|
||
|
||
# 共用 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 包含敏感信息,请勿提交或打印到日志"
|