forked from bai/curriculum-project-hub
build(deploy): 部署与限流配置切换到 filelib-web,并加共存回归测试
三处引用旧工程名/旧资源路径的地方一并更新,它们必须同时改 —— 少改一处 就是静默故障,而不是构建期报错: 1. 部署脚本(deploy_platform.sh / deploy_fleet_release.sh):npm ci 的 prefix、rsync 排除项、构建产物存在性检查从 database-admin 换成 filelib-web。最后一项是真门禁:static.ts 缺产物时只 warn 不注册路由, 漏改会让 /app 与 /database 静默 404 —— 恰是 database-admin 长期处于 禁用状态的原因。 2. silo 限流豁免:资源路径随 appDir 改名而变(/database/_app/* 已不存在, 现为 /_filelib/*);/app/* 此前不在豁免列表,它现在也是 SPA 外壳, 客户端路由无法预先枚举。 注:/database/* 是整体豁免,filelib 的 JSON API 也绕过限流预算。这是 迁移前就有的行为,原样保留,但覆盖面因多了 /app/* 而变宽。 3. 回归测试:把 registerStaticSpa 与 registerDatabaseSpa 挂到同一个 Fastify 实例,断言 ready() 不因重复路由抛错 —— appDir 若用回默认的 _app,这里会红(ADR-0029 的承重约束)。另断言 /app 与 /database/dashboard/users 返回同一份字节(SPA 回退不读请求)、body 含 /_filelib/。构建产物缺失时不 skip 而是直接失败:那说明该先跑 filelib-web 的 build,不是测试不适用。
This commit is contained in:
@@ -82,11 +82,11 @@ REMOTE
|
||||
rsync -az --delete \
|
||||
--exclude node_modules --exclude dist --exclude .env \
|
||||
--exclude admin-web/node_modules --exclude admin-web/build --exclude admin-web/.svelte-kit \
|
||||
--exclude database-admin/node_modules --exclude database-admin/build --exclude database-admin/.svelte-kit \
|
||||
--exclude filelib-web/node_modules --exclude filelib-web/build --exclude filelib-web/.svelte-kit \
|
||||
-e "ssh ${SSH_OPTS[*]}" \
|
||||
"$REPO_ROOT/hub/" "$DEPLOY_USER@$HOST:$HUB_DIR/"
|
||||
|
||||
echo "[fleet] npm ci + build (tsc + admin-web & database-admin SPAs)"
|
||||
echo "[fleet] npm ci + build (tsc + admin-web & filelib-web SPAs)"
|
||||
ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" bash -s <<REMOTE
|
||||
set -euo pipefail
|
||||
flock /var/lock/cph-hub-release-publish bash -c '
|
||||
@@ -98,11 +98,11 @@ flock /var/lock/cph-hub-release-publish bash -c '
|
||||
cd "$HUB_DIR"
|
||||
PUPPETEER_SKIP_DOWNLOAD=1 npm ci
|
||||
npm ci --prefix admin-web
|
||||
npm ci --prefix database-admin
|
||||
npm ci --prefix filelib-web
|
||||
npm run audit:production
|
||||
npm run build
|
||||
test -f admin-web/build/index.html
|
||||
test -f database-admin/build/index.html
|
||||
test -f filelib-web/build/index.html
|
||||
touch "$RELEASE_DIR/.complete"
|
||||
'
|
||||
REMOTE
|
||||
|
||||
@@ -61,10 +61,10 @@ if [ "$release_ready" = false ]; then
|
||||
"$REPO_ROOT/hub/" "$DEPLOY_USER@$HOST:$HUB_DIR/"
|
||||
|
||||
# 2. Install deps (hub + both SPAs), audit hub prod, build tsc + SPAs, mark complete.
|
||||
# `npm run build` → tsc then admin:build + database:build → admin-web/build and
|
||||
# database-admin/build for registerStaticSpa / registerDatabaseSpa.
|
||||
# `npm run build` → tsc then admin:build + filelib:build → admin-web/build and
|
||||
# filelib-web/build for registerStaticSpa / registerDatabaseSpa.
|
||||
ssh "${SSH_OPTS[@]}" "$DEPLOY_USER@$HOST" \
|
||||
"cd '$HUB_DIR' && PUPPETEER_SKIP_DOWNLOAD=1 npm ci && npm ci --prefix admin-web && npm ci --prefix database-admin && npm run audit:production && npm run build && touch '$RELEASE_DIR/.complete'"
|
||||
"cd '$HUB_DIR' && PUPPETEER_SKIP_DOWNLOAD=1 npm ci && npm ci --prefix admin-web && npm ci --prefix filelib-web && npm run audit:production && npm run build && touch '$RELEASE_DIR/.complete'"
|
||||
fi
|
||||
|
||||
# 3. Ensure the service is installed (idempotent), then restart.
|
||||
|
||||
@@ -17,16 +17,16 @@ export function isSiloHttpRateLimitExempt(url: string): boolean {
|
||||
if (path === "/_app" || path.startsWith("/_app/")) return true;
|
||||
if (path === "/favicon.ico" || path === "/favicon.svg" || path === "/robots.txt") return true;
|
||||
|
||||
// database-admin SvelteKit build output, served under /database (base path;
|
||||
// see database/static.ts).
|
||||
if (path === "/database/_app" || path.startsWith("/database/_app/")) return true;
|
||||
if (path === "/database/favicon.svg" || path === "/database/robots.txt") return true;
|
||||
// filelib-web SvelteKit build output. appDir 改名为 `_filelib` 以避开根 /_app/*
|
||||
// (见 database/static.ts);同一份产物服务 /app 与 /database 两个前缀。
|
||||
if (path === "/_filelib" || path.startsWith("/_filelib/")) return true;
|
||||
|
||||
// SPA index shells for client-side routes (not APIs). The /database/* shell is
|
||||
// blanket-exempt like /admin/* since client routes are unknowable up front;
|
||||
// this also covers the once-per-load /database/config bootstrap.
|
||||
// SPA index shells for client-side routes (not APIs). /admin/*、/database/* 与
|
||||
// /app/* 整体豁免 —— 客户端路由无法预先枚举;这也覆盖了每次加载一次的
|
||||
// /database/config bootstrap。
|
||||
if (path === "/admin" || path.startsWith("/admin/")) return true;
|
||||
if (path === "/database" || path.startsWith("/database/")) return true;
|
||||
if (path === "/app" || path.startsWith("/app/")) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,17 @@ describe("isSiloHttpRateLimitExempt", () => {
|
||||
expect(isSiloHttpRateLimitExempt("/admin/org/para-26071100/members")).toBe(true);
|
||||
});
|
||||
|
||||
it("exempts the filelib-web SPA assets and both of its mount prefixes", () => {
|
||||
// appDir 改名为 `_filelib`(见 src/database/static.ts):同一份产物服务
|
||||
// 老师端 /app 与管理后台 /database。
|
||||
expect(isSiloHttpRateLimitExempt("/_filelib/version.json")).toBe(true);
|
||||
expect(isSiloHttpRateLimitExempt("/_filelib/immutable/chunks/foo.js?v=1")).toBe(true);
|
||||
expect(isSiloHttpRateLimitExempt("/app")).toBe(true);
|
||||
expect(isSiloHttpRateLimitExempt("/app/dev-login-teacher")).toBe(true);
|
||||
expect(isSiloHttpRateLimitExempt("/database")).toBe(true);
|
||||
expect(isSiloHttpRateLimitExempt("/database/dashboard/users")).toBe(true);
|
||||
});
|
||||
|
||||
it("still rate-limits APIs and auth", () => {
|
||||
expect(isSiloHttpRateLimitExempt("/api/me")).toBe(false);
|
||||
expect(isSiloHttpRateLimitExempt("/api/org/x/members")).toBe(false);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 两套 SPA 静态托管共存于同一个 Fastify 实例。
|
||||
*
|
||||
* 这是 ADR-0029 的承重约束的回归测试:filelib-web 的 appDir 若用 SvelteKit 默认的
|
||||
* `_app`,就会与 admin-web 在根上注册的 `/_app/*` 撞成重复路由,Fastify 启动即抛错。
|
||||
* 改名为 `_filelib` 后两者共存 —— 这里把两个注册函数挂到同一实例上验证。
|
||||
*/
|
||||
import Fastify from "fastify";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { registerStaticSpa } from "../../src/admin/static.js";
|
||||
import { registerDatabaseSpa } from "../../src/database/static.js";
|
||||
|
||||
describe("SPA static route registration", () => {
|
||||
it("admin-web and filelib-web shells coexist without duplicate routes", async () => {
|
||||
const app = Fastify({ logger: false });
|
||||
await registerStaticSpa(app);
|
||||
await registerDatabaseSpa(app);
|
||||
await expect(app.ready()).resolves.toBeDefined();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("serves the same index.html at /app and /database, and assets under /_filelib", async () => {
|
||||
const app = Fastify({ logger: false });
|
||||
await registerDatabaseSpa(app);
|
||||
await app.ready();
|
||||
|
||||
const appShell = await app.inject({ method: "GET", url: "/app" });
|
||||
const dbShell = await app.inject({ method: "GET", url: "/database/dashboard/users" });
|
||||
|
||||
// 前置:构建产物必须存在。缺失时 registerDatabaseSpa 只 warn 不注册路由,
|
||||
// 断言会全部落到 404 —— 那说明该先跑 `npm run build --prefix filelib-web`,
|
||||
// 不是测试不适用,所以这里不跳过而是直接失败。
|
||||
expect(appShell.statusCode).toBe(200);
|
||||
expect(dbShell.statusCode).toBe(200);
|
||||
// 同一份字节:SPA 回退不读请求(ADR-0029)。
|
||||
expect(dbShell.body).toBe(appShell.body);
|
||||
expect(appShell.headers["content-type"]).toContain("text/html");
|
||||
// 资源用绝对路径引用(paths.relative=false),否则深层 URL 下会解析错。
|
||||
expect(appShell.body).toContain("/_filelib/");
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user