|
- #!/usr/bin/env bash
- set -Eeuo pipefail
-
- # Build selected app images and package them as an incremental update.
- # Usage:
- # ./build-update.sh admin monitor
- # SERVICES="admin monitor" ./build-update.sh
- # EMP_ROOT=/path/to/emp IMAGE_TAG=20260602153000 ./build-update.sh emp-admin emp-monitor
- #
- # Output:
- # dist/${DEPLOY_ENV}-update-${IMAGE_TAG}-${services}.tar.gz
-
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- DIST_DIR="${DIST_DIR:-$SCRIPT_DIR/dist}"
- BUILD_CONTEXT_DIR="$SCRIPT_DIR/.build-context"
-
- DEPLOY_ENV="${DEPLOY_ENV:-emp-test}"
- IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-$DEPLOY_ENV}"
- IMAGE_TAG="${IMAGE_TAG:-$(date '+%Y%m%d%H%M%S')}"
- NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}"
- SKIP_BUILD="${SKIP_BUILD:-0}"
- COS_UPLOAD="${COS_UPLOAD:-0}"
-
- log() {
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
- }
-
- die() {
- echo "ERROR: $*" >&2
- exit 1
- }
-
- need_cmd() {
- command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"
- }
-
- has_emp_root() {
- [[ -d "$1/emp_server" && -d "$1/emp_admin" && -d "$1/emp_ws" ]]
- }
-
- resolve_root_dir() {
- if [[ -n "${EMP_ROOT:-}" ]]; then
- local root
- root="$(cd "$EMP_ROOT" && pwd)"
- has_emp_root "$root" || die "EMP_ROOT is invalid: $EMP_ROOT"
- printf '%s\n' "$root"
- return
- fi
-
- local candidates=(
- "$SCRIPT_DIR/../.."
- "$SCRIPT_DIR/../../emp"
- "$SCRIPT_DIR/.."
- "$PWD"
- )
- local candidate root
- for candidate in "${candidates[@]}"; do
- if [[ -d "$candidate" ]]; then
- root="$(cd "$candidate" && pwd)"
- if has_emp_root "$root"; then
- printf '%s\n' "$root"
- return
- fi
- fi
- done
-
- die "Cannot find EMP root. Set EMP_ROOT=/path/to/emp where emp_server, emp_admin and emp_ws exist."
- }
-
- normalize_service() {
- case "$1" in
- gateway|emp-gateway) echo "gateway" ;;
- auth|emp-auth) echo "auth" ;;
- monitor|emp-monitor) echo "monitor" ;;
- data|emp-data) echo "data" ;;
- pdf|emp-pdf) echo "pdf" ;;
- ws|emp-ws) echo "ws" ;;
- admin|emp-admin) echo "admin" ;;
- *) die "Unknown service: $1. Allowed: gateway auth monitor data pdf ws admin" ;;
- esac
- }
-
- compose_service_name() {
- echo "emp-$1"
- }
-
- image_name() {
- echo "$IMAGE_NAMESPACE/emp-$1"
- }
-
- java_module_name() {
- case "$1" in
- gateway) echo "emp_gateway" ;;
- auth) echo "emp_auth" ;;
- monitor) echo "emp_monitor" ;;
- data) echo "emp_data" ;;
- *) return 1 ;;
- esac
- }
-
- is_java_service() {
- case "$1" in
- gateway|auth|monitor|data) return 0 ;;
- *) return 1 ;;
- esac
- }
-
- read_requested_services() {
- local raw_services=()
- if [[ "$#" -gt 0 ]]; then
- raw_services=("$@")
- elif [[ -n "${SERVICES:-}" ]]; then
- # shellcheck disable=SC2206
- raw_services=($SERVICES)
- else
- die "No services specified. Example: ./build-update.sh admin monitor"
- fi
-
- local -A seen=()
- local service normalized
- for service in "${raw_services[@]}"; do
- normalized="$(normalize_service "$service")"
- if [[ -z "${seen[$normalized]:-}" ]]; then
- seen[$normalized]=1
- echo "$normalized"
- fi
- done
- }
-
- join_by_comma() {
- local IFS=,
- echo "$*"
- }
-
- run_step() {
- local workdir="$1"
- shift
- (cd "$workdir" && "$@")
- }
-
- build_java_images() {
- local services=("$@")
- [[ "${#services[@]}" -gt 0 ]] || return 0
-
- local server_dir="$ROOT_DIR/emp_server"
- local modules=()
- local service module jar target_dir
-
- for service in "${services[@]}"; do
- modules+=("$(java_module_name "$service")")
- done
-
- log "Build backend jars: ${modules[*]}"
- run_step "$server_dir" mvn package -DskipTests -B -pl "$(join_by_comma "${modules[@]}")" -am
-
- for service in "${services[@]}"; do
- module="$(java_module_name "$service")"
- target_dir="$server_dir/$module/target"
- jar="$(find "$target_dir" -maxdepth 1 -name '*.jar' ! -name '*original*' ! -name 'app.jar' | head -1)"
- [[ -n "$jar" ]] || die "Jar not found for $module"
- cp "$jar" "$target_dir/app.jar"
-
- log "Build image: $(image_name "$service"):$IMAGE_TAG"
- run_step "$server_dir" docker build \
- -f Dockerfile.service \
- --build-arg "MODULE=$module" \
- -t "$(image_name "$service"):$IMAGE_TAG" \
- -t "$(image_name "$service"):latest" \
- .
- done
- }
-
- build_pdf_image() {
- local server_dir="$ROOT_DIR/emp_server"
- log "Build image: $(image_name pdf):$IMAGE_TAG"
- run_step "$server_dir" docker build \
- -f emp_pdf/Dockerfile \
- -t "$(image_name pdf):$IMAGE_TAG" \
- -t "$(image_name pdf):latest" \
- emp_pdf
- }
-
- build_ws_image() {
- log "Build image: $(image_name ws):$IMAGE_TAG"
- run_step "$ROOT_DIR" docker build \
- -f "$SCRIPT_DIR/dockerfiles/emp-ws.Dockerfile" \
- --build-arg "NPM_REGISTRY=$NPM_REGISTRY" \
- -t "$(image_name ws):$IMAGE_TAG" \
- -t "$(image_name ws):latest" \
- .
- }
-
- build_admin_image() {
- local admin_dir="$ROOT_DIR/emp_admin"
- local context_dir="$BUILD_CONTEXT_DIR/emp-admin"
-
- log "Build frontend dist"
- run_step "$admin_dir" pnpm install --frozen-lockfile
- run_step "$admin_dir" pnpm run build:shunfeng
-
- log "Prepare frontend image context"
- rm -rf "$context_dir"
- mkdir -p "$context_dir"
- cp -R "$admin_dir/dist" "$context_dir/dist"
- cp "$SCRIPT_DIR/nginx/admin.conf" "$context_dir/admin.conf"
-
- log "Build image: $(image_name admin):$IMAGE_TAG"
- run_step "$context_dir" docker build \
- -f "$SCRIPT_DIR/dockerfiles/emp-admin.Dockerfile" \
- -t "$(image_name admin):$IMAGE_TAG" \
- -t "$(image_name admin):latest" \
- .
- }
-
- prepare_package() {
- local services_slug="$1"
- PACKAGE_NAME="${PACKAGE_NAME:-${DEPLOY_ENV}-update-${IMAGE_TAG}-${services_slug}}"
- PACKAGE_DIR="$DIST_DIR/$PACKAGE_NAME"
- PACKAGE_ARCHIVE="$DIST_DIR/${PACKAGE_NAME}.tar.gz"
-
- rm -rf "$PACKAGE_DIR"
- mkdir -p "$PACKAGE_DIR"
-
- cp "$SCRIPT_DIR/apply-update.sh" "$PACKAGE_DIR/apply-update.sh"
- chmod +x "$PACKAGE_DIR/apply-update.sh"
-
- local service
- for service in "${REQUESTED_SERVICES[@]}"; do
- compose_service_name "$service"
- done > "$PACKAGE_DIR/services.txt"
-
- {
- echo "deploy_env=$DEPLOY_ENV"
- echo "image_namespace=$IMAGE_NAMESPACE"
- echo "image_tag=$IMAGE_TAG"
- echo "services=${REQUESTED_SERVICES[*]}"
- echo "compose_services=$(tr '\n' ' ' < "$PACKAGE_DIR/services.txt")"
- echo "created_at=$(date '+%Y-%m-%d %H:%M:%S')"
- } > "$PACKAGE_DIR/manifest.txt"
- }
-
- save_images() {
- local images=()
- local service image
- for service in "${REQUESTED_SERVICES[@]}"; do
- image="$(image_name "$service")"
- images+=("$image:$IMAGE_TAG" "$image:latest")
- done
-
- log "Save images: ${images[*]}"
- docker save -o "$PACKAGE_DIR/images.tar" "${images[@]}"
- }
-
- archive_package() {
- mkdir -p "$DIST_DIR"
- rm -f "$PACKAGE_ARCHIVE"
- tar -czf "$PACKAGE_ARCHIVE" -C "$DIST_DIR" "$PACKAGE_NAME"
- log "Update package created: $PACKAGE_ARCHIVE"
- }
-
- publish_package() {
- if [[ "$COS_UPLOAD" != "1" ]]; then
- return
- fi
-
- log "Upload update package to COS"
- DEPLOY_ENV="$DEPLOY_ENV" PACKAGE_KIND=update bash "$SCRIPT_DIR/publish-cos.sh" "$PACKAGE_ARCHIVE"
- }
-
- need_cmd docker
- need_cmd tar
-
- mapfile -t REQUESTED_SERVICES < <(read_requested_services "$@")
- [[ "${#REQUESTED_SERVICES[@]}" -gt 0 ]] || die "No services to update."
-
- ROOT_DIR="$(resolve_root_dir)"
- SERVICES_SLUG="$(IFS=-; echo "${REQUESTED_SERVICES[*]}")"
-
- log "EMP root: $ROOT_DIR"
- log "Deploy root: $SCRIPT_DIR"
- log "Deploy env: $DEPLOY_ENV"
- log "Update services: ${REQUESTED_SERVICES[*]}"
- log "Image tag: $IMAGE_TAG"
-
- if [[ "$SKIP_BUILD" != "1" ]]; then
- JAVA_SERVICES=()
- NEED_PNPM=0
- for service in "${REQUESTED_SERVICES[@]}"; do
- if is_java_service "$service"; then
- JAVA_SERVICES+=("$service")
- fi
- if [[ "$service" == "admin" ]]; then
- NEED_PNPM=1
- fi
- done
-
- if [[ "${#JAVA_SERVICES[@]}" -gt 0 ]]; then
- need_cmd mvn
- fi
- if [[ "$NEED_PNPM" == "1" ]]; then
- need_cmd pnpm
- fi
-
- build_java_images "${JAVA_SERVICES[@]}"
-
- for service in "${REQUESTED_SERVICES[@]}"; do
- case "$service" in
- pdf) build_pdf_image ;;
- ws) build_ws_image ;;
- admin) build_admin_image ;;
- esac
- done
- else
- log "Skip build, package existing local images only"
- fi
-
- prepare_package "$SERVICES_SLUG"
- save_images
- archive_package
- publish_package
-
- if [[ "$COS_UPLOAD" == "1" ]]; then
- cat <<EOF
-
- COS upload finished. Use the "Target deploy command" above on the target server.
-
- EOF
- else
- cat <<EOF
-
- Next steps on target server:
- Copy $(basename "$PACKAGE_ARCHIVE") to the target server, then run:
-
- Manual fallback:
- mkdir -p /tmp/emp-update
- tar -xzf $(basename "$PACKAGE_ARCHIVE") -C /tmp/emp-update --strip-components=1
- cd /tmp/emp-update
- bash apply-update.sh
-
- If the target runtime uses a compose override, pass all compose files:
- COMPOSE_FILES="/home/admin-x99/emp/$DEPLOY_ENV/runtime/docker-compose.yml:/home/admin-x99/emp/$DEPLOY_ENV/runtime/docker-compose.logs.yml" bash apply-update.sh
-
- EOF
- fi
|