ops: verify every release asset

This commit is contained in:
lingniu
2026-07-16 01:51:31 +08:00
parent c86c9d1e31
commit 3051caf327
5 changed files with 140 additions and 3 deletions

View File

@@ -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"

View File

@@ -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 '<!doctype html><div id="root"></div><script src="/assets/new.js"></script>\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 '<!doctype html><div id="root"></div>\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'

View File

@@ -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.

View File

@@ -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

View File

@@ -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"
}
}