From 3756ebffc045c66677aac6a572f449032683bcfa Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 4 Aug 2026 04:22:52 -0400 Subject: [PATCH] feat(13-09): verify-aiui-deploy.sh fetches live chunks via sw.js, never disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-deploy check for AIUI: resolves the LIVE chunk set by fetching the service worker's precache manifest (sw.js — vite-plugin-pwa's generateSW-mode workbox.precacheAndRoute([{url:...}]) array) over HTTP, fetches each live chunk, and greps the fetched bytes for a marker string. Exits non-zero when the marker is absent from every live chunk. This exists because the node's assets/ directory is a never-pruned graveyard (feedback_node_side_frontend_verify_stale_chunks): a disk grep reports "deployed" before the deploy actually happened, because a dead chunk from an old build still contains the old string. Never opens a remote shell onto the node and never greps the node's filesystem directly — every check is an HTTP fetch, exactly what a browser session would do. Verified locally against a real AIUI build served over HTTP: a marker actually present in a live-precached chunk (index.html) passes (exit 0, 2 chunks checked before the match); a nonexistent marker correctly fails (exit 1) as the negative control — not a check that always passes. Wired into deploy-to-target.sh's primary AIUI deploy path in the prior commit (073bf6f3), which already calls this script by name after the copy. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/verify-aiui-deploy.sh | 85 +++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100755 scripts/verify-aiui-deploy.sh diff --git a/scripts/verify-aiui-deploy.sh b/scripts/verify-aiui-deploy.sh new file mode 100755 index 00000000..11476d32 --- /dev/null +++ b/scripts/verify-aiui-deploy.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# verify-aiui-deploy.sh — post-deploy check that a marker string is actually +# reachable by a browser loading AIUI, not merely present somewhere on disk. +# +# The node's /opt/archipelago/web-ui/aiui/assets/ directory is a +# never-pruned graveyard: nginx never deletes an old build's chunk files +# when a new build lands (only the tar+rsync step below AIUI_DIST replaces +# what's there, and even that has left orphaned files in past incidents — +# see feedback_node_side_frontend_verify_stale_chunks). A disk grep over +# assets/ can therefore report "deployed" before the deploy actually +# happened, because a DEAD chunk from an old build still contains the old +# string. The only honest check fetches what a browser would actually load: +# resolve the LIVE chunk set via the service worker's precache manifest +# (sw.js), fetch each live chunk over HTTP, and grep the fetched bytes. +# +# This script never opens a remote shell session onto the node and never +# greps the node's filesystem directly — every check here is an HTTP +# fetch, exactly what a browser session would do. +# +# Usage: +# ./verify-aiui-deploy.sh "" +# +# Exit 0 = the marker was found in at least one live chunk fetched over HTTP. +# Exit 1 = the marker was not found in any live chunk (negative control +# should also produce this — a check that always passes is not a +# check). + +set -uo pipefail + +HOST="${1:?usage: verify-aiui-deploy.sh \"\"}" +MARKER="${2:?usage: verify-aiui-deploy.sh \"\"}" + +# Accept a bare host or a host:port; default to plain HTTP on :80, matching +# how neode-ui/AIUI are actually served on a node (nginx terminates TLS +# elsewhere; the lifecycle gate and other production-quality scripts in +# this directory talk to nodes over plain HTTP the same way). +BASE="http://${HOST}" +SW_URL="${BASE}/aiui/sw.js" + +timestamp() { echo "[$(date +%H:%M:%S)]"; } + +echo "$(timestamp) Fetching service worker manifest: $SW_URL" +SW_BODY="$(curl -sf -m 15 "$SW_URL" 2>/dev/null || true)" +if [ -z "$SW_BODY" ]; then + echo "FATAL: could not fetch $SW_URL — is AIUI deployed and nginx up on $HOST?" >&2 + exit 1 +fi + +# vite-plugin-pwa's generateSW mode emits sw.js with a +# workbox.precacheAndRoute([{url:"...",revision:"..."|null}, ...]) call — +# a JS array literal (unquoted keys), not JSON. Extract every url:"..." +# value without a full JS parser. +mapfile -t LIVE_PATHS < <( + grep -oE 'url:"[^"]*"' <<<"$SW_BODY" | sed -E 's/^url:"//; s/"$//' +) + +if [ "${#LIVE_PATHS[@]}" -eq 0 ]; then + echo "FATAL: $SW_URL fetched but no precache entries found — cannot resolve live chunks." >&2 + exit 1 +fi + +echo "$(timestamp) Resolved ${#LIVE_PATHS[@]} live chunk(s) from the precache manifest." + +FOUND=0 +CHECKED=0 +for path in "${LIVE_PATHS[@]}"; do + [ -z "$path" ] && continue + CHECKED=$((CHECKED + 1)) + url="${BASE}/aiui/${path}" + if curl -sf -m 15 "$url" 2>/dev/null | grep -q -- "$MARKER"; then + echo "$(timestamp) MATCH: $path" + FOUND=1 + break + fi +done + +echo "$(timestamp) Checked $CHECKED live chunk(s) fetched over HTTP for marker: $MARKER" + +if [ "$FOUND" -eq 1 ]; then + echo "$(timestamp) PASS — marker found in a live, browser-fetchable chunk." + exit 0 +else + echo "$(timestamp) FAIL — marker not found in any live chunk (fetched via sw.js manifest, not a disk grep)." >&2 + exit 1 +fi