Files
baiyue-firstleg/deploy/scripts/deploy-built-service.sh
T
2026-09-29 18:47:37 +08:00

227 lines
7.6 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 5 ]; then
echo "用法: $0 <环境> <镜像仓库> <镜像标签> <服务> <环境文件>" >&2
exit 2
fi
DEPLOY_ENV="$1"
IMAGE_REPOSITORY="$2"
IMAGE_TAG="$3"
SERVICE="$4"
ENV_FILE="$5"
COMPOSE_FILE="deploy/docker-compose.yml"
IMAGE="$IMAGE_REPOSITORY/$SERVICE:$IMAGE_TAG"
REGISTRY_HOST="${IMAGE_REPOSITORY%%/*}"
PROJECT_NAME="firstleg-$DEPLOY_ENV"
HEALTH_TIMEOUT_SECONDS="${DEPLOY_HEALTH_TIMEOUT_SECONDS:-240}"
HEALTH_POLL_SECONDS="${DEPLOY_HEALTH_POLL_SECONDS:-5}"
for value_name in HEALTH_TIMEOUT_SECONDS HEALTH_POLL_SECONDS; do
eval "value=\${$value_name}"
case "$value" in
''|*[!0-9]*|0)
echo "$value_name 必须为正整数,当前: $value" >&2
exit 2
;;
esac
done
[ "$DEPLOY_ENV" = "test" ] || { echo "Only test is supported" >&2; exit 2; }
case "$SERVICE" in
gateway|auth|iam|firstleg|finance|order|warehouse|file|print|integration-service|lastleg-service|warehouse-overseas-service|audit-service) ;;
*) echo "Unknown or retired service: $SERVICE" >&2; exit 2 ;;
esac
[ "${TARGET_IS_LOCAL:-false}" = "true" ] || { echo "TEST deploy must run on the target host" >&2; exit 2; }
[ -f "$ENV_FILE" ] || { echo "Environment file missing" >&2; exit 2; }
env_file_value() { sed -n "s/^$1=//p" "$ENV_FILE" | tail -n 1 | tr -d '\r'; }
for key in NACOS_SERVER_ADDR NACOS_NAMESPACE NACOS_USERNAME NACOS_PASSWORD REDIS_HOST REDIS_PORT REDIS_DATABASE REDIS_TOKEN_KEY_PREFIX INTERNAL_SERVICE_TOKEN JWT_RSA_JWK_SET JWT_CURRENT_KID ROCKETMQ_NAME_SERVER; do
[ -n "$(env_file_value "$key")" ] || { echo "Missing $key" >&2; exit 2; }
done
[ "$(env_file_value MQ_TOPIC_PREFIX)" = "test-" ] || { echo "MQ_TOPIC_PREFIX must be test-" >&2; exit 2; }
case "$SERVICE" in
gateway|print|integration-service) db_prefix='' ;;
lastleg-service) db_prefix=LASTLEG ;;
warehouse-overseas-service) db_prefix=WAREHOUSE_OVERSEAS ;;
audit-service) db_prefix=AUDIT ;;
*) db_prefix=$(printf '%s' "$SERVICE" | tr '[:lower:]' '[:upper:]') ;;
esac
if [ -n "$db_prefix" ]; then
for suffix in URL USERNAME PASSWORD; do
key="${db_prefix}_DB_${suffix}"
[ -n "$(env_file_value "$key")" ] || { echo "Missing $key" >&2; exit 2; }
done
fi
if [ "$SERVICE" = "firstleg" ] && [ "$(env_file_value TERMINAL_DRAYAGE_MAIL_ENABLED)" = "true" ]; then
for key in TERMINAL_DRAYAGE_MAIL_HOST TERMINAL_DRAYAGE_MAIL_PORT TERMINAL_DRAYAGE_MAIL_USERNAME TERMINAL_DRAYAGE_MAIL_PASSWORD; do
[ -n "$(env_file_value "$key")" ] || { echo "Missing $key" >&2; exit 2; }
done
fi
# Architecture/schema changes must use whole-release recovery, never an implicit old image.
export PUBLISHED_BIND_ADDRESS=127.0.0.1
: "${REGISTRY_USER:?REGISTRY_USER 未设置}"
: "${REGISTRY_PASSWORD:?REGISTRY_PASSWORD 未设置}"
local_registry_logout() {
docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true
if [ -n "${deploy_docker_config:-}" ]; then rm -rf -- "$deploy_docker_config"; fi
}
local_compose() {
docker compose \
--project-name "$PROJECT_NAME" \
--env-file "$ENV_FILE" \
-f "$COMPOSE_FILE" \
"$@"
}
local_container_id() {
local_compose ps --all --quiet "$SERVICE" | head -n 1
}
local_container_image() {
container_id="$(local_container_id)"
if [ -n "$container_id" ]; then
docker inspect --format '{{.Config.Image}}' "$container_id"
fi
}
local_health_state() {
container_id="$(local_container_id)"
if [ -z "$container_id" ]; then
echo missing
return
fi
docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-health:{{.State.Status}}{{end}}' "$container_id" 2>/dev/null || echo missing
}
local_runtime_state() {
container_id="$(local_container_id)"
if [ -z "$container_id" ]; then
echo missing
return
fi
docker inspect --format '{{.State.Status}}' "$container_id" 2>/dev/null || echo missing
}
wait_for_local_health() {
elapsed=0
while [ "$elapsed" -lt "$HEALTH_TIMEOUT_SECONDS" ]; do
runtime_state="$(local_runtime_state)"
health_state="$(local_health_state)"
echo "容器运行状态: $runtime_state,健康状态: $health_state(已等待 ${elapsed}s)"
case "$runtime_state" in
restarting|exited|dead)
echo "容器未保持运行,立即停止等待: $runtime_state" >&2
return 1
;;
esac
case "$health_state" in
healthy)
return 0
;;
no-health:running)
echo "Missing HEALTHCHECK; deployment failed" >&2
return 1
;;
unhealthy|no-health:exited|no-health:dead)
return 1
;;
esac
sleep "$HEALTH_POLL_SECONDS"
elapsed=$((elapsed + HEALTH_POLL_SECONDS))
done
echo "等待容器健康超时: ${HEALTH_TIMEOUT_SECONDS}s" >&2
return 1
}
deploy_local() {
deploy_docker_config=$(mktemp -d)
DOCKER_CONFIG="$deploy_docker_config"
export DOCKER_CONFIG
trap local_registry_logout EXIT HUP INT TERM
printf '%s' "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin "$REGISTRY_HOST"
trap local_registry_logout EXIT HUP INT TERM
previous_image="$(local_container_image || true)"
if [ -n "$previous_image" ]; then
echo "部署前运行镜像: $previous_image"
else
echo "首次部署,未发现可回滚的旧镜像"
fi
docker pull "$IMAGE"
export DEPLOY_ENV IMAGE_REPOSITORY IMAGE_TAG
new_started=true
if ! local_compose up -d --no-deps --pull never "$SERVICE"; then
echo "新镜像启动命令失败: $IMAGE" >&2
new_started=false
fi
if [ "$new_started" = "true" ] && wait_for_local_health; then
running_image="$(local_container_image || true)"
if [ "$running_image" != "$IMAGE" ]; then
echo "运行容器镜像与请求镜像不一致: expected=$IMAGE actual=${running_image:-missing}" >&2
return 1
fi
echo "新镜像健康检查通过: $IMAGE"
local_compose ps "$SERVICE"
local_registry_logout
trap - EXIT HUP INT TERM
return 0
fi
echo "新镜像健康检查失败: $IMAGE" >&2
failed_container_id="$(local_container_id || true)"
if [ -n "$failed_container_id" ]; then
echo "新镜像容器最近日志(最多 200 行):" >&2
docker logs --tail 200 "$failed_container_id" 2>&1 || true
else
echo "新镜像容器不存在,无法读取启动日志" >&2
fi
if [ "${ALLOW_SAME_CONTRACT_ROLLBACK:-false}" != "true" ]; then
echo "Automatic rollback disabled: keep writes closed and recover the matched release" >&2
local_compose stop "$SERVICE" >/dev/null 2>&1 || true
return 1
fi
expected_prefix="$IMAGE_REPOSITORY/$SERVICE:"
case "$previous_image" in
"$expected_prefix"*)
previous_tag="${previous_image#"$expected_prefix"}"
;;
'')
echo "首次部署没有可回滚镜像,部署失败" >&2
local_compose stop "$SERVICE" >/dev/null 2>&1 || true
return 1
;;
*)
echo "旧镜像不属于当前服务,拒绝自动回滚: $previous_image" >&2
return 1
;;
esac
if [ "$previous_image" = "$IMAGE" ]; then
echo "旧镜像与新镜像相同,无法自动回滚: $IMAGE" >&2
return 1
fi
echo "开始自动回滚到: $previous_image" >&2
if ! docker pull "$previous_image"; then
echo "自动回滚失败,无法拉取旧镜像: $previous_image" >&2
return 1
fi
IMAGE_TAG="$previous_tag"
export IMAGE_TAG
if ! local_compose up -d --no-deps --pull never "$SERVICE"; then
echo "自动回滚失败,旧镜像启动命令失败: $previous_image" >&2
return 1
fi
if wait_for_local_health; then
echo "自动回滚成功: $previous_image" >&2
local_compose ps "$SERVICE" || true
else
echo "自动回滚失败,旧镜像未恢复健康: $previous_image" >&2
fi
return 1
}
deploy_local