#!/usr/bin/env bash set -euo pipefail RELEASE_ID=${1:?release id is required} ARCHIVE=${2:?web archive is required} API_BINARY=${3:-} ONEOS_SCOPE_BINARY=${4:-} RECONCILIATION_BINARY=${5:-} ROOT=${PLATFORM_ROOT:-/opt/lingniu-vehicle-platform} BASE_URL=${PLATFORM_BASE_URL:-http://127.0.0.1:20300} SERVICE=${PLATFORM_SERVICE:-lingniu-vehicle-platform} COMPATIBILITY_GENERATIONS=${COMPATIBILITY_GENERATIONS:-3} RELEASE_HISTORY_LIMIT=${RELEASE_HISTORY_LIMIT:-20} SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SYSTEMCTL_BIN=${SYSTEMCTL_BIN:-systemctl} CURL_BIN=${CURL_BIN:-curl} case "$RELEASE_ID" in ''|*[!A-Za-z0-9._-]*) printf 'invalid release id: %s\n' "$RELEASE_ID" >&2; exit 1 ;; esac test -f "$ARCHIVE" || { printf 'web archive is missing: %s\n' "$ARCHIVE" >&2; exit 1; } if test -n "$API_BINARY"; then test -f "$API_BINARY" || { printf 'platform API binary is missing: %s\n' "$API_BINARY" >&2; exit 1; } fi if test -n "$ONEOS_SCOPE_BINARY"; then test -f "$ONEOS_SCOPE_BINARY" || { printf 'OneOS scope sync binary is missing: %s\n' "$ONEOS_SCOPE_BINARY" >&2; exit 1; } fi if test -n "$RECONCILIATION_BINARY"; then test -f "$RECONCILIATION_BINARY" || { printf 'reconciliation evaluator binary is missing: %s\n' "$RECONCILIATION_BINARY" >&2; exit 1; } fi test -x "$SCRIPT_DIR/prepare-web-release-tree.sh" || { printf 'release tree helper is missing\n' >&2; exit 1; } test -f "$SCRIPT_DIR/prune-release-history.py" || { printf 'release pruning helper is missing\n' >&2; exit 1; } test -f "$SCRIPT_DIR/verify-customer-demo.py" || { printf 'customer demo gate is missing\n' >&2; exit 1; } old=$(readlink -f "$ROOT/current") next="$ROOT/releases/$RELEASE_ID" env_file="$ROOT/env/platform.env" old_release=$(sed -n 's/^PLATFORM_RELEASE=//p' "$env_file" | tail -1) switched=false set_release_env() { python3 - "$env_file" "$1" <<'PY' from __future__ import print_function import io import sys path, release = sys.argv[1], sys.argv[2] with io.open(path, 'r', encoding='utf-8') as handle: lines = handle.read().splitlines() updated = [] found = False for line in lines: if line.startswith('PLATFORM_RELEASE='): updated.append('PLATFORM_RELEASE=' + release) found = True else: updated.append(line) if not found: updated.append('PLATFORM_RELEASE=' + release) with io.open(path, 'w', encoding='utf-8') as handle: handle.write('\n'.join(updated) + '\n') PY } rollback() { local status=$? trap - ERR set +e if test "$switched" = true; then ln -s "$old" "$ROOT/current.rollback" python3 -c 'import os,sys; os.replace(sys.argv[1], sys.argv[2])' "$ROOT/current.rollback" "$ROOT/current" set_release_env "$old_release" "$SYSTEMCTL_BIN" restart "$SERVICE" fi printf 'web release install failed; current restored to %s\n' "$old" >&2 exit "$status" } trap rollback ERR test ! -e "$next" || { printf 'release already exists: %s\n' "$next" >&2; exit 1; } mkdir -p "$next/web" for runtime_file in platform-api alert-evaluator alert-stream-evaluator platform-migrate oneos-scope-sync reconciliation-evaluator alert-benchmark; do if test -f "$old/$runtime_file"; then cp "$old/$runtime_file" "$next/$runtime_file" fi done test -f "$next/platform-api" || { printf 'current release is missing platform-api\n' >&2; exit 1; } if test -n "$API_BINARY"; then cp "$API_BINARY" "$next/platform-api" fi if test -n "$ONEOS_SCOPE_BINARY"; then cp "$ONEOS_SCOPE_BINARY" "$next/oneos-scope-sync" fi if test -n "$RECONCILIATION_BINARY"; then cp "$RECONCILIATION_BINARY" "$next/reconciliation-evaluator" fi if test -f "$old/lingniu-vehicle-platform.service"; then cp "$old/lingniu-vehicle-platform.service" "$next/lingniu-vehicle-platform.service" fi cp -a "$old/deploy" "$next/deploy" if test -d "$SCRIPT_DIR/migrations"; then mkdir -p "$next/deploy/migrations" cp "$SCRIPT_DIR"/migrations/*.sql "$next/deploy/migrations/" fi if test -d "$SCRIPT_DIR/systemd"; then mkdir -p "$next/deploy/systemd" cp "$SCRIPT_DIR"/systemd/* "$next/deploy/systemd/" fi cp "$SCRIPT_DIR/prepare-web-release-tree.sh" "$next/deploy/prepare-web-release-tree.sh" cp "$SCRIPT_DIR/prune-release-history.py" "$next/deploy/prune-release-history.py" cp "$SCRIPT_DIR/verify-customer-demo.py" "$next/deploy/verify-customer-demo.py" cp "$0" "$next/deploy/install-web-release.sh" chmod +x "$next/platform-api" "$next/deploy/"*.sh "$next/deploy/prune-release-history.py" "$next/deploy/verify-customer-demo.py" for runtime_file in alert-evaluator alert-stream-evaluator platform-migrate oneos-scope-sync reconciliation-evaluator alert-benchmark; do test ! -f "$next/$runtime_file" || chmod +x "$next/$runtime_file" done while IFS= read -r member; do case "$member" in /*|../*|*/../*|*/..) printf 'web archive contains unsafe path: %s\n' "$member" >&2; exit 1 ;; esac done < <(tar -tzf "$ARCHIVE") tar --no-same-owner -xzf "$ARCHIVE" -C "$next/web" find "$next/web" -type f -name '._*' -delete "$next/deploy/prepare-web-release-tree.sh" "$next/web" "$old/web" "$COMPATIBILITY_GENERATIONS" ln -s "$next" "$ROOT/current.next" python3 -c 'import os,sys; os.replace(sys.argv[1], sys.argv[2])' "$ROOT/current.next" "$ROOT/current" switched=true set_release_env "$RELEASE_ID" "$SYSTEMCTL_BIN" restart "$SERVICE" ready=false for _ in $(seq 1 15); do if "$SYSTEMCTL_BIN" is-active --quiet "$SERVICE" && "$CURL_BIN" -fsS "$BASE_URL/" >/dev/null; then ready=true; break; fi sleep 1 done test "$ready" = true || { printf 'service did not become ready\n' >&2; exit 1; } verify_bin=${VERIFY_WEB_RELEASE_BIN:-$next/deploy/verify-web-release.sh} "$verify_bin" "$next/web" "$BASE_URL" "$next/web/.compatibility-assets" if test -f "$ROOT/env/access-tokens.env"; then set -a # shellcheck disable=SC1090 . "$ROOT/env/access-tokens.env" set +a test -n "${VIEWER_TOKEN:-}" || { printf 'viewer token is unavailable for health verification\n' >&2; exit 1; } health=$("$CURL_BIN" -fsS -H "Authorization: Bearer $VIEWER_TOKEN" "$BASE_URL/api/ops/health") printf '%s' "$health" | python3 -c 'import json,sys; value=(((json.load(sys.stdin).get("data") or {}).get("runtime") or {}).get("platformRelease")); sys.exit(0 if value == sys.argv[1] else 1)' "$RELEASE_ID" fi prune_bin=${PRUNE_RELEASE_BIN:-$next/deploy/prune-release-history.py} python3 "$prune_bin" "$ROOT" "$RELEASE_HISTORY_LIMIT" "$next" "$old" rm -f "$ARCHIVE" switched=false trap - ERR printf 'web_release_install=ok release=%s previous=%s\n' "$RELEASE_ID" "$(basename "$old")"