#!/usr/bin/env sh set -eu SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" REPO_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)" RELEASE_VERSION="${RELEASE_VERSION:?RELEASE_VERSION is required}" RELEASE_MANIFEST_SOURCE="${RELEASE_MANIFEST_SOURCE:?RELEASE_MANIFEST_SOURCE is required}" RELEASE_ENV_SOURCE="${RELEASE_ENV_SOURCE:-$SCRIPT_DIR/managed-portal.env}" OUTPUT_DIR="${OUTPUT_DIR:-$REPO_ROOT/dist/ota}" STAGE_DIR="${OUTPUT_DIR}/managed-portal-${RELEASE_VERSION}" BUNDLE_PATH="${OUTPUT_DIR}/managed-portal-${RELEASE_VERSION}.zip" INSTALLER_PATH="${OUTPUT_DIR}/install-managed-portal-${RELEASE_VERSION}.sh" LATEST_INSTALLER_PATH="${OUTPUT_DIR}/install-managed-portal-Latest.sh" LOWERCASE_LATEST_INSTALLER_PATH="${OUTPUT_DIR}/install-managed-portal-latest.sh" INCLUDE_WEIGHTS="${INCLUDE_WEIGHTS:-0}" GENERATE_WEIGHTS_ARCHIVE="${GENERATE_WEIGHTS_ARCHIVE:-1}" PEOPLE_FLOW_WEIGHTS_SOURCE="${PEOPLE_FLOW_WEIGHTS_SOURCE:-$REPO_ROOT/managed/people_flow_project/weights}" WEIGHTS_ARCHIVE_PATH="${OUTPUT_DIR}/people-flow-weights-${RELEASE_VERSION}.tar.gz" YOLO_ARCHIVE_PATH="${OUTPUT_DIR}/people-flow-yolo11n-${RELEASE_VERSION}.tar.gz" require_path() { target="$1" if [ ! -e "$target" ]; then echo "missing required path: $target" >&2 exit 1 fi } dir_has_files() { directory="$1" [ -d "$directory" ] && [ -n "$(find "$directory" -mindepth 1 -print -quit 2>/dev/null)" ] } dir_has_payload_files() { directory="$1" [ -d "$directory" ] && [ -n "$(find "$directory" -type f ! -name '.gitkeep' -print -quit 2>/dev/null)" ] } copy_dir() { source_dir="$1" target_dir="$2" mkdir -p "$(dirname "$target_dir")" cp -R "$source_dir" "$target_dir" } require_path "$RELEASE_MANIFEST_SOURCE" require_path "$RELEASE_ENV_SOURCE" require_path "$SCRIPT_DIR/docker-compose.ota-release.yml" require_path "$SCRIPT_DIR/Dockerfile.runtime-overlay" require_path "$SCRIPT_DIR/install-managed-portal-ota.sh" require_path "$REPO_ROOT/managed/store_dwell_alert/config" require_path "$REPO_ROOT/managed/people_flow_project/config" rm -rf "$STAGE_DIR" mkdir -p "$STAGE_DIR/deploy" "$OUTPUT_DIR" cp "$SCRIPT_DIR/docker-compose.ota-release.yml" "$STAGE_DIR/deploy/docker-compose.ota-release.yml" cp "$SCRIPT_DIR/Dockerfile.runtime-overlay" "$STAGE_DIR/deploy/Dockerfile.runtime-overlay" cp "$RELEASE_ENV_SOURCE" "$STAGE_DIR/deploy/managed-portal.release.env" cp "$RELEASE_MANIFEST_SOURCE" "$STAGE_DIR/release-manifest.env" copy_dir "$REPO_ROOT/managed/store_dwell_alert/config" "$STAGE_DIR/managed/store_dwell_alert/config" copy_dir "$REPO_ROOT/managed/people_flow_project/config" "$STAGE_DIR/managed/people_flow_project/config" if [ "$INCLUDE_WEIGHTS" = "1" ]; then if ! dir_has_payload_files "$PEOPLE_FLOW_WEIGHTS_SOURCE"; then echo "people-flow weights requested but missing under $PEOPLE_FLOW_WEIGHTS_SOURCE" >&2 exit 1 fi copy_dir "$PEOPLE_FLOW_WEIGHTS_SOURCE" "$STAGE_DIR/managed/people_flow_project/weights" fi python3 - "$OUTPUT_DIR" "$STAGE_DIR" "$BUNDLE_PATH" <<'PY' from pathlib import Path import sys import zipfile output_dir = Path(sys.argv[1]) stage_dir = Path(sys.argv[2]) bundle_path = Path(sys.argv[3]) if bundle_path.exists(): bundle_path.unlink() with zipfile.ZipFile(bundle_path, "w", compression=zipfile.ZIP_DEFLATED) as archive: for path in sorted(stage_dir.rglob("*")): arcname = path.relative_to(output_dir) if path.is_dir(): if not any(path.iterdir()): archive.writestr(f"{arcname.as_posix().rstrip('/')}/", "") continue archive.write(path, arcname.as_posix()) PY python3 - "$SCRIPT_DIR/install-managed-portal-ota.sh" "$INSTALLER_PATH" "$RELEASE_VERSION" <<'PY' from pathlib import Path import re import sys source_path = Path(sys.argv[1]) target_path = Path(sys.argv[2]) release_version = sys.argv[3] content = source_path.read_text(encoding="utf-8") content = re.sub( r'^RELEASE_VERSION="\$\{RELEASE_VERSION:-[^"]+\}"$', f'RELEASE_VERSION="${{RELEASE_VERSION:-{release_version}}}"', content, count=1, flags=re.MULTILINE, ) target_path.write_text(content, encoding="utf-8") target_path.chmod(0o755) PY cp "$INSTALLER_PATH" "$LATEST_INSTALLER_PATH" cp "$INSTALLER_PATH" "$LOWERCASE_LATEST_INSTALLER_PATH" chmod 755 "$LATEST_INSTALLER_PATH" "$LOWERCASE_LATEST_INSTALLER_PATH" if [ "$GENERATE_WEIGHTS_ARCHIVE" = "1" ] && dir_has_payload_files "$PEOPLE_FLOW_WEIGHTS_SOURCE"; then python3 - "$PEOPLE_FLOW_WEIGHTS_SOURCE" "$WEIGHTS_ARCHIVE_PATH" <<'PY' from pathlib import Path import sys import tarfile source_dir = Path(sys.argv[1]) archive_path = Path(sys.argv[2]) if archive_path.exists(): archive_path.unlink() with tarfile.open(archive_path, "w:gz") as archive: for path in sorted(source_dir.rglob("*")): if path.name == ".gitkeep": continue arcname = Path("people_flow_project") / "weights" / path.relative_to(source_dir) archive.add(path, arcname=arcname.as_posix(), recursive=False) PY fi if [ "$GENERATE_WEIGHTS_ARCHIVE" = "1" ] && [ -f "$PEOPLE_FLOW_WEIGHTS_SOURCE/yolo11n.pt" ]; then python3 - "$PEOPLE_FLOW_WEIGHTS_SOURCE/yolo11n.pt" "$YOLO_ARCHIVE_PATH" <<'PY' from pathlib import Path import sys import tarfile source_path = Path(sys.argv[1]) archive_path = Path(sys.argv[2]) if archive_path.exists(): archive_path.unlink() with tarfile.open(archive_path, "w:gz") as archive: archive.add(source_path, arcname="people_flow_project/weights/yolo11n.pt", recursive=False) PY fi echo "OTA bundle created: $BUNDLE_PATH" echo "Versioned installer created: $INSTALLER_PATH" echo "Latest installers created: $LATEST_INSTALLER_PATH and $LOWERCASE_LATEST_INSTALLER_PATH" if [ "$INCLUDE_WEIGHTS" = "1" ]; then echo "Bundle includes managed/people_flow_project/weights" else echo "Bundle excludes managed/people_flow_project/weights; the installer will reuse the shared host weights directory if available" fi if [ "$GENERATE_WEIGHTS_ARCHIVE" = "1" ] && dir_has_payload_files "$PEOPLE_FLOW_WEIGHTS_SOURCE"; then echo "Separate weights archive created: $WEIGHTS_ARCHIVE_PATH" else echo "Separate weights archive skipped; no people-flow weights payload found under $PEOPLE_FLOW_WEIGHTS_SOURCE" fi if [ "$GENERATE_WEIGHTS_ARCHIVE" = "1" ] && [ -f "$PEOPLE_FLOW_WEIGHTS_SOURCE/yolo11n.pt" ]; then echo "Separate YOLO archive created: $YOLO_ARCHIVE_PATH" else echo "Separate YOLO archive skipped; no yolo11n.pt found under $PEOPLE_FLOW_WEIGHTS_SOURCE" fi