fix(updates): verify readable frontend assets before reopening traffic

Keep backups private while publishing build directories as 755 and files as 644. Verify the served entry, JS/CSS and service worker before success. Document cache policy for stable widget and runtime entry URLs.
This commit is contained in:
WangShen007 committed 2026-10-24 07:42:34 +00:00
1 parent f3ad539998
commit 6a4127ed24
3 files changed
+60 -2

No files matched your search

+31
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import json
import os
import re
import shutil
import subprocess
import tarfile
@@ -45,6 +46,32 @@ class Deployment:
time.sleep(2)
raise RuntimeError('新版本后端未通过健康检查。')
def verify_frontend(self) -> None:
web = self.root / 'deploy/local/.state/dashboard'
with urllib.request.urlopen('http://127.0.0.1/index.html', timeout=10) as response:
html = response.read()
if html != (web / 'index.html').read_bytes():
raise RuntimeError('前端入口未切换到新构建。')
assets = re.findall(rb'<(?:script|link)\b[^>]*(?:src|href)="(/assets/[^" ]+)"', html)
if not assets:
raise RuntimeError('前端入口缺少构建资源。')
for asset in assets:
with urllib.request.urlopen('http://127.0.0.1' + asset.decode(), timeout=10) as response:
content_type = response.headers.get('Content-Type', '').split(';')[0]
expected = {'text/css'} if asset.endswith(b'.css') else {'text/javascript', 'application/javascript'}
if content_type not in expected:
raise RuntimeError('前端静态资源响应异常:' + asset.decode())
with urllib.request.urlopen('http://127.0.0.1/sw.js', timeout=10) as response:
if response.headers.get('Content-Type', '').split(';')[0] not in {'text/javascript', 'application/javascript'}:
raise RuntimeError('离线脚本响应异常。')
@staticmethod
def publish_permissions(root: Path) -> None:
# systemd uses umask 0077 for private backups, but Caddy must read public builds.
root.chmod(0o755)
for path in root.rglob('*'):
path.chmod(0o755 if path.is_dir() else 0o644)
def prepare(self, target: str) -> Path:
stage = self.state_dir / 'releases' / f'{target}-{int(time.time())}'
stage.mkdir(parents=True)
@@ -85,6 +112,8 @@ class Deployment:
shutil.copy2(live_web / 'config.js', stage / 'dashboard/config.js')
if (live_web / 'local-demo.html').exists():
shutil.copy2(live_web / 'local-demo.html', stage / 'dashboard/local-demo.html')
self.publish_permissions(stage / 'dashboard')
self.publish_permissions(stage / 'assets')
shutil.rmtree(front / 'node_modules')
lock = Path('deploy/local/requirements.lock.txt')
self.new_venv = None
@@ -159,6 +188,7 @@ class Deployment:
for name in ('assets', 'dashboard'):
(state / name).rename(backup / name)
(stage / name).rename(state / name)
self.verify_frontend()
def restore(self, backup: Path, *, boot: bool = False) -> None:
if not boot:
@@ -204,6 +234,7 @@ class Deployment:
if not boot:
self.services('start')
self.healthy()
self.verify_frontend()
def recover(self, *, boot: bool = False) -> None:
if not (self.state_dir / 'maintenance').exists():