#!/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 cat <" 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 EOF