#!/usr/bin/env bash # =========================================================== # render_env.sh — 从模板 + Secrets 渲染 .env 文件 # =========================================================== # 用法: scripts/render_env.sh # # 输入: 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 " >&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 WECHAT_APP_ID WECHAT_APP_SECRET" 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 包含敏感信息,请勿提交或打印到日志"