diff --git a/vehicle-data-platform/deploy/verify-web-release.sh b/vehicle-data-platform/deploy/verify-web-release.sh new file mode 100755 index 00000000..77469a1c --- /dev/null +++ b/vehicle-data-platform/deploy/verify-web-release.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +set -euo pipefail + +WEB_ROOT=${1:-/opt/lingniu-vehicle-platform/current/web} +BASE_URL=${2:-http://127.0.0.1:20300} +PREVIOUS_MANIFEST=${3:-} +CURL_TIMEOUT_SEC=${CURL_TIMEOUT_SEC:-15} + +BASE_URL=${BASE_URL%/} +CURRENT_MANIFEST="$WEB_ROOT/.release-assets" +response_file=$(mktemp) +trap 'rm -f "$response_file"' EXIT + +fail() { + printf 'web release smoke failed: %s\n' "$*" >&2 + exit 1 +} + +fetch_exact() { + local url_path=$1 + local expected_file=$2 + local label=$3 + local status + + test -f "$expected_file" || fail "$label is missing on disk: $expected_file" + status=$(curl --silent --show-error --max-time "$CURL_TIMEOUT_SEC" --output "$response_file" --write-out '%{http_code}' "$BASE_URL$url_path" || true) + test "$status" = 200 || fail "$label returned HTTP ${status:-000}: $url_path" + cmp -s "$expected_file" "$response_file" || fail "$label response differs from release file: $url_path" +} + +verify_manifest() { + local manifest=$1 + local kind=$2 + local count=0 + local asset + + test -f "$manifest" || fail "$kind manifest is missing: $manifest" + while IFS= read -r asset || test -n "$asset"; do + asset=${asset%$'\r'} + test -n "$asset" || continue + case "$asset" in + /*|*../*|../*|*/..|..) + fail "$kind manifest contains unsafe asset path: $asset" + ;; + esac + fetch_exact "/assets/$asset" "$WEB_ROOT/assets/$asset" "$kind asset" + count=$((count + 1)) + done < "$manifest" + test "$count" -gt 0 || fail "$kind manifest is empty: $manifest" + printf '%s' "$count" +} + +fetch_exact / "$WEB_ROOT/index.html" 'root document' + +config_status=$(curl --silent --show-error --max-time "$CURL_TIMEOUT_SEC" --output "$response_file" --write-out '%{http_code}' "$BASE_URL/app-config.js" || true) +test "$config_status" = 200 || fail "app config returned HTTP ${config_status:-000}" +grep -q 'window\.__LINGNIU_APP_CONFIG__' "$response_file" || fail 'app config response does not expose the runtime configuration object' + +current_count=$(verify_manifest "$CURRENT_MANIFEST" current) +compatibility_count=0 +if test -n "$PREVIOUS_MANIFEST"; then + compatibility_count=$(verify_manifest "$PREVIOUS_MANIFEST" compatibility) +fi + +printf 'web_release_smoke=ok current_assets=%s compatibility_assets=%s\n' "$current_count" "$compatibility_count" diff --git a/vehicle-data-platform/deploy/verify-web-release.test.sh b/vehicle-data-platform/deploy/verify-web-release.test.sh new file mode 100755 index 00000000..188dc341 --- /dev/null +++ b/vehicle-data-platform/deploy/verify-web-release.test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +fixture=$(mktemp -d) +server_pid= +cleanup() { + if test -n "$server_pid"; then + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi + rm -rf "$fixture" +} +trap cleanup EXIT + +mkdir -p "$fixture/web/assets" +printf '
\n' > "$fixture/web/index.html" +printf 'window.__LINGNIU_APP_CONFIG__ = {};\n' > "$fixture/web/app-config.js" +printf 'console.log("new");\n' > "$fixture/web/assets/new.js" +printf 'console.log("old");\n' > "$fixture/web/assets/old.js" +printf 'new.js\n' > "$fixture/web/.release-assets" +printf 'old.js\n' > "$fixture/previous-assets" +cp -R "$fixture/web" "$fixture/served" + +port=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()') +python3 -m http.server "$port" --bind 127.0.0.1 --directory "$fixture/served" > "$fixture/server.log" 2>&1 & +server_pid=$! +for _ in $(seq 1 30); do + if curl --silent --fail "http://127.0.0.1:$port/" >/dev/null; then break; fi + sleep 0.1 +done + +success_output=$("$SCRIPT_DIR/verify-web-release.sh" "$fixture/web" "http://127.0.0.1:$port" "$fixture/previous-assets") +test "$success_output" = 'web_release_smoke=ok current_assets=1 compatibility_assets=1' + +rm "$fixture/web/assets/old.js" +if "$SCRIPT_DIR/verify-web-release.sh" "$fixture/web" "http://127.0.0.1:$port" "$fixture/previous-assets" > "$fixture/missing.out" 2>&1; then + printf 'expected a missing compatibility asset to fail\n' >&2 + exit 1 +fi +grep -q 'compatibility asset is missing on disk' "$fixture/missing.out" + +printf 'console.log("old");\n' > "$fixture/web/assets/old.js" +printf '
\n' > "$fixture/served/assets/old.js" +if "$SCRIPT_DIR/verify-web-release.sh" "$fixture/web" "http://127.0.0.1:$port" "$fixture/previous-assets" > "$fixture/mismatch.out" 2>&1; then + printf 'expected a fallback response to fail byte comparison\n' >&2 + exit 1 +fi +grep -q 'response differs from release file' "$fixture/mismatch.out" + +printf 'console.log("old");\n' > "$fixture/served/assets/old.js" +printf '../secret.js\n' > "$fixture/previous-assets" +if "$SCRIPT_DIR/verify-web-release.sh" "$fixture/web" "http://127.0.0.1:$port" "$fixture/previous-assets" > "$fixture/unsafe.out" 2>&1; then + printf 'expected an unsafe manifest path to fail\n' >&2 + exit 1 +fi +grep -q 'unsafe asset path' "$fixture/unsafe.out" + +printf 'verify-web-release tests passed\n' diff --git a/vehicle-data-platform/docs/deployment.md b/vehicle-data-platform/docs/deployment.md index bbe6b6c9..84536785 100644 --- a/vehicle-data-platform/docs/deployment.md +++ b/vehicle-data-platform/docs/deployment.md @@ -54,7 +54,17 @@ else fi ``` -Never overwrite a new hashed file. The `.release-assets` manifest prevents compatibility files from accumulating recursively: the next release copies only the immediately previous build's original assets. The React route boundary also detects a failed dynamic import, reloads once with a short cooldown and keeps the application shell usable if recovery still fails. Verify one old route asset and the new main asset both return HTTP 200 after the switch. +Never overwrite a new hashed file. The `.release-assets` manifest prevents compatibility files from accumulating recursively: the next release copies only the immediately previous build's original assets. The React route boundary also detects a failed dynamic import, reloads once with a short cooldown and keeps the application shell usable if recovery still fails. + +After the switch, verify the root document, runtime configuration, every current asset and every compatibility asset from the immediately previous release. The smoke downloads each asset and compares it byte-for-byte with the release file, so a SPA fallback returning `index.html` with HTTP 200 cannot hide a missing chunk: + +```bash +PREVIOUS_WEB=/opt/lingniu-vehicle-platform/releases/$PREVIOUS_RELEASE/web +/opt/lingniu-vehicle-platform/current/deploy/verify-web-release.sh \ + /opt/lingniu-vehicle-platform/current/web \ + http://127.0.0.1:20300 \ + "$PREVIOUS_WEB/.release-assets" +``` ## Environment @@ -210,7 +220,7 @@ curl -fsS "http://127.0.0.1:20300$MAIN_ASSET" >/dev/null unset PLATFORM_TOKEN AUTH_HEADER ``` -The release gate must include both the root document and its hashed main asset. API-only smoke checks are insufficient because an incomplete archive can leave the service healthy while the browser returns 404. Before upload, verify the archive contains `web/index.html`, `platform-api`, both evaluator binaries, `platform-migrate`, `oneos-scope-sync`, every numbered migration and all platform systemd units. +The release gate must include the root document, runtime configuration, every asset in the current `.release-assets` manifest and every asset in the immediately previous release manifest. API-only smoke checks are insufficient because an incomplete archive can leave the service healthy while the browser returns 404 or an HTML fallback for a missing chunk. Before upload, verify the archive contains `web/index.html`, `platform-api`, both evaluator binaries, `platform-migrate`, `oneos-scope-sync`, every numbered migration, the web-release verifier and all platform systemd units. When `alertStream.lastInvalidCode` reports a recent `missing_vin_jt808`, query `/api/v2/access/unresolved-identities` as a viewer and hand the masked evidence to the identity owner. The response must not contain raw `phone` or unmasked identifier fields. Confirm the evidence against the GPS provider or vehicle owner, then maintain the authoritative `vehicle_identity_binding`; never derive VIN from the terminal number. The queue excludes a terminal after a valid binding exists. Retain the invalid counter as audit evidence and verify the recent warning clears after five minutes without another invalid frame. diff --git a/vehicle-data-platform/docs/frontend-production-readiness.md b/vehicle-data-platform/docs/frontend-production-readiness.md index c70920b0..872ea6f0 100644 --- a/vehicle-data-platform/docs/frontend-production-readiness.md +++ b/vehicle-data-platform/docs/frontend-production-readiness.md @@ -76,7 +76,7 @@ The navigation and progressive-loading direction follows mature observability sy 2. Bound or explicitly evict other high-cardinality query families, especially arbitrary history windows and track playback ranges. 3. Retire or migrate the remaining legacy `App.tsx` integration suite after its compatibility coverage is replaced in V2. 4. Profile repeated large exports after the Web Worker migration and define an explicit maximum supported row/column envelope for browser-side workbooks. -5. Add an automated release smoke that reads the previous `.release-assets` manifest and asserts every listed compatibility asset returns HTTP 200. +5. Run the automated byte-for-byte current/compatibility asset smoke in every future ECS release and promote it into CI/CD when the deployment pipeline is formalized. ## 2026-07-15: query memory and vehicle-count semantics diff --git a/vehicle-data-platform/package.json b/vehicle-data-platform/package.json index 4d6235d2..b58932b8 100644 --- a/vehicle-data-platform/package.json +++ b/vehicle-data-platform/package.json @@ -5,6 +5,7 @@ "web:build": "pnpm --dir apps/web run build", "web:test": "pnpm --dir apps/web run test", "api:test": "cd apps/api && go test ./...", + "release:web-smoke:test": "bash deploy/verify-web-release.test.sh", "build": "pnpm run web:build && cd apps/api && go build -o ../../dist/platform-api ./cmd/platform-api && go build -o ../../dist/oneos-scope-sync ./cmd/oneos-scope-sync" } }