#!/bin/bash set -euo pipefail ENV="dev" PROJECT_NAME="LQMDRetrievalFront" BASE_DIR="/home/lq/lq_workspace/${PROJECT_NAME}" SRC_DIR="${BASE_DIR}/source/${PROJECT_NAME}" COMPOSE_RUN_DIR="${BASE_DIR}/app/docker" COMPOSE_FILE_DIR="${BASE_DIR}/app/docker" COMPOSE_FILE_NAME="docker-compose.${ENV}.yaml" SERVICES=("LQMDRetrievalFront") 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 } require_path() { local type=$1 local path=$2 if [ "$type" = "file" ] && [ ! -f "$path" ]; then log_error "file does not exist: $path" exit 1 fi if [ "$type" = "dir" ] && [ ! -d "$path" ]; then log_error "directory does not exist: $path" exit 1 fi } get_current_version() { local service=$1 local compose_file=$2 local image_line image_line=$(grep -A 20 "^[[:space:]]*${service}:" "$compose_file" | grep -E "^[[:space:]]*image:" | head -1) || { log_error "image field not found for service: $service" exit 1 } image_line=$(echo "$image_line" | sed 's/#.*$//') echo "$image_line" | sed -E 's/.*:([^:]*)$/\1/' | tr -d '[:space:]' } increment_version() { local ver=$1 if [[ ! $ver =~ ^v[0-9]+\.[0-9]+$ ]]; then log_error "invalid version format, expected v0.01 or v1.2, got: $ver" exit 1 fi local prefix="${ver%.*}" local last_digit="${ver##*.}" local len=${#last_digit} local num=$((10#$last_digit + 1)) local new_last new_last=$(printf "%0${len}d" "$num") echo "${prefix}.${new_last}" } 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 "/^[[:space:]]*${svc}:/,/^[[:space:]]*image:/ s/(image:[[:space:]]*.*:)([^[:space:]#]*)([[:space:]]*#.*)?$/\1${new_version}\3/" "$compose_file" log_info "updated $svc image version to $new_version" done } get_image_repository() { local service=$1 local compose_file=$2 local image_line image_line=$(grep -A 20 "^[[:space:]]*${service}:" "$compose_file" | grep -E "^[[:space:]]*image:" | head -1) image_line=$(echo "$image_line" | sed 's/#.*$//') echo "$image_line" | sed -E 's/^[[:space:]]*image:[[:space:]]*//' | sed -E 's/:[^:]*$//' | tr -d '[:space:]' } cleanup_old_images_for_services() { local compose_file=$1 shift local services=("$@") for svc in "${services[@]}"; do local image_repo image_repo=$(get_image_repository "$svc" "$compose_file") mapfile -t all_tags < <(docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${image_repo}:" | sed "s|${image_repo}:||" | sort -V) if [ ${#all_tags[@]} -le 3 ]; then log_info "$svc image count <= 3, skip cleanup" continue fi local to_delete=$(( ${#all_tags[@]} - 3 )) for (( i=0; i/dev/null || echo "not_found") if [ "$status" != "running" ]; then all_running=false log_info "$svc status: $status, waiting..." break fi done if [ "$all_running" = true ]; then log_info "all target services are running" return 0 fi done 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[@]}" } require_path file "$COMPOSE_FILE_PATH" require_path dir "$SRC_DIR" require_path dir "$COMPOSE_RUN_DIR" log_info "pull source code: $SRC_DIR" cd "$SRC_DIR" current_branch=$(git symbolic-ref --short HEAD) log_info "current branch: $current_branch" git pull origin "$current_branch" git log -1 --format="[latest commit] %H %ad %s" --date=format:'%Y-%m-%d %H:%M:%S' cd "$COMPOSE_RUN_DIR" CURRENT_VERSION=$(get_current_version "$VERSION_SOURCE_SERVICE" "$COMPOSE_FILE_PATH") NEW_VERSION=$(increment_version "$CURRENT_VERSION") log_info "version: $CURRENT_VERSION -> $NEW_VERSION" update_compose_version_for_services "$COMPOSE_FILE_PATH" "$NEW_VERSION" "${SERVICES[@]}" for svc in "${SERVICES[@]}"; do log_info "build service: $svc" DOCKER_BUILDKIT=1 docker compose -f "$COMPOSE_FILE_PATH" build "$svc" || { log_error "build failed, restore compose file" mv "${COMPOSE_FILE_PATH}.bak" "$COMPOSE_FILE_PATH" exit 1 } done stop_and_remove_containers "${SERVICES[@]}" start_containers "${SERVICES[@]}" if check_all_containers_running "${SERVICES[@]}"; then log_info "deploy succeeded, running version: $NEW_VERSION" cleanup_old_images_for_services "$COMPOSE_FILE_PATH" "${SERVICES[@]}" rm -f "${COMPOSE_FILE_PATH}.bak" else log_error "new version failed, rollback to: $CURRENT_VERSION" stop_and_remove_containers "${SERVICES[@]}" mv "${COMPOSE_FILE_PATH}.bak" "$COMPOSE_FILE_PATH" start_containers "${SERVICES[@]}" check_all_containers_running "${SERVICES[@]}" || { log_error "rollback failed, manual check required" exit 1 } exit 1 fi log_info "deploy finished"