收紧分支环境安装流程

This commit is contained in:
Cheng Zhou
2026-05-28 10:57:49 +08:00
parent 8611bd66f8
commit c9b677e96a
2 changed files with 56 additions and 65 deletions
@@ -113,7 +113,7 @@ bash scripts/install-ctms.sh release --base-url https://ctms.example.com
```text ```text
--base-url <url> 健康检查使用的访问地址。dev/main 默认 http://127.0.0.1:8888release 必填或交互输入。 --base-url <url> 健康检查使用的访问地址。dev/main 默认 http://127.0.0.1:8888release 必填或交互输入。
--yes 跳过交互确认,适合自动化执行。 --yes 跳过交互确认,适合自动化执行。
--skip-build 跳过 docker compose up -d --build --skip-build 跳过镜像构建,仍会执行 docker compose up -d。
--skip-migrate 跳过 alembic upgrade head。 --skip-migrate 跳过 alembic upgrade head。
``` ```
@@ -121,10 +121,10 @@ bash scripts/install-ctms.sh release --base-url https://ctms.example.com
```text ```text
1. 检查 docker、docker compose、openssl、curl。 1. 检查 docker、docker compose、openssl、curl。
2. 创建或更新 .env;已有 .env 会先备份为 .env.bak.<时间戳> 2. 检查 .env;已存在则保留不改,不存在则新建
3. main/release 缺少 JWT RSA 私钥时自动生成,已有有效密钥保持不变 3. 新建 main/release 的 .env 时自动生成 JWT RSA 私钥。
4. main/release 先执行 backend-init。 4. main/release 先执行 backend-init。
5. 执行 docker compose up -d --build。 5. 启动容器;默认执行 docker compose up -d --build--skip-build 时执行 docker compose up -d
6. 执行 docker compose run --rm backend python -m alembic upgrade head。 6. 执行 docker compose run --rm backend python -m alembic upgrade head。
7. 检查容器状态、后端环境、/health、/api/v1/auth/login-key。 7. 检查容器状态、后端环境、/health、/api/v1/auth/login-key。
``` ```
+47 -56
View File
@@ -152,7 +152,7 @@ ${C_BOLD}选项:${C_RESET}
dev/main 默认 http://127.0.0.1:8888 dev/main 默认 http://127.0.0.1:8888
release 必须通过此参数或交互输入提供 release 必须通过此参数或交互输入提供
${C_WHITE}--yes${C_RESET} 跳过所有交互确认,适合 CI/CD 自动化执行 ${C_WHITE}--yes${C_RESET} 跳过所有交互确认,适合 CI/CD 自动化执行
${C_WHITE}--skip-build${C_RESET} 跳过镜像重新构建(docker compose up -d ${C_WHITE}--skip-build${C_RESET} 跳过镜像构建,仍会启动容器docker compose up -d
${C_WHITE}--skip-migrate${C_RESET} 跳过数据库迁移(alembic upgrade head ${C_WHITE}--skip-migrate${C_RESET} 跳过数据库迁移(alembic upgrade head
${C_WHITE}--verbose${C_RESET} 展开所有命令的实时输出(默认折叠为 spinner) ${C_WHITE}--verbose${C_RESET} 展开所有命令的实时输出(默认折叠为 spinner)
${C_WHITE}-h, --help${C_RESET} 显示此帮助信息 ${C_WHITE}-h, --help${C_RESET} 显示此帮助信息
@@ -279,13 +279,36 @@ read_env_value() {
' "$ENV_FILE" ' "$ENV_FILE"
} }
normalize_private_key_to_stdout() { resolve_effective_config() {
printf '%b' "$1" local desired_project_name="$1"
} local desired_runtime_env="$2"
local desired_login_key_id="$3"
private_key_is_valid() { EFFECTIVE_PROJECT_NAME="$desired_project_name"
[[ -n "$1" ]] || return 1 EFFECTIVE_RUNTIME_ENV="$desired_runtime_env"
normalize_private_key_to_stdout "$1" | openssl rsa -check -noout >/dev/null 2>&1 EFFECTIVE_LOGIN_KEY_ID="$desired_login_key_id"
[[ -f "$ENV_FILE" ]] || return 0
local env_project_name env_runtime_env env_login_key_id
env_project_name="$(read_env_value COMPOSE_PROJECT_NAME || true)"
env_runtime_env="$(read_env_value ENV || true)"
env_login_key_id="$(read_env_value LOGIN_RSA_KEY_ID || true)"
[[ -n "$env_project_name" ]] || fail ".env 缺少 COMPOSE_PROJECT_NAME,请补齐后重试"
[[ -n "$env_runtime_env" ]] || fail ".env 缺少 ENV,请补齐后重试"
[[ -n "$env_login_key_id" ]] || fail ".env 缺少 LOGIN_RSA_KEY_ID,请补齐后重试"
if [[ "$env_project_name" != "$desired_project_name" ]]; then
fail ".env 的 COMPOSE_PROJECT_NAME=$env_project_name 与目标环境 $TARGET_ENV 期望值 $desired_project_name 不一致"
fi
if [[ "$env_runtime_env" != "$desired_runtime_env" ]]; then
fail ".env 的 ENV=$env_runtime_env 与目标环境 $TARGET_ENV 期望值 $desired_runtime_env 不一致"
fi
EFFECTIVE_PROJECT_NAME="$env_project_name"
EFFECTIVE_RUNTIME_ENV="$env_runtime_env"
EFFECTIVE_LOGIN_KEY_ID="$env_login_key_id"
} }
generate_escaped_private_key() { generate_escaped_private_key() {
@@ -323,7 +346,7 @@ confirm_install() {
step "确认安装参数" step "确认安装参数"
local env_status build_status migrate_status local env_status build_status migrate_status
env_status="$([[ -f "$ENV_FILE" ]] && printf '已存在(将备份后重写)' || printf '不存在(将新建')" env_status="$([[ -f "$ENV_FILE" ]] && printf '已存在' || printf '新建')"
build_status="$([[ "$SKIP_BUILD" -eq 1 ]] && printf '跳过' || printf '执行')" build_status="$([[ "$SKIP_BUILD" -eq 1 ]] && printf '跳过' || printf '执行')"
migrate_status="$([[ "$SKIP_MIGRATE" -eq 1 ]] && printf '跳过' || printf '执行')" migrate_status="$([[ "$SKIP_MIGRATE" -eq 1 ]] && printf '跳过' || printf '执行')"
@@ -333,7 +356,7 @@ confirm_install() {
printf ' %s%-18s%s %s\n' "${C_BOLD}" "Compose 项目" "${C_RESET}" "$project_name" printf ' %s%-18s%s %s\n' "${C_BOLD}" "Compose 项目" "${C_RESET}" "$project_name"
printf ' %s%-18s%s %s\n' "${C_BOLD}" "健康检查地址" "${C_RESET}" "$BASE_URL" printf ' %s%-18s%s %s\n' "${C_BOLD}" "健康检查地址" "${C_RESET}" "$BASE_URL"
printf ' %s%-18s%s %s\n' "${C_BOLD}" ".env 文件" "${C_RESET}" "$env_status" printf ' %s%-18s%s %s\n' "${C_BOLD}" ".env 文件" "${C_RESET}" "$env_status"
printf ' %s%-18s%s %s\n' "${C_BOLD}" "pg_data 目录" "${C_RESET}" "$([[ -d "$ROOT_DIR/pg_data" ]] && printf '已存在(保留)' || printf '不存在')" printf ' %s%-18s%s %s\n' "${C_BOLD}" "pg_data 目录" "${C_RESET}" "$([[ -d "$ROOT_DIR/pg_data" ]] && printf '已存在' || printf '新建')"
printf ' %s%-18s%s %s\n' "${C_BOLD}" "镜像构建" "${C_RESET}" "$build_status" printf ' %s%-18s%s %s\n' "${C_BOLD}" "镜像构建" "${C_RESET}" "$build_status"
printf ' %s%-18s%s %s\n' "${C_BOLD}" "数据库迁移" "${C_RESET}" "$migrate_status" printf ' %s%-18s%s %s\n' "${C_BOLD}" "数据库迁移" "${C_RESET}" "$migrate_status"
printf '\n' printf '\n'
@@ -352,40 +375,12 @@ confirm_install() {
# ── .env 写入 ───────────────────────────────── # ── .env 写入 ─────────────────────────────────
backup_env_file() {
if [[ -f "$ENV_FILE" ]]; then
local backup_file
backup_file="$ENV_FILE.bak.$(date +%Y%m%d%H%M%S)"
cp "$ENV_FILE" "$backup_file"
ok "已备份现有 .env → $(basename "$backup_file")"
fi
}
write_env_file() { write_env_file() {
local project_name="$1" runtime_env="$2" login_key_id="$3" local project_name="$1" runtime_env="$2" login_key_id="$3"
local jwt_secret="$4" rsa_private_key="$5" local jwt_secret="$4" rsa_private_key="$5"
local temp_file local temp_file
temp_file="$(mktemp "$ROOT_DIR/.env.tmp.XXXXXX")" temp_file="$(mktemp "$ROOT_DIR/.env.tmp.XXXXXX")"
if [[ -f "$ENV_FILE" ]]; then
awk '
BEGIN {
prefixes[1] = "COMPOSE_PROJECT_NAME="
prefixes[2] = "ENV="
prefixes[3] = "JWT_SECRET_KEY="
prefixes[4] = "LOGIN_RSA_KEY_ID="
prefixes[5] = "LOGIN_RSA_PRIVATE_KEY="
}
{
drop = 0
for (i = 1; i <= 5; i++) {
if (index($0, prefixes[i]) == 1) { drop = 1; break }
}
if (!drop) print
}
' "$ENV_FILE" > "$temp_file"
fi
{ {
printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name" printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name"
printf 'ENV=%s\n' "$runtime_env" printf 'ENV=%s\n' "$runtime_env"
@@ -403,31 +398,23 @@ prepare_env_file() {
local project_name="$1" runtime_env="$2" login_key_id="$3" local project_name="$1" runtime_env="$2" login_key_id="$3"
local jwt_secret="" rsa_private_key="" local jwt_secret="" rsa_private_key=""
if [[ -f "$ENV_FILE" ]]; then
ok "检测到现有 .env,保留原文件并跳过写入"
return
fi
if [[ "$TARGET_ENV" == "dev" ]]; then if [[ "$TARGET_ENV" == "dev" ]]; then
jwt_secret="dev-secret" jwt_secret="dev-secret"
rsa_private_key="" rsa_private_key=""
else else
jwt_secret="$(read_env_value JWT_SECRET_KEY || true)"
if [[ -z "$jwt_secret" || "$jwt_secret" == "dev-secret" ]]; then
jwt_secret="$(generate_jwt_secret)" jwt_secret="$(generate_jwt_secret)"
ok "已生成新的 JWT_SECRET_KEY" ok "已生成新的 JWT_SECRET_KEY"
else
ok "保留现有 JWT_SECRET_KEY"
fi
rsa_private_key="$(read_env_value LOGIN_RSA_PRIVATE_KEY || true)"
if [[ -z "$rsa_private_key" ]]; then
log "正在生成 RSA 2048 密钥对,请稍候…" log "正在生成 RSA 2048 密钥对,请稍候…"
rsa_private_key="$(generate_escaped_private_key)" rsa_private_key="$(generate_escaped_private_key)"
ok "已生成新的 LOGIN_RSA_PRIVATE_KEY" ok "已生成新的 LOGIN_RSA_PRIVATE_KEY"
elif private_key_is_valid "$rsa_private_key"; then
ok "保留现有有效 LOGIN_RSA_PRIVATE_KEY"
else
fail "现有 LOGIN_RSA_PRIVATE_KEY 不是合法的 PEM 私钥,请修复 .env 后重试(脚本不会自动覆盖已有密钥)"
fi
fi fi
backup_env_file
write_env_file "$project_name" "$runtime_env" "$login_key_id" "$jwt_secret" "$rsa_private_key" write_env_file "$project_name" "$runtime_env" "$login_key_id" "$jwt_secret" "$rsa_private_key"
} }
@@ -440,13 +427,13 @@ run_compose_config() {
run_backend_init() { run_backend_init() {
[[ "$TARGET_ENV" != "dev" ]] || return 0 [[ "$TARGET_ENV" != "dev" ]] || return 0
step "初始化生产数据库与管理员账号" step "初始化数据库与管理员账号"
run_quiet "运行 backend-init 容器" -- docker compose run --rm backend-init run_quiet "运行 backend-init 容器" -- docker compose run --rm backend-init
} }
run_build_and_start() { run_build_and_start() {
if [[ "$SKIP_BUILD" -eq 1 ]]; then if [[ "$SKIP_BUILD" -eq 1 ]]; then
step "启动服务(跳过镜像重新构建)" step "启动服务(跳过镜像构建)"
run_quiet "启动容器" -- docker compose up -d run_quiet "启动容器" -- docker compose up -d
else else
step "构建镜像并启动服务" step "构建镜像并启动服务"
@@ -519,9 +506,11 @@ check_http_endpoint() {
local path="$1" local path="$1"
local url="${BASE_URL%/}$path" local url="${BASE_URL%/}$path"
log "探测接口: $url" log "探测接口: $url"
local attempt=1 max_attempts=10 delay=2 local attempt=1 max_attempts=10 delay=2 warmup_delay=3
log "等待服务预热 ${warmup_delay}s …"
sleep "$warmup_delay"
while [[ "$attempt" -le "$max_attempts" ]]; do while [[ "$attempt" -le "$max_attempts" ]]; do
if curl -fsS "$url" >/dev/null; then if curl -fsS "$url" >/dev/null 2>&1; then
ok "接口就绪: $path" ok "接口就绪: $path"
return 0 return 0
fi fi
@@ -575,14 +564,16 @@ main() {
login_key_id="$(key_id)" login_key_id="$(key_id)"
resolve_base_url resolve_base_url
resolve_effective_config "$project_name" "$runtime_env" "$login_key_id"
check_dependencies check_dependencies
confirm_install "$project_name" "$runtime_env" confirm_install "$EFFECTIVE_PROJECT_NAME" "$EFFECTIVE_RUNTIME_ENV"
prepare_env_file "$project_name" "$runtime_env" "$login_key_id" prepare_env_file "$project_name" "$runtime_env" "$login_key_id"
run_compose_config run_compose_config
run_backend_init run_backend_init
run_build_and_start run_build_and_start
run_migrations run_migrations
run_health_checks "$runtime_env" "$login_key_id" resolve_effective_config "$project_name" "$runtime_env" "$login_key_id"
run_health_checks "$EFFECTIVE_RUNTIME_ENV" "$EFFECTIVE_LOGIN_KEY_ID"
show_success show_success
} }