Files
Reactive_Resume/scripts/test-personal-install-packages.sh

339 lines
14 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
if docker exec "$PROJECT-reactive-resume-1" sh -lc 'APP_DIR=$(cat /tmp/reactive-resume-app-dir); grep -R -E "index-[A-Za-z0-9_-]+\\.js\\?v=rr-filename-title" "$APP_DIR/.output/public/assets" >/dev/null 2>&1'; then
fail "direct 包错误地给 index 主入口追加了 rr-filename-title 缓存标记"
fi
docker exec "$PROJECT-reactive-resume-1" sh -lc 'APP_DIR=$(cat /tmp/reactive-resume-app-dir); grep -R "String(t).trim().replace" "$APP_DIR/.output/public/assets"/file-*.js >/dev/null 2>&1' \
|| fail "direct 包未在 file-*.js 下载工具中应用文件名补丁"
curl -fsS "http://127.0.0.1:3004/" >/tmp/reactive-resume-home.html \
|| fail "direct 包首页无法访问"
grep -E '/assets/index-[A-Za-z0-9_-]+-rr[0-9a-z]+\.js' /tmp/reactive-resume-home.html >/dev/null \
|| fail "direct 包首页未改用防缓存的 index 主入口文件名"
curl -fsS "http://127.0.0.1:3004/sw.js" >/tmp/reactive-resume-sw.js \
|| fail "direct 包 sw.js 无法访问"
grep -q 'disable stale PWA caches' /tmp/reactive-resume-sw.js \
|| fail "direct 包 sw.js 未替换为清理旧缓存版本"
if command -v google-chrome >/dev/null 2>&1 && command -v node >/dev/null 2>&1; then
log "使用 Chrome 执行 direct 包首页 JS"
node <<'NODE'
const { spawn } = require("child_process");
const fs = require("fs");
const http = require("http");
const port = 9444;
const userData = "/tmp/reactive-resume-chrome-smoke";
fs.rmSync(userData, { recursive: true, force: true });
const chrome = spawn("google-chrome", [
"--headless=new",
"--disable-gpu",
"--no-sandbox",
`--remote-debugging-port=${port}`,
`--user-data-dir=${userData}`,
"about:blank",
], { stdio: ["ignore", "ignore", "ignore"] });
function request(path, method = "GET") {
return new Promise((resolve, reject) => {
const req = http.request({ host: "127.0.0.1", port, path, method }, (res) => {
let body = "";
res.on("data", (chunk) => body += chunk);
res.on("end", () => resolve({ status: res.statusCode, body }));
});
req.on("error", reject);
req.end();
});
}
async function waitForChrome() {
for (let i = 0; i < 60; i++) {
try {
const res = await request("/json/version");
if (res.status === 200) return;
} catch {}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error("Chrome remote debugging was not ready");
}
(async () => {
await waitForChrome();
const tab = JSON.parse((await request("/json/new?about:blank", "PUT")).body);
const ws = new WebSocket(tab.webSocketDebuggerUrl);
let id = 0;
const pending = new Map();
const errors = [];
const indexRequests = [];
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.id && pending.has(message.id)) {
pending.get(message.id)(message);
pending.delete(message.id);
return;
}
if (message.method === "Runtime.exceptionThrown") {
errors.push(message.params.exceptionDetails);
}
if (message.method === "Network.requestWillBeSent" && message.params.request.url.includes("index-")) {
indexRequests.push(message.params.request.url);
}
};
await new Promise((resolve) => { ws.onopen = resolve; });
function send(method, params = {}) {
return new Promise((resolve) => {
pending.set(++id, resolve);
ws.send(JSON.stringify({ id, method, params }));
});
}
await send("Runtime.enable");
await send("Page.enable");
await send("Network.enable");
await send("Page.navigate", { url: "http://127.0.0.1:3004/" });
await new Promise((resolve) => setTimeout(resolve, 5000));
const result = await send("Runtime.evaluate", {
expression: "document.body.innerText.includes('A free and open-source resume builder')",
returnByValue: true,
});
ws.close();
chrome.kill("SIGKILL");
if (errors.length > 0) {
console.error(JSON.stringify(errors.map((error) => ({
text: error.text,
url: error.url,
line: error.lineNumber,
column: error.columnNumber,
exception: error.exception?.description,
})), null, 2));
process.exit(1);
}
if (!result.result?.result?.value) {
console.error("Chrome did not render the expected home page text");
process.exit(1);
}
if (indexRequests.some((url) => /\/assets\/index-[A-Za-z0-9_-]+\.js$/.test(url) && !/-rr[0-9a-z]+\.js$/.test(url))) {
console.error(`Chrome still requested stale app shell: ${indexRequests.join(", ")}`);
process.exit(1);
}
})().catch((error) => {
chrome.kill("SIGKILL");
console.error(error);
process.exit(1);
});
NODE
else
log "未找到 google-chrome跳过浏览器级首页 JS 测试"
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 未改为按标题下载"
if grep -R -E "index-[A-Za-z0-9_-]+\\.js\\?v=rr-filename-title" "$ARM64_ASSETS_DIR" >/dev/null 2>&1; then
fail "arm64 补丁错误地给 index 主入口追加了 rr-filename-title 缓存标记"
fi
find "$TMP_DIR/arm64-root/app" -type f -exec grep -qE 'index-[A-Za-z0-9_-]+-rr[0-9a-z]+\.js' {} \; -print -quit \
| grep -q . \
|| fail "arm64 补丁未改用防缓存的 index 主入口文件名"
find "$TMP_DIR/arm64-root/app" -path '*/sw.js' -type f -print0 | xargs -0 grep -l 'disable stale PWA caches' >/dev/null \
|| fail "arm64 sw.js 未替换为清理旧缓存版本"
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"