fix: prevent white screens during production rebuilds

This commit is contained in:
134 2026-07-15 11:26:41 +08:00
parent abc6a7266a
commit 3c1b84ccd0
5 changed files with 86 additions and 11 deletions

View File

@ -29,6 +29,7 @@ function resolvePascalEditorFrameSources(isDev: boolean) {
}
const nextConfig: NextConfig = {
distDir: process.env.NEXT_DIST_DIR?.trim() || ".next",
poweredByHeader: false,
allowedDevOrigins: ["127.0.0.1"],
// 禁用 cacheComponents 以支持 dynamic = "force-dynamic"

View File

@ -20,7 +20,7 @@
"prebuild": "node scripts/security/auto-enable-perf-guard.mjs",
"build": "next build",
"build:with-migrate": "pnpm db:migrate && next build",
"postbuild": "find .next/static -type f -name '*.map' -delete 2>/dev/null || true",
"postbuild": "find \"${NEXT_DIST_DIR:-.next}/static\" -type f -name '*.map' -delete 2>/dev/null || true",
"start": "next start",
"lint": "pnpm exec ultracite check",
"check:logs": "node scripts/check-log-hygiene.mjs && tsx scripts/check-log-redaction.ts",

View File

@ -10,6 +10,7 @@ MAIN_PM2_NAME="${MAIN_PM2_NAME:-ai-chatbot}"
MAIN_INTERNAL_URL="${MAIN_INTERNAL_URL:-http://127.0.0.1:3125}"
MAIN_NGINX_UPSTREAM="${MAIN_NGINX_UPSTREAM:-127.0.0.1:3125}"
BUILD_MAIN_APP="${BUILD_MAIN_APP:-true}"
MAIN_STATIC_RETENTION_DAYS="${MAIN_STATIC_RETENTION_DAYS:-7}"
PASCAL_INSTALL_DIR="${PASCAL_INSTALL_DIR:-/opt/pascal-editor}"
PASCAL_SOURCE_DIR="${PASCAL_SOURCE_DIR:-$MAIN_APP_DIR/services/pascal-editor}"
@ -137,9 +138,20 @@ install_rsync() {
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq rsync
}
restart_main_app() {
set -a
# shellcheck disable=SC1090
source "$MAIN_ENV_FILE"
set +a
pm2 restart "$MAIN_PM2_NAME" --update-env
}
configure_main_app() {
local secret="$1"
local next_env_backup=""
local current_build="$MAIN_APP_DIR/.next"
local staged_build="$MAIN_APP_DIR/.next-build"
local previous_build="$MAIN_APP_DIR/.next-previous"
mkdir -p "$PASCAL_BACKUP_DIR"
if [ -f "$MAIN_ENV_FILE" ]; then
cp "$MAIN_ENV_FILE" "$PASCAL_BACKUP_DIR/main-env-$(date +%Y%m%d%H%M%S).bak"
@ -155,7 +167,11 @@ configure_main_app() {
require_command pnpm
require_command pm2
[[ "$MAIN_STATIC_RETENTION_DAYS" =~ ^[0-9]+$ ]] ||
die "MAIN_STATIC_RETENTION_DAYS must be a non-negative integer"
log "Building main application in $MAIN_APP_DIR"
[ ! -e "$staged_build" ] || rm -r "$staged_build"
[ ! -e "$previous_build" ] || rm -r "$previous_build"
if [ -f "$MAIN_APP_DIR/next-env.d.ts" ]; then
next_env_backup="$(mktemp)"
cp "$MAIN_APP_DIR/next-env.d.ts" "$next_env_backup"
@ -169,21 +185,56 @@ configure_main_app() {
source "$MAIN_ENV_FILE"
set +a
DISABLE_PERF_GUARD_AUTO_INSTALL=1 pnpm install --frozen-lockfile --prod=false
DISABLE_PERF_GUARD_AUTO_INSTALL=1 pnpm build
NEXT_DIST_DIR=.next-build DISABLE_PERF_GUARD_AUTO_INSTALL=1 pnpm build
) || build_status=$?
set -e
if [ -n "$next_env_backup" ]; then
cp "$next_env_backup" "$MAIN_APP_DIR/next-env.d.ts"
rm -f "$next_env_backup"
fi
[ "$build_status" -eq 0 ] || die "Main application build failed"
(
set -a
# shellcheck disable=SC1090
source "$MAIN_ENV_FILE"
set +a
pm2 restart "$MAIN_PM2_NAME" --update-env
)
[ "$build_status" -eq 0 ] || {
rm -rf "$staged_build"
die "Main application build failed"
}
[ -f "$staged_build/BUILD_ID" ] || die "Main staged build did not produce a BUILD_ID"
if [ -d "$current_build/static" ]; then
log "Retaining static chunks from the active main build"
mkdir -p "$staged_build/static"
rsync -a --ignore-existing "$current_build/static/" "$staged_build/static/"
find "$staged_build/static" -type f -mtime +"$MAIN_STATIC_RETENTION_DAYS" -delete
find "$staged_build/static" -depth -type d -empty -delete
fi
if [ -e "$current_build" ]; then
mv "$current_build" "$previous_build"
fi
if ! mv "$staged_build" "$current_build"; then
[ ! -e "$previous_build" ] || mv "$previous_build" "$current_build"
die "Could not activate the staged main build"
fi
if ! restart_main_app; then
log "Main restart failed; rolling back the previous build"
rm -rf "$current_build"
[ ! -e "$previous_build" ] || mv "$previous_build" "$current_build"
restart_main_app || true
die "Main application failed to restart"
fi
local health_attempt
for health_attempt in $(seq 1 30); do
if curl -fsS "$MAIN_INTERNAL_URL/ping" >/dev/null 2>&1; then
return
fi
sleep 1
done
log "Main health check failed; rolling back the previous build"
rm -rf "$current_build"
[ ! -e "$previous_build" ] || mv "$previous_build" "$current_build"
restart_main_app || true
die "Main application did not become healthy after deployment"
}
sync_pascal_source() {
@ -444,6 +495,7 @@ main() {
fi
[ "$(id -u)" -eq 0 ] || die "Production deployment must run as root"
install_rsync
configure_main_app "$secret"
install_bun
sync_pascal_source

View File

@ -21,3 +21,23 @@ test("requires an explicit environment switch for local fallbacks", () => {
);
}
});
test("production deploy stages the main build and retains active static chunks", () => {
const nextConfig = readFileSync(
resolve(process.cwd(), "next.config.ts"),
"utf8"
);
const deployScript = readFileSync(
resolve(process.cwd(), "scripts/deploy-pascal-production.sh"),
"utf8"
);
expect(nextConfig).toContain(
'distDir: process.env.NEXT_DIST_DIR?.trim() || ".next"'
);
expect(deployScript).toContain("NEXT_DIST_DIR=.next-build");
expect(deployScript).toContain(
'rsync -a --ignore-existing "$current_build/static/" "$staged_build/static/"'
);
expect(deployScript).toContain('curl -fsS "$MAIN_INTERNAL_URL/ping"');
});

View File

@ -29,7 +29,9 @@
"**/*.tsx",
".next/types/**/*.ts",
"next.config.js",
".next/dev/types/**/*.ts"
".next/dev/types/**/*.ts",
".next-build/types/**/*.ts",
".next-build/dev/types/**/*.ts"
],
"exclude": ["node_modules", "services/pascal-editor"]
}