deploy: 前端部署后自动清理孤儿 assets + 冒烟脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-06-01 12:29:57 +08:00
parent b76295aaec
commit 2deb89b49c
3 changed files with 122 additions and 2 deletions
+4 -2
View File
@@ -23,10 +23,12 @@ echo "▶ Uploading to server..."
scp /tmp/nexus-frontend.tar.gz nexus:/tmp/nexus-frontend.tar.gz
echo "✓ Upload complete"
# 4. Extract on server (overwrites old index.html + assets)
# 4. Extract on server + prune orphaned legacy chunks
echo "▶ Deploying on server..."
ssh nexus "cd /www/wwwroot/api.synaglobal.vip/web/app && tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz"
echo "✓ Extracted"
echo "▶ Pruning orphaned assets..."
ssh nexus "python3 /www/wwwroot/api.synaglobal.vip/deploy/prune_frontend_assets.py"
echo "✓ Extracted and pruned"
# 5. Restart backend
echo "▶ Restarting backend..."
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Remove orphaned files under web/app/assets/ not referenced by current Vite build."""
from __future__ import annotations
import re
import sys
from pathlib import Path
IMPORT_RE = re.compile(
r"""(?:from\s*["']|import\s*\(\s*["']|import\s*["'])\./([^"']+\.(?:js|css))"""
)
URL_RE = re.compile(r"""url\(["']?([^"')]+\.(?:woff2?|ttf|eot))["']?\)""")
def collect(app_dir: Path) -> set[Path]:
assets = app_dir / "assets"
index = app_dir / "index.html"
if not index.is_file():
raise SystemExit(f"missing {index}")
keep: set[Path] = set()
queue: list[Path] = []
for m in re.finditer(r"/app/assets/([^\s\"']+)", index.read_text(encoding="utf-8")):
p = assets / m.group(1)
if p.is_file():
queue.append(p)
while queue:
path = queue.pop()
if path in keep or not path.is_file():
continue
keep.add(path)
text = path.read_text(encoding="utf-8", errors="ignore")
for rel in IMPORT_RE.findall(text):
nxt = assets / rel
if nxt.is_file():
queue.append(nxt)
if path.suffix == ".css":
for rel in URL_RE.findall(text):
rel = rel.lstrip("./")
nxt = assets / rel
if nxt.is_file():
keep.add(nxt)
return keep
def main() -> int:
app_dir = Path(sys.argv[1] if len(sys.argv) > 1 else "/www/wwwroot/api.synaglobal.vip/web/app")
assets = app_dir / "assets"
if not assets.is_dir():
raise SystemExit(f"not a directory: {assets}")
keep = collect(app_dir)
all_files = {p for p in assets.rglob("*") if p.is_file()}
orphan = sorted(all_files - keep)
for p in orphan:
p.unlink()
print(f"kept={len(keep)} removed={len(orphan)} total_before={len(all_files)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Post-deploy smoke: login + optional batch endpoint (run on server)."""
import json
import os
import sys
import urllib.error
import urllib.request
BASE = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600")
USER = os.environ.get("NEXUS_TEST_ADMIN_USER", "admin")
PASSWORD = os.environ.get("NEXUS_TEST_ADMIN_PASSWORD", "")
def post(path: str, body: dict, headers: dict | None = None) -> dict:
h = {"Content-Type": "application/json", **(headers or {})}
req = urllib.request.Request(
f"{BASE}{path}",
data=json.dumps(body).encode(),
headers=h,
method="POST",
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.load(resp)
def main() -> int:
if not PASSWORD:
print("SKIP: set NEXUS_TEST_ADMIN_PASSWORD", file=sys.stderr)
return 2
login = post("/api/auth/login", {"username": USER, "password": PASSWORD})
token = login.get("access_token")
exp = login.get("expires_in")
print(f"login ok expires_in={exp}")
if exp != 3600:
print(f"WARN: expected expires_in=3600, got {exp}")
if not token:
print("FAIL: no access_token")
return 1
# Minimal authenticated call
req = urllib.request.Request(
f"{BASE}/api/servers/stats",
headers={"Authorization": f"Bearer {token}"},
)
with urllib.request.urlopen(req, timeout=30) as resp:
stats = json.load(resp)
print(f"servers/stats ok keys={list(stats.keys())[:5]}")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except urllib.error.HTTPError as e:
print(f"HTTP {e.code}: {e.read().decode()[:500]}", file=sys.stderr)
raise SystemExit(1)