#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" . "$ROOT_DIR/scripts/lib.sh" load_env OUT_DIR="${1:-$ROOT_DIR/dist}" mkdir -p "$OUT_DIR" BUNDLE_NAME="xuqm-private-$(cat "$ROOT_DIR/VERSION" | tr -d '[:space:]')-offline" BUNDLE_DIR="$OUT_DIR/$BUNDLE_NAME" BUNDLE_ARCHIVE="$OUT_DIR/${BUNDLE_NAME}.tar.gz" audit "export-offline-bundle" "STARTED" "$OUT_DIR" progress "export-offline-bundle" "STARTED" "$OUT_DIR" mkdir -p "$BUNDLE_DIR" # Copy deploy repo (exclude data, dist, git, secrets) tar --exclude='data' --exclude='dist' --exclude='.git' \ --exclude='config/secrets.env' --exclude='logs/audit.log' \ -czf "$BUNDLE_DIR/deploy-repo.tar.gz" -C "$ROOT_DIR" . # Pull and save Docker images IMAGES_DIR="$BUNDLE_DIR/images" mkdir -p "$IMAGES_DIR" REGISTRY="${REGISTRY:-registry.example.com/xuqm}" TAG="${IMAGE_TAG:-$(cat "$ROOT_DIR/VERSION" | tr -d '[:space:]')}" PROFILES="${COMPOSE_PROFILES:-base}" pull_and_save() { local svc="$1" local image="${REGISTRY}/${svc}:${TAG}" local out_file="$IMAGES_DIR/${svc}.tar" printf 'Pulling %s ...\n' "$image" if docker pull "$image" 2>/dev/null; then docker save "$image" -o "$out_file" printf 'Saved %s → %s\n' "$image" "$out_file" else printf 'WARNING: could not pull %s (skip in offline bundle)\n' "$image" fi } # Always include base images for svc in tenant-service file-service tenant-web ops-web docs-site; do pull_and_save "$svc" done # Optional services profile_contains() { case ",${PROFILES}," in *","$1","*) return 0;; esac; return 1; } profile_contains "im" && pull_and_save "im-service" profile_contains "push" && pull_and_save "push-service" profile_contains "update" && pull_and_save "update-service" profile_contains "license" && pull_and_save "license-service" # Infrastructure images (always include for managed mode) docker pull nginx:1.27-alpine 2>/dev/null && docker save nginx:1.27-alpine -o "$IMAGES_DIR/nginx.tar" || true docker pull mysql:8.4 2>/dev/null && docker save mysql:8.4 -o "$IMAGES_DIR/mysql.tar" || true docker pull redis:7.4-alpine 2>/dev/null && docker save redis:7.4-alpine -o "$IMAGES_DIR/redis.tar" || true # Generate image load script for offline use cat > "$BUNDLE_DIR/load-images.sh" <<'SCRIPT' #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" for img in "$SCRIPT_DIR/images/"*.tar; do [ -f "$img" ] || continue printf 'Loading %s...\n' "$img" docker load -i "$img" done printf 'All images loaded.\n' SCRIPT chmod +x "$BUNDLE_DIR/load-images.sh" # Copy manifest cp "$ROOT_DIR/image-manifest.json" "$BUNDLE_DIR/" 2>/dev/null || true # Checksums for all files in bundle (cd "$OUT_DIR" && find "$BUNDLE_NAME" -type f -exec sha256sum {} \; > "$BUNDLE_DIR/sha256sums.txt") # Package tar -czf "$BUNDLE_ARCHIVE" -C "$OUT_DIR" "$BUNDLE_NAME" rm -rf "$BUNDLE_DIR" # Checksum of the final archive sha256sum "$BUNDLE_ARCHIVE" > "${BUNDLE_ARCHIVE}.sha256" audit "export-offline-bundle" "DONE" "archive=$BUNDLE_ARCHIVE" progress "export-offline-bundle" "DONE" "archive=$BUNDLE_ARCHIVE" printf 'Offline bundle: %s\n' "$BUNDLE_ARCHIVE" printf 'SHA256: %s\n' "$(cat "${BUNDLE_ARCHIVE}.sha256")"