#!/bin/sh set -eu ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" DIRECT_COMPOSE="$ROOT_DIR/packages/reactive-resume-personal-direct/compose.yml" DIRECT_ENV="$ROOT_DIR/packages/reactive-resume-personal-direct/.env" QNAP_COMPOSE="$ROOT_DIR/packages/reactive-resume-personal-qnap-nas/compose-Nas.yml" QNAP_PATCH_DIR="$ROOT_DIR/packages/reactive-resume-personal-qnap-nas/patches" DIRECT_PATCH_DIR="$ROOT_DIR/packages/reactive-resume-personal-direct/patches" QNAP_ZIP="$ROOT_DIR/dist/reactive-resume-personal-qnap-nas-20260520.zip" DIRECT_ZIP="$ROOT_DIR/dist/reactive-resume-personal-direct-20260520.zip" IMAGE_REPO="amruthpillai/reactive-resume" IMAGE_INDEX="$IMAGE_REPO@sha256:b760446c4301af067e7d595537a877e378363aa6ce921b7349e62983621826aa" PROJECT="reactive-resume-personal" log() { printf '\n[TEST] %s\n' "$*" } fail() { printf '\n[FAIL] %s\n' "$*" >&2 exit 1 } cleanup_direct() { docker compose -f "$DIRECT_COMPOSE" --env-file "$DIRECT_ENV" down -v >/dev/null 2>&1 || true } cleanup_tmp() { if [ -n "${TMP_DIR:-}" ] && [ -d "$TMP_DIR" ]; then rm -rf "$TMP_DIR" fi } cleanup_all() { cleanup_direct cleanup_tmp } trap cleanup_all HUP INT TERM EXIT cd "$ROOT_DIR" log "检查补丁脚本语法" sh -n "$QNAP_PATCH_DIR/reactive-resume-runtime-patch.sh" sh -n "$QNAP_PATCH_DIR/reactive-resume-entrypoint.sh" sh -n "$DIRECT_PATCH_DIR/reactive-resume-runtime-patch.sh" sh -n "$DIRECT_PATCH_DIR/reactive-resume-entrypoint.sh" log "检查 Compose 配置可解析" docker compose -f "$QNAP_COMPOSE" config >/tmp/reactive-resume-qnap-compose-test.yml docker compose -f "$DIRECT_COMPOSE" --env-file "$DIRECT_ENV" config >/tmp/reactive-resume-direct-compose-test.yml grep -q 'reactive-resume-entrypoint.sh' /tmp/reactive-resume-qnap-compose-test.yml grep -q 'reactive-resume-entrypoint.sh' /tmp/reactive-resume-direct-compose-test.yml log "检查 zip 安装包内容" unzip -t "$QNAP_ZIP" >/dev/null unzip -t "$DIRECT_ZIP" >/dev/null unzip -l "$QNAP_ZIP" | grep -q 'reactive_resume/compose-Nas.yml' unzip -l "$QNAP_ZIP" | grep -q 'reactive_resume/patches/reactive-resume-entrypoint.sh' unzip -l "$QNAP_ZIP" | grep -q 'reactive_resume/patches/reactive-resume-runtime-patch.sh' unzip -l "$DIRECT_ZIP" | grep -q 'reactive-resume-personal-direct/compose.yml' unzip -l "$DIRECT_ZIP" | grep -q 'reactive-resume-personal-direct/patches/reactive-resume-entrypoint.sh' if unzip -p "$QNAP_ZIP" 'reactive_resume/*' 2>/dev/null | grep -E 'isiseg|10004|Reactive_Resume_Personal|/share/Container/Reactive_Resume_Personal' >/dev/null; then fail "QNAP zip 中仍有旧域名、旧端口或旧路径" fi if unzip -p "$QNAP_ZIP" 'reactive_resume/*' 2>/dev/null | grep -E 'amruthpillai/reactive-resume:latest|snowdreamtech/frpc:latest' >/dev/null; then fail "QNAP zip 中仍有 latest 镜像" fi if unzip -p "$DIRECT_ZIP" 'reactive-resume-personal-direct/*' 2>/dev/null | grep -E 'amruthpillai/reactive-resume:latest|snowdreamtech/frpc:latest' >/dev/null; then fail "direct zip 中仍有 latest 镜像" fi log "真实启动 direct 包并检查健康状态" cleanup_direct docker compose -f "$DIRECT_COMPOSE" --env-file "$DIRECT_ENV" up -d postgres reactive-resume seed attempt=0 until curl -fsS "http://127.0.0.1:3004/api/health" >/tmp/reactive-resume-health.json 2>/dev/null; do attempt=$((attempt + 1)) if [ "$attempt" -ge 60 ]; then docker logs "$PROJECT-reactive-resume-1" --tail 200 >&2 || true fail "direct 包启动后 /api/health 未在 60 秒内就绪" fi sleep 1 done docker wait "$PROJECT-seed-1" >/tmp/reactive-resume-seed-exit seed_status="$(docker inspect "$PROJECT-seed-1" --format '{{.State.ExitCode}}' 2>/dev/null || printf 'missing')" [ "$seed_status" = "0" ] || fail "seed 容器退出码不是 0:$seed_status" attempt=0 until curl -fsS -I "http://127.0.0.1:3004/audience/resume" >/tmp/reactive-resume-audience.headers 2>/dev/null \ && grep -q '200 OK' /tmp/reactive-resume-audience.headers; do attempt=$((attempt + 1)) if [ "$attempt" -ge 30 ]; then docker logs "$PROJECT-reactive-resume-1" --tail 200 >&2 || true fail "seed 完成后 /audience/resume 未在 30 秒内返回 200" fi sleep 1 done docker logs "$PROJECT-reactive-resume-1" --tail 200 >/tmp/reactive-resume-direct.log 2>&1 || true if grep -E 'Cannot find module|Buffer is not defined|Unexpected end of input' /tmp/reactive-resume-direct.log >/dev/null; then cat /tmp/reactive-resume-direct.log >&2 fail "direct 包日志仍包含已知启动或前端错误" fi log "离线检查 arm64/QNAP 镜像布局" ARM64_DIGEST="$( docker manifest inspect "$IMAGE_INDEX" \ | node -e ' let source = ""; process.stdin.on("data", (chunk) => source += chunk); process.stdin.on("end", () => { const manifest = JSON.parse(source); const arm = manifest.manifests.find((item) => item.platform?.os === "linux" && item.platform?.architecture === "arm64"); if (!arm) process.exit(2); process.stdout.write(arm.digest); }); ' )" TMP_DIR="$(mktemp -d)" CID="$(docker create --platform linux/arm64 "$IMAGE_INDEX" 2>/dev/null || true)" [ -n "$CID" ] || fail "无法创建 arm64 镜像容器用于离线检查" docker export "$CID" -o "$TMP_DIR/arm64-root.tar" docker rm "$CID" >/dev/null mkdir -p "$TMP_DIR/arm64-root" tar -xf "$TMP_DIR/arm64-root.tar" -C "$TMP_DIR/arm64-root" if [ -f "$TMP_DIR/arm64-root/app/apps/server/dist/index.mjs" ]; then ARM64_SERVER_ENTRY="$TMP_DIR/arm64-root/app/apps/server/dist/index.mjs" ARM64_FILENAME_ENTRY="$ARM64_SERVER_ENTRY" ARM64_ASSETS_DIR="$TMP_DIR/arm64-root/app/apps/web/dist/assets" EXPECTED_ENTRYPOINT_PWD="$TMP_DIR/arm64-root/app" EXPECTED_ENTRYPOINT_ARGS="node apps/server/dist/index.mjs" elif [ -f "$TMP_DIR/arm64-root/app/apps/web/.output/server/index.mjs" ]; then ARM64_SERVER_ENTRY="$TMP_DIR/arm64-root/app/apps/web/.output/server/index.mjs" ARM64_FILENAME_ENTRY="$(grep -Rsl 'function generateFilename' "$TMP_DIR/arm64-root/app/apps/web/.output/server/_ssr" 2>/dev/null | head -n 1 || true)" [ -n "$ARM64_FILENAME_ENTRY" ] || fail "arm64 .output 布局中未找到 generateFilename SSR bundle" ARM64_ASSETS_DIR="$TMP_DIR/arm64-root/app/apps/web/.output/public/assets" EXPECTED_ENTRYPOINT_PWD="$TMP_DIR/arm64-root/app/apps/web" EXPECTED_ENTRYPOINT_ARGS="node .output/server/index.mjs" else find "$TMP_DIR/arm64-root/app" -maxdepth 6 \( -name index.mjs -o -name server.js -o -name main.js \) 2>/dev/null >&2 || true fail "arm64 镜像中未找到支持的服务入口" fi [ -d "$ARM64_ASSETS_DIR" ] || fail "arm64 镜像中未找到 assets 目录:$ARM64_ASSETS_DIR" perl -0pe " s#/app/apps#$TMP_DIR/arm64-root/app/apps#g; s#for candidate in $TMP_DIR/arm64-root/app/apps/web /app#for candidate in $TMP_DIR/arm64-root/app/apps/web $TMP_DIR/arm64-root/app#g; s#find /app#find $TMP_DIR/arm64-root/app#g; s#APP_DIR=\"/app\"#APP_DIR=\"$TMP_DIR/arm64-root/app\"#g; s#under /app#under $TMP_DIR/arm64-root/app#g; " "$QNAP_PATCH_DIR/reactive-resume-runtime-patch.sh" > "$TMP_DIR/runtime-patch-arm64-test.sh" sh "$TMP_DIR/runtime-patch-arm64-test.sh" >/tmp/reactive-resume-arm64-runtime.log 2>&1 || { cat /tmp/reactive-resume-arm64-runtime.log >&2 fail "arm64 离线运行 runtime patch 失败" } grep -R 'rr-browser-buffer-polyfill' "$ARM64_ASSETS_DIR" >/dev/null \ || fail "arm64 public PDF bundle 未注入 Buffer polyfill" grep -R -F 'replace(/[\\/:*?"<>|]/g' "$ARM64_ASSETS_DIR" >/dev/null \ || fail "arm64 文件名 bundle 未改为按标题下载" grep -q 'function generateFilename(prefix, extension)' "$ARM64_FILENAME_ENTRY" \ || fail "arm64 server entry 未包含 generateFilename" grep -F 'filename.replace(/[\\/:*?"<>|]/g' "$ARM64_FILENAME_ENTRY" >/dev/null \ || fail "arm64 server entry 未改为按标题生成下载文件名" perl -0pe " s#/app/apps#$TMP_DIR/arm64-root/app/apps#g; s#cd /app#cd $TMP_DIR/arm64-root/app#g; s#find /app#find $TMP_DIR/arm64-root/app#g; s#under /app#under $TMP_DIR/arm64-root/app#g; " "$QNAP_PATCH_DIR/reactive-resume-entrypoint.sh" > "$TMP_DIR/entrypoint-arm64-test.sh" mkdir -p "$TMP_DIR/fakebin" { printf '#!/bin/sh\n' printf 'printf "PWD=%%s\\n" "$PWD" > "%s/entrypoint-result.txt"\n' "$TMP_DIR" printf 'printf "ARGS=%%s\\n" "$*" >> "%s/entrypoint-result.txt"\n' "$TMP_DIR" } > "$TMP_DIR/fakebin/docker-entrypoint.sh" chmod +x "$TMP_DIR/fakebin/docker-entrypoint.sh" PATH="$TMP_DIR/fakebin:$PATH" sh "$TMP_DIR/entrypoint-arm64-test.sh" >/tmp/reactive-resume-arm64-entrypoint.log 2>&1 || { cat /tmp/reactive-resume-arm64-entrypoint.log >&2 fail "arm64 entrypoint 选择测试失败" } grep -q "PWD=$EXPECTED_ENTRYPOINT_PWD" "$TMP_DIR/entrypoint-result.txt" \ || fail "arm64 entrypoint 未切换到预期目录:$EXPECTED_ENTRYPOINT_PWD" grep -q "ARGS=$EXPECTED_ENTRYPOINT_ARGS" "$TMP_DIR/entrypoint-result.txt" \ || fail "arm64 entrypoint 未选择预期入口:$EXPECTED_ENTRYPOINT_ARGS" log "清理 direct 测试容器" cleanup_direct log "全部测试通过" printf 'direct health: %s\n' "$(cat /tmp/reactive-resume-health.json)" printf 'arm64 digest: %s\n' "$ARM64_DIGEST"