v0.5.84: matches origin filter, mobile gate, refresh reliability.

Ship Web refresh cache/lock, mobile demand gate, matches 职业/国服 filter, and related site updates through 0.5.84.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-29 18:31:55 +08:00
co-authored by Cursor
parent 7681fdb069
commit b01552ee6e
50 changed files with 3406 additions and 487 deletions
+85 -6
View File
@@ -34,7 +34,9 @@ import os
import shutil
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
@@ -239,10 +241,16 @@ def wrangler_deploy(
) -> None:
"""Upload dist via `wrangler pages deploy` (direct upload, non-interactive).
The whole dist directory is uploaded, so dist/functions/ (Pages Functions,
e.g. /api/live-status) is included automatically — Pages Functions need
no extra wrangler config or flags for direct-upload projects.
Wrangler looks for ``functions/`` relative to the process cwd (not inside
an absolute asset-directory argument). Run from ``dist`` and deploy ``.``
so ``dist/functions/`` (e.g. /api/live-status) is compiled and uploaded.
"""
functions_dir = dist / "functions"
if not functions_dir.is_dir():
print(
f"warn: {functions_dir} missing — Pages Functions will not deploy "
"(re-run export_relations_site.py)"
)
env = os.environ.copy()
# wrangler reads Global API Key auth from these env vars. They are passed
# to the subprocess only; wrangler does not echo them.
@@ -256,15 +264,76 @@ def wrangler_deploy(
"wrangler@3",
"pages",
"deploy",
str(dist),
".",
"--project-name",
project,
"--commit-dirty",
"--branch",
"main",
]
print(f"uploading {dist} via wrangler ...")
subprocess.run(cmd, cwd=str(ROOT), env=env, check=True)
print(f"uploading {dist} via wrangler (cwd=dist, functions={functions_dir.is_dir()}) ...")
subprocess.run(cmd, cwd=str(dist), env=env, check=True)
def smoke_test_production(
base_url: str,
*,
expected_run_id: str = "",
attempts: int = 6,
delay_s: float = 5.0,
) -> None:
"""Verify the deployed payload and Pages Function after propagation."""
base = base_url.rstrip("/")
marker = expected_run_id or str(int(time.time()))
error = "unknown failure"
for attempt in range(1, attempts + 1):
try:
data_url = f"{base}/data.json?refresh={urllib.parse.quote(marker)}"
req = urllib.request.Request(data_url, headers={"User-Agent": "climperor-smoke"})
with urllib.request.urlopen(req, timeout=30) as resp:
payload = json.loads(resp.read().decode("utf-8"))
if not isinstance(payload, dict) or not payload.get("heroes"):
raise RuntimeError("data.json has no heroes")
actual_run_id = str((payload.get("meta") or {}).get("refresh_run_id") or "")
if expected_run_id and actual_run_id != expected_run_id:
raise RuntimeError(
f"stale deployment run_id={actual_run_id!r}, expected={expected_run_id!r}"
)
live_url = f"{base}/api/live-status?refresh={urllib.parse.quote(marker)}"
live_req = urllib.request.Request(
live_url, headers={"User-Agent": "climperor-smoke"}
)
with urllib.request.urlopen(live_req, timeout=30) as resp:
content_type = resp.headers.get_content_type()
live_cache = (resp.headers.get("X-Live-Cache") or "").lower()
live_payload = json.loads(resp.read().decode("utf-8"))
if content_type != "application/json" or not isinstance(live_payload, dict):
raise RuntimeError(
f"/api/live-status is not JSON (content-type={content_type})"
)
if live_cache in ("error", "stale-override"):
print(
f"warn: live-status Function is routed but probe state is {live_cache}"
)
print(
f"production smoke passed: {base} "
f"(heroes={len(payload['heroes'])}, run_id={actual_run_id or '-'}, "
f"live_cache={live_cache or '?'})"
)
return
except (
OSError,
ValueError,
RuntimeError,
json.JSONDecodeError,
urllib.error.URLError,
) as exc:
error = str(exc)
print(f"smoke attempt {attempt}/{attempts} failed: {error}")
if attempt < attempts:
time.sleep(delay_s)
raise SystemExit(f"deployment smoke failed after {attempts} attempts: {error}")
def ensure_cname(email: str, api_key: str, domain: str, project: str) -> None:
@@ -358,6 +427,11 @@ def main() -> None:
help="passed to export as --static-asset-base (default: same as ability-video-base "
"or STATIC_ASSET_BASE env)",
)
ap.add_argument(
"--skip-smoke",
action="store_true",
help="skip post-deploy data.json and /api/live-status verification",
)
args = ap.parse_args()
dist = Path(args.dist).resolve()
@@ -379,6 +453,11 @@ def main() -> None:
check_integrity(dist)
wrangler_deploy(dist, args.project_name, email=email, api_key=api_key, account_id=account_id)
bind_domain(email, api_key, account_id, args.project_name, args.domain)
if not args.skip_smoke:
smoke_test_production(
f"https://{args.project_name}.pages.dev",
expected_run_id=os.environ.get("REFRESH_RUN_ID") or "",
)
print()
print(f"deployment url : https://{args.project_name}.pages.dev")