| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- #!/bin/bash
- set -euo pipefail
- # ============================================
- # 用户配置区域(请根据需要修改以下变量)
- # ============================================
- ENV="dev" # 环境标识 (dev/test/uat/prod)
- SRC_DIR="/home/lq/lq_workspace/LQStandardsService/source/LQStandardsService" # 源代码目录
- COMPOSE_RUN_DIR="/home/lq/lq_workspace/LQConstructionReviewServer/app/docker" # docker-compose 执行目录(cd到此目录运行命令)
- COMPOSE_FILE_DIR="/home/lq/lq_workspace/LQStandardsService/app/docker" # compose 配置文件所在目录
- COMPOSE_FILE_NAME="docker-compose.${ENV}.yaml" # compose 配置文件名
- SERVICES=("LQStandardsService") # 需要更新和部署的服务列表(按依赖顺序)
- GIT_USER="lingmin" # Git 用户名
- GIT_PASS="123456" # Git 密码或token
- # 可选:指定用于提取版本号的服务(默认为 SERVICES 中的第一个)
- VERSION_SOURCE_SERVICE="${SERVICES[0]}"
- # ============================================
- # 以下为脚本逻辑,一般无需修改
- # ============================================
- COMPOSE_FILE_PATH="${COMPOSE_FILE_DIR}/${COMPOSE_FILE_NAME}"
- log_info() {
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
- }
- log_error() {
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
- }
- # 检查必要文件/目录
- if [ ! -f "$COMPOSE_FILE_PATH" ]; then
- log_error "docker-compose 配置文件不存在: $COMPOSE_FILE_PATH"
- exit 1
- fi
- if [ ! -d "$SRC_DIR" ]; then
- log_error "源代码目录不存在: $SRC_DIR"
- exit 1
- fi
- if [ ! -d "$COMPOSE_RUN_DIR" ]; then
- log_error "docker-compose 运行目录不存在: $COMPOSE_RUN_DIR"
- exit 1
- fi
- cd "$COMPOSE_RUN_DIR"
- # ----------------------------------------------------------------------
- # 辅助函数: 从指定服务的 image 字段中提取当前版本号
- get_current_version() {
- local service=$1
- local compose_file=$2
- local image_line
- image_line=$(grep -A 10 "^\s*${service}:" "$compose_file" | grep -E "^\s*image:" | head -1) || {
- log_error "未在 compose 文件中找到服务 $service 的 image 字段"
- exit 1
- }
- # 去除行内注释(# 及其后面的内容)
- image_line=$(echo "$image_line" | sed 's/#.*$//')
- local tag
- tag=$(echo "$image_line" | sed -E 's/.*:([^:]*)$/\1/' | tr -d '[:space:]')
- if [[ -z "$tag" ]]; then
- log_error "无法解析镜像标签 (tag)"
- exit 1
- fi
- echo "$tag"
- }
- # 辅助函数: 版本号递增 (v数字.数字)
- increment_version() {
- local ver=$1
- if [[ ! $ver =~ ^v[0-9]+\.[0-9]+$ ]]; then
- log_error "版本号格式错误,期望如 v0.05 或 v1.2,实际: $ver"
- exit 1
- fi
- local prefix="${ver%.*}"
- local last_digit="${ver##*.}"
- local len=${#last_digit}
- local num=$((10#$last_digit + 1))
- local new_last=$(printf "%0${len}d" "$num")
- echo "${prefix}.${new_last}"
- }
- # 辅助函数: 更新 compose 文件中多个服务的镜像版本号
- update_compose_version_for_services() {
- local compose_file=$1
- local new_version=$2
- shift 2
- local services=("$@")
-
- cp "$compose_file" "${compose_file}.bak"
- for svc in "${services[@]}"; do
- # 替换镜像版本号,同时保留行末可能存在的注释
- sed -i -E "/^\s*${svc}:/,/^\s*image:/ s/(image:\s*.*:)([^[:space:]#]*)([[:space:]]*#.*)?$/\1${new_version}\3/" "$compose_file"
- log_info "已更新服务 $svc 的镜像版本为 $new_version"
- done
- }
- # 辅助函数: 获取镜像的所有历史版本(用于清理)
- get_sorted_image_tags() {
- local image_name=$1
- docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${image_name}:" | sed "s|${image_name}:||" | sort -V
- }
- # 辅助函数: 清理所有服务的旧镜像(每个服务保留最新3个)
- cleanup_old_images_for_services() {
- local services=("$@")
- for svc in "${services[@]}"; do
- log_info "清理服务 $svc 的旧镜像"
- local all_tags
- mapfile -t all_tags < <(get_sorted_image_tags "$svc")
- if [ ${#all_tags[@]} -le 3 ]; then
- log_info "服务 $svc 镜像数量 <= 3,跳过清理"
- continue
- fi
- local to_delete=$(( ${#all_tags[@]} - 3 ))
- for (( i=0; i<to_delete; i++ )); do
- local old_tag="${all_tags[$i]}"
- log_info "删除旧镜像: ${svc}:${old_tag}"
- docker rmi "${svc}:${old_tag}" || log_error "删除镜像 ${svc}:${old_tag} 失败"
- done
- done
- }
- # 辅助函数: 检查所有容器是否都正常运行
- check_all_containers_healthy() {
- local services=("$@")
- # 等待最多30秒,每2秒检查一次
- for i in {1..15}; do
- sleep 2
- all_running=true
- for svc in "${services[@]}"; do
- local status
- status=$(docker inspect --format='{{.State.Status}}' "$(docker compose -f "$COMPOSE_FILE_PATH" ps -q "$svc")" 2>/dev/null || echo "not_found")
- if [ "$status" != "running" ]; then
- all_running=false
- log_info "服务 $svc 状态: $status (等待中...)"
- break
- fi
- done
- if [ "$all_running" = true ]; then
- log_info "所有服务均正常运行"
- return 0
- fi
- log_info "等待所有容器启动... (${i}/15)"
- done
- log_error "部分服务未能正常启动"
- return 1
- }
- # 辅助函数: 停止并删除所有服务的容器
- stop_and_remove_containers() {
- local services=("$@")
- for svc in "${services[@]}"; do
- docker compose -f "$COMPOSE_FILE_PATH" stop "$svc" >/dev/null 2>&1 || true
- docker compose -f "$COMPOSE_FILE_PATH" rm -f "$svc" >/dev/null 2>&1 || true
- done
- }
- # 辅助函数: 启动所有服务的容器
- start_containers() {
- local services=("$@")
- docker compose -f "$COMPOSE_FILE_PATH" up -d "${services[@]}"
- }
- # URL 编码函数(针对密码中的特殊字符)
- urlencode() {
- local string="$1"
- local strlen=${#string}
- local encoded=""
- local pos c o
- for (( pos=0; pos<strlen; pos++ )); do
- c="${string:$pos:1}"
- case "$c" in
- [-_.~a-zA-Z0-9])
- encoded="${encoded}${c}"
- ;;
- *)
- printf -v o '%%%02x' "'$c"
- encoded="${encoded}${o}"
- ;;
- esac
- done
- echo "$encoded"
- }
- # ----------------------------------------------------------------------
- # 1. 拉取代码
- log_info "开始拉取代码: $SRC_DIR"
- cd "$SRC_DIR"
- # 获取当前分支名
- current_branch=$(git symbolic-ref --short HEAD)
- log_info "当前分支: $current_branch"
- remote_url=$(git remote get-url origin)
- if [[ ! $remote_url =~ ^https?:// ]]; then
- log_error "不支持的 Git 协议: $remote_url (只支持 http/https)"
- exit 1
- fi
- # 对密码进行 URL 编码
- encoded_pass=$(urlencode "$GIT_PASS")
- # 解析协议、主机和路径
- proto=$(echo "$remote_url" | cut -d':' -f1)
- host_path=$(echo "$remote_url" | cut -d'/' -f3-)
- # 构建带凭据的 URL(用户名不编码,通常只含数字字母)
- auth_url="${proto}://${GIT_USER}:${encoded_pass}@${host_path}"
- #log_info "正在拉取代码,URL: ${proto}://${GIT_USER}:****@${host_path}"
- #git pull "$auth_url" || {
- # log_error "git pull 失败,请检查用户名/密码或网络"
- # exit 1
- #}
- # 拉取最新代码(使用配置的远程仓库)
- git pull origin "$current_branch" || {
- log_error "git pull 失败,请检查网络或凭据"
- exit 1
- }
- log_info "代码更新完成"
- # 显示最新提交记录
- log_info "最新提交记录:"
- git log -1 --format="提交哈希: %H%n作者: %an <%ae>%n日期: %ad%n提交信息: %s" --date=format:'%Y-%m-%d %H:%M:%S' | while IFS= read -r line; do
- log_info "$line"
- done
- # 2. 获取当前版本号并计算新版本号
- log_info "从服务 $VERSION_SOURCE_SERVICE 中提取当前版本号"
- CURRENT_VERSION=$(get_current_version "$VERSION_SOURCE_SERVICE" "$COMPOSE_FILE_PATH")
- log_info "当前统一版本: $CURRENT_VERSION"
- NEW_VERSION=$(increment_version "$CURRENT_VERSION")
- log_info "新版本: $NEW_VERSION"
- # 3. 更新 compose 文件中所有服务的镜像版本号
- update_compose_version_for_services "$COMPOSE_FILE_PATH" "$NEW_VERSION" "${SERVICES[@]}"
- # 4. 构建所有服务的新镜像(顺序构建,依赖顺序由 SERVICES 列表保证)
- log_info "开始依次构建镜像(服务列表: ${SERVICES[*]})"
- for svc in "${SERVICES[@]}"; do
- log_info "构建服务 $svc 镜像: ${svc}:${NEW_VERSION}"
- DOCKER_BUILDKIT=1 docker compose -f "$COMPOSE_FILE_PATH" build "$svc" || {
- log_error "服务 $svc 镜像构建失败"
- mv "${COMPOSE_FILE_PATH}.bak" "$COMPOSE_FILE_PATH"
- exit 1
- }
- done
- log_info "所有服务镜像构建成功"
- # 5. 停止并删除所有旧容器
- log_info "停止并删除所有旧容器"
- stop_and_remove_containers "${SERVICES[@]}"
- # 6. 启动所有新容器
- log_info "启动所有新容器(版本 $NEW_VERSION)"
- start_containers "${SERVICES[@]}"
- # 7. 检查所有容器健康状态
- if check_all_containers_healthy "${SERVICES[@]}"; then
- log_info "部署成功!当前运行版本: $NEW_VERSION"
- cleanup_old_images_for_services "${SERVICES[@]}"
- rm -f "${COMPOSE_FILE_PATH}.bak"
- else
- # 部署失败,执行回滚
- log_error "新版本启动失败,开始回滚到上一版本 $CURRENT_VERSION"
- stop_and_remove_containers "${SERVICES[@]}"
- # 恢复 compose 文件中的版本号
- mv "${COMPOSE_FILE_PATH}.bak" "$COMPOSE_FILE_PATH"
- log_info "已恢复 compose 文件版本到 $CURRENT_VERSION"
- # 启动旧版本容器
- start_containers "${SERVICES[@]}"
- if check_all_containers_healthy "${SERVICES[@]}"; then
- log_info "回滚成功,当前运行版本: $CURRENT_VERSION"
- else
- log_error "回滚失败,旧版本也无法正常启动!请人工介入"
- exit 1
- fi
- exit 1
- fi
- log_info "所有步骤执行完毕"
|