deploy_constplan_standards_service.sh 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. #!/bin/bash
  2. set -euo pipefail
  3. # ============================================
  4. # 用户配置区域(请根据需要修改以下变量)
  5. # ============================================
  6. ENV="dev" # 环境标识 (dev/test/uat/prod)
  7. SRC_DIR="/home/lq/lq_workspace/LQStandardsService/source/LQStandardsService" # 源代码目录
  8. COMPOSE_RUN_DIR="/home/lq/lq_workspace/LQConstructionReviewServer/app/docker" # docker-compose 执行目录(cd到此目录运行命令)
  9. COMPOSE_FILE_DIR="/home/lq/lq_workspace/LQStandardsService/app/docker" # compose 配置文件所在目录
  10. COMPOSE_FILE_NAME="docker-compose.${ENV}.yaml" # compose 配置文件名
  11. SERVICES=("LQStandardsService") # 需要更新和部署的服务列表(按依赖顺序)
  12. GIT_USER="lingmin" # Git 用户名
  13. GIT_PASS="123456" # Git 密码或token
  14. # 可选:指定用于提取版本号的服务(默认为 SERVICES 中的第一个)
  15. VERSION_SOURCE_SERVICE="${SERVICES[0]}"
  16. # ============================================
  17. # 以下为脚本逻辑,一般无需修改
  18. # ============================================
  19. COMPOSE_FILE_PATH="${COMPOSE_FILE_DIR}/${COMPOSE_FILE_NAME}"
  20. log_info() {
  21. echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
  22. }
  23. log_error() {
  24. echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
  25. }
  26. # 检查必要文件/目录
  27. if [ ! -f "$COMPOSE_FILE_PATH" ]; then
  28. log_error "docker-compose 配置文件不存在: $COMPOSE_FILE_PATH"
  29. exit 1
  30. fi
  31. if [ ! -d "$SRC_DIR" ]; then
  32. log_error "源代码目录不存在: $SRC_DIR"
  33. exit 1
  34. fi
  35. if [ ! -d "$COMPOSE_RUN_DIR" ]; then
  36. log_error "docker-compose 运行目录不存在: $COMPOSE_RUN_DIR"
  37. exit 1
  38. fi
  39. cd "$COMPOSE_RUN_DIR"
  40. # ----------------------------------------------------------------------
  41. # 辅助函数: 从指定服务的 image 字段中提取当前版本号
  42. get_current_version() {
  43. local service=$1
  44. local compose_file=$2
  45. local image_line
  46. image_line=$(grep -A 10 "^\s*${service}:" "$compose_file" | grep -E "^\s*image:" | head -1) || {
  47. log_error "未在 compose 文件中找到服务 $service 的 image 字段"
  48. exit 1
  49. }
  50. # 去除行内注释(# 及其后面的内容)
  51. image_line=$(echo "$image_line" | sed 's/#.*$//')
  52. local tag
  53. tag=$(echo "$image_line" | sed -E 's/.*:([^:]*)$/\1/' | tr -d '[:space:]')
  54. if [[ -z "$tag" ]]; then
  55. log_error "无法解析镜像标签 (tag)"
  56. exit 1
  57. fi
  58. echo "$tag"
  59. }
  60. # 辅助函数: 版本号递增 (v数字.数字)
  61. increment_version() {
  62. local ver=$1
  63. if [[ ! $ver =~ ^v[0-9]+\.[0-9]+$ ]]; then
  64. log_error "版本号格式错误,期望如 v0.05 或 v1.2,实际: $ver"
  65. exit 1
  66. fi
  67. local prefix="${ver%.*}"
  68. local last_digit="${ver##*.}"
  69. local len=${#last_digit}
  70. local num=$((10#$last_digit + 1))
  71. local new_last=$(printf "%0${len}d" "$num")
  72. echo "${prefix}.${new_last}"
  73. }
  74. # 辅助函数: 更新 compose 文件中多个服务的镜像版本号
  75. update_compose_version_for_services() {
  76. local compose_file=$1
  77. local new_version=$2
  78. shift 2
  79. local services=("$@")
  80. cp "$compose_file" "${compose_file}.bak"
  81. for svc in "${services[@]}"; do
  82. # 替换镜像版本号,同时保留行末可能存在的注释
  83. sed -i -E "/^\s*${svc}:/,/^\s*image:/ s/(image:\s*.*:)([^[:space:]#]*)([[:space:]]*#.*)?$/\1${new_version}\3/" "$compose_file"
  84. log_info "已更新服务 $svc 的镜像版本为 $new_version"
  85. done
  86. }
  87. # 辅助函数: 获取镜像的所有历史版本(用于清理)
  88. get_sorted_image_tags() {
  89. local image_name=$1
  90. docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${image_name}:" | sed "s|${image_name}:||" | sort -V
  91. }
  92. # 辅助函数: 清理所有服务的旧镜像(每个服务保留最新3个)
  93. cleanup_old_images_for_services() {
  94. local services=("$@")
  95. for svc in "${services[@]}"; do
  96. log_info "清理服务 $svc 的旧镜像"
  97. local all_tags
  98. mapfile -t all_tags < <(get_sorted_image_tags "$svc")
  99. if [ ${#all_tags[@]} -le 3 ]; then
  100. log_info "服务 $svc 镜像数量 <= 3,跳过清理"
  101. continue
  102. fi
  103. local to_delete=$(( ${#all_tags[@]} - 3 ))
  104. for (( i=0; i<to_delete; i++ )); do
  105. local old_tag="${all_tags[$i]}"
  106. log_info "删除旧镜像: ${svc}:${old_tag}"
  107. docker rmi "${svc}:${old_tag}" || log_error "删除镜像 ${svc}:${old_tag} 失败"
  108. done
  109. done
  110. }
  111. # 辅助函数: 检查所有容器是否都正常运行
  112. check_all_containers_healthy() {
  113. local services=("$@")
  114. # 等待最多30秒,每2秒检查一次
  115. for i in {1..15}; do
  116. sleep 2
  117. all_running=true
  118. for svc in "${services[@]}"; do
  119. local status
  120. status=$(docker inspect --format='{{.State.Status}}' "$(docker compose -f "$COMPOSE_FILE_PATH" ps -q "$svc")" 2>/dev/null || echo "not_found")
  121. if [ "$status" != "running" ]; then
  122. all_running=false
  123. log_info "服务 $svc 状态: $status (等待中...)"
  124. break
  125. fi
  126. done
  127. if [ "$all_running" = true ]; then
  128. log_info "所有服务均正常运行"
  129. return 0
  130. fi
  131. log_info "等待所有容器启动... (${i}/15)"
  132. done
  133. log_error "部分服务未能正常启动"
  134. return 1
  135. }
  136. # 辅助函数: 停止并删除所有服务的容器
  137. stop_and_remove_containers() {
  138. local services=("$@")
  139. for svc in "${services[@]}"; do
  140. docker compose -f "$COMPOSE_FILE_PATH" stop "$svc" >/dev/null 2>&1 || true
  141. docker compose -f "$COMPOSE_FILE_PATH" rm -f "$svc" >/dev/null 2>&1 || true
  142. done
  143. }
  144. # 辅助函数: 启动所有服务的容器
  145. start_containers() {
  146. local services=("$@")
  147. docker compose -f "$COMPOSE_FILE_PATH" up -d "${services[@]}"
  148. }
  149. # URL 编码函数(针对密码中的特殊字符)
  150. urlencode() {
  151. local string="$1"
  152. local strlen=${#string}
  153. local encoded=""
  154. local pos c o
  155. for (( pos=0; pos<strlen; pos++ )); do
  156. c="${string:$pos:1}"
  157. case "$c" in
  158. [-_.~a-zA-Z0-9])
  159. encoded="${encoded}${c}"
  160. ;;
  161. *)
  162. printf -v o '%%%02x' "'$c"
  163. encoded="${encoded}${o}"
  164. ;;
  165. esac
  166. done
  167. echo "$encoded"
  168. }
  169. # ----------------------------------------------------------------------
  170. # 1. 拉取代码
  171. log_info "开始拉取代码: $SRC_DIR"
  172. cd "$SRC_DIR"
  173. # 获取当前分支名
  174. current_branch=$(git symbolic-ref --short HEAD)
  175. log_info "当前分支: $current_branch"
  176. remote_url=$(git remote get-url origin)
  177. if [[ ! $remote_url =~ ^https?:// ]]; then
  178. log_error "不支持的 Git 协议: $remote_url (只支持 http/https)"
  179. exit 1
  180. fi
  181. # 对密码进行 URL 编码
  182. encoded_pass=$(urlencode "$GIT_PASS")
  183. # 解析协议、主机和路径
  184. proto=$(echo "$remote_url" | cut -d':' -f1)
  185. host_path=$(echo "$remote_url" | cut -d'/' -f3-)
  186. # 构建带凭据的 URL(用户名不编码,通常只含数字字母)
  187. auth_url="${proto}://${GIT_USER}:${encoded_pass}@${host_path}"
  188. #log_info "正在拉取代码,URL: ${proto}://${GIT_USER}:****@${host_path}"
  189. #git pull "$auth_url" || {
  190. # log_error "git pull 失败,请检查用户名/密码或网络"
  191. # exit 1
  192. #}
  193. # 拉取最新代码(使用配置的远程仓库)
  194. git pull origin "$current_branch" || {
  195. log_error "git pull 失败,请检查网络或凭据"
  196. exit 1
  197. }
  198. log_info "代码更新完成"
  199. # 显示最新提交记录
  200. log_info "最新提交记录:"
  201. 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
  202. log_info "$line"
  203. done
  204. # 2. 获取当前版本号并计算新版本号
  205. log_info "从服务 $VERSION_SOURCE_SERVICE 中提取当前版本号"
  206. CURRENT_VERSION=$(get_current_version "$VERSION_SOURCE_SERVICE" "$COMPOSE_FILE_PATH")
  207. log_info "当前统一版本: $CURRENT_VERSION"
  208. NEW_VERSION=$(increment_version "$CURRENT_VERSION")
  209. log_info "新版本: $NEW_VERSION"
  210. # 3. 更新 compose 文件中所有服务的镜像版本号
  211. update_compose_version_for_services "$COMPOSE_FILE_PATH" "$NEW_VERSION" "${SERVICES[@]}"
  212. # 4. 构建所有服务的新镜像(顺序构建,依赖顺序由 SERVICES 列表保证)
  213. log_info "开始依次构建镜像(服务列表: ${SERVICES[*]})"
  214. for svc in "${SERVICES[@]}"; do
  215. log_info "构建服务 $svc 镜像: ${svc}:${NEW_VERSION}"
  216. DOCKER_BUILDKIT=1 docker compose -f "$COMPOSE_FILE_PATH" build "$svc" || {
  217. log_error "服务 $svc 镜像构建失败"
  218. mv "${COMPOSE_FILE_PATH}.bak" "$COMPOSE_FILE_PATH"
  219. exit 1
  220. }
  221. done
  222. log_info "所有服务镜像构建成功"
  223. # 5. 停止并删除所有旧容器
  224. log_info "停止并删除所有旧容器"
  225. stop_and_remove_containers "${SERVICES[@]}"
  226. # 6. 启动所有新容器
  227. log_info "启动所有新容器(版本 $NEW_VERSION)"
  228. start_containers "${SERVICES[@]}"
  229. # 7. 检查所有容器健康状态
  230. if check_all_containers_healthy "${SERVICES[@]}"; then
  231. log_info "部署成功!当前运行版本: $NEW_VERSION"
  232. cleanup_old_images_for_services "${SERVICES[@]}"
  233. rm -f "${COMPOSE_FILE_PATH}.bak"
  234. else
  235. # 部署失败,执行回滚
  236. log_error "新版本启动失败,开始回滚到上一版本 $CURRENT_VERSION"
  237. stop_and_remove_containers "${SERVICES[@]}"
  238. # 恢复 compose 文件中的版本号
  239. mv "${COMPOSE_FILE_PATH}.bak" "$COMPOSE_FILE_PATH"
  240. log_info "已恢复 compose 文件版本到 $CURRENT_VERSION"
  241. # 启动旧版本容器
  242. start_containers "${SERVICES[@]}"
  243. if check_all_containers_healthy "${SERVICES[@]}"; then
  244. log_info "回滚成功,当前运行版本: $CURRENT_VERSION"
  245. else
  246. log_error "回滚失败,旧版本也无法正常启动!请人工介入"
  247. exit 1
  248. fi
  249. exit 1
  250. fi
  251. log_info "所有步骤执行完毕"