deploy: 前端部署后自动清理孤儿 assets + 冒烟脚本
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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..."
|
||||
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user