CI / config (push) Canceled after 0s
CI / test-frontend (push) Canceled after 0s
CI / test-rust (push) Canceled after 0s
CI / test-e2e (push) Canceled after 0s
CI / test-e2e-release (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / release (push) Canceled after 0s
CI / docker (push) Canceled after 0s
CI / docker-website (push) Canceled after 0s
Co-Authored-By: Codex <noreply@anthropic.com>
110 lines
3.3 KiB
Bash
Executable File
110 lines
3.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -Eeuo pipefail
|
|
|
|
readonly DEPLOY_DIR="${PLAINLEAF_DEPLOY_DIR:-/etc/plainleaf}"
|
|
readonly COMPOSE_FILE="${DEPLOY_DIR}/compose.yaml"
|
|
readonly ENV_FILE="${DEPLOY_DIR}/plainleaf.env"
|
|
readonly EXPECTED_IMAGE_PREFIX="gitea.aichickenfarm.cn/wushenghua/plainleaf:"
|
|
readonly HEALTH_TIMEOUT_SECONDS="${PLAINLEAF_HEALTH_TIMEOUT_SECONDS:-120}"
|
|
|
|
compose=(docker compose --env-file "$ENV_FILE" --file "$COMPOSE_FILE")
|
|
|
|
log() {
|
|
printf '[%s] %s\n' "$(date '+%F %T')" "$*"
|
|
}
|
|
|
|
wait_for_healthy_container() {
|
|
local deadline=$((SECONDS + HEALTH_TIMEOUT_SECONDS))
|
|
local container_id state
|
|
|
|
while ((SECONDS < deadline)); do
|
|
container_id="$("${compose[@]}" ps --quiet plainleaf 2>/dev/null || true)"
|
|
if [[ -n "$container_id" ]]; then
|
|
state="$(docker inspect \
|
|
--format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \
|
|
"$container_id" 2>/dev/null || true)"
|
|
case "$state" in
|
|
healthy | running)
|
|
return 0
|
|
;;
|
|
exited | dead)
|
|
return 1
|
|
;;
|
|
esac
|
|
fi
|
|
sleep 3
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
restore_previous_image() {
|
|
local previous_image_id="$1"
|
|
local target_image="$2"
|
|
|
|
if [[ -z "$previous_image_id" ]]; then
|
|
log "首次部署没有可回滚的旧镜像,请检查 Plainleaf 日志。"
|
|
return 1
|
|
fi
|
|
|
|
log "新版本启动失败,正在恢复旧镜像 ${previous_image_id}。"
|
|
docker image tag "$previous_image_id" "$target_image"
|
|
"${compose[@]}" up --detach --no-deps --force-recreate plainleaf
|
|
wait_for_healthy_container
|
|
}
|
|
|
|
if [[ ! -r "$COMPOSE_FILE" || ! -r "$ENV_FILE" ]]; then
|
|
log "缺少部署文件:${COMPOSE_FILE} 或 ${ENV_FILE}。"
|
|
exit 1
|
|
fi
|
|
|
|
mapfile -t services < <("${compose[@]}" config --services)
|
|
if [[ "${#services[@]}" -ne 1 || "${services[0]}" != "plainleaf" ]]; then
|
|
log "安全检查失败:该 Compose 必须只包含 plainleaf 服务。"
|
|
exit 1
|
|
fi
|
|
|
|
target_image="$("${compose[@]}" config --images)"
|
|
if [[ "$target_image" != "${EXPECTED_IMAGE_PREFIX}"* ]]; then
|
|
log "安全检查失败:不允许更新镜像 ${target_image}。"
|
|
exit 1
|
|
fi
|
|
|
|
current_container_id="$("${compose[@]}" ps --quiet plainleaf 2>/dev/null || true)"
|
|
current_image_id=""
|
|
if [[ -n "$current_container_id" ]]; then
|
|
current_image_id="$(docker inspect --format '{{.Image}}' "$current_container_id")"
|
|
rollback_image="${EXPECTED_IMAGE_PREFIX}rollback"
|
|
docker image tag "$current_image_id" "$rollback_image"
|
|
log "已保留回滚镜像 ${rollback_image}。"
|
|
fi
|
|
|
|
log "检查 ${target_image} 是否有新版本。"
|
|
"${compose[@]}" pull plainleaf
|
|
target_image_id="$(docker image inspect --format '{{.Id}}' "$target_image")"
|
|
|
|
if [[ -n "$current_image_id" && "$current_image_id" == "$target_image_id" ]]; then
|
|
log "当前已经是最新镜像,无需重建容器。"
|
|
exit 0
|
|
fi
|
|
|
|
log "发现新镜像 ${target_image_id},仅重建 plainleaf 服务。"
|
|
if ! "${compose[@]}" up --detach --no-deps --force-recreate plainleaf; then
|
|
restore_previous_image "$current_image_id" "$target_image"
|
|
exit 1
|
|
fi
|
|
|
|
if wait_for_healthy_container; then
|
|
log "Plainleaf 已更新并通过健康检查。"
|
|
exit 0
|
|
fi
|
|
|
|
docker logs --tail 100 plainleaf >&2 2>/dev/null || true
|
|
if restore_previous_image "$current_image_id" "$target_image"; then
|
|
log "已恢复旧版本;新版本仍保留在本机供排查。"
|
|
else
|
|
log "自动回滚失败,需要人工检查 Plainleaf。"
|
|
fi
|
|
exit 1
|