v0.5.112: keep skill demos at 16:9 and harden web refresh deploy triggers.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+76
-17
@@ -3,9 +3,9 @@
|
||||
Tiers (see AGENTS.md / Gitea Actions workflows):
|
||||
daily — OpenDota stats, leaderboards, matches, pro matches, streamers,
|
||||
streamer live probe; patch check
|
||||
(live badge still trails reality by up to a day — truly real-time
|
||||
would need a dedicated higher-frequency workflow, e.g. 15-min)
|
||||
weekly — STRATZ meta, hero items, items_meta, item_fears
|
||||
(live badge is served by /api/live-status on visit; daily probe is
|
||||
only a data.json fallback until the edge API returns)
|
||||
weekly — STRATZ meta, hero items, items_meta, item_counter_stats, item_fears
|
||||
patch — patch list check; on has_new fetch details + version-linked scripts
|
||||
all — weekly then daily (patch check included in daily/patch)
|
||||
|
||||
@@ -29,9 +29,11 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(ROOT.parent))
|
||||
REPO_ROOT = ROOT.parent
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from shared.http_utils import write_json_atomic
|
||||
from shared.paths import HEROES_JSON, RELATIONS_JSON, WEB_FRONTEND
|
||||
|
||||
# Paths whose content change should trigger deploy / OSS.
|
||||
DATA_WATCH = [
|
||||
@@ -50,6 +52,21 @@ DATA_WATCH = [
|
||||
ROOT / "data" / "item_shop.json",
|
||||
ROOT / "data" / "item_counter_stats.json",
|
||||
]
|
||||
# Static site inputs that are not rewritten by fetch tiers but still ship in
|
||||
# data.json / dist (frontend + qualitative relations + hero table / grid).
|
||||
INPUT_WATCH = [
|
||||
RELATIONS_JSON,
|
||||
HEROES_JSON,
|
||||
ROOT / "data" / "hero_grid_order.json",
|
||||
WEB_FRONTEND / "index.html",
|
||||
WEB_FRONTEND / "app.js",
|
||||
WEB_FRONTEND / "style.css",
|
||||
WEB_FRONTEND / "router.js",
|
||||
WEB_FRONTEND / "mobile-gate.js",
|
||||
WEB_FRONTEND / "config.js",
|
||||
WEB_FRONTEND / "_headers",
|
||||
]
|
||||
FRONTEND_FUNCTIONS_DIR = WEB_FRONTEND / "functions"
|
||||
ASSET_DIRS = [
|
||||
ROOT / "assets" / "item_icons",
|
||||
ROOT / "assets" / "ability_icons",
|
||||
@@ -67,12 +84,24 @@ VOLATILE_JSON_KEYS = frozenset(
|
||||
"live_probed_at",
|
||||
"profile_fetched_at",
|
||||
"updated_at",
|
||||
# Live badge is owned by /api/live-status; flipping is_live alone must
|
||||
# not trigger a full static redeploy.
|
||||
"is_live",
|
||||
}
|
||||
)
|
||||
RUN_RESULTS: list[dict] = []
|
||||
PATCH_RESULT: dict = {}
|
||||
|
||||
|
||||
def _watch_key(path: Path) -> str:
|
||||
"""Stable snapshot key relative to the repo root when possible."""
|
||||
resolved = path.resolve()
|
||||
try:
|
||||
return resolved.relative_to(REPO_ROOT.resolve()).as_posix()
|
||||
except ValueError:
|
||||
return resolved.as_posix()
|
||||
|
||||
|
||||
def _file_digest(path: Path) -> str | None:
|
||||
if not path.is_file():
|
||||
return None
|
||||
@@ -131,27 +160,39 @@ def _dir_digest(path: Path) -> str | None:
|
||||
def snapshot() -> dict[str, str | None]:
|
||||
out: dict[str, str | None] = {}
|
||||
for p in DATA_WATCH:
|
||||
out[str(p.relative_to(ROOT))] = _semantic_file_digest(p)
|
||||
out[_watch_key(p)] = _semantic_file_digest(p)
|
||||
for p in INPUT_WATCH:
|
||||
# Frontend/source files use byte digests; JSON inputs use semantic hash.
|
||||
if p.suffix.lower() == ".json":
|
||||
out[_watch_key(p)] = _semantic_file_digest(p)
|
||||
else:
|
||||
out[_watch_key(p)] = _file_digest(p)
|
||||
out[_watch_key(FRONTEND_FUNCTIONS_DIR) + "/"] = _dir_digest(FRONTEND_FUNCTIONS_DIR)
|
||||
for d in ASSET_DIRS:
|
||||
out[str(d.relative_to(ROOT)) + "/"] = _dir_digest(d)
|
||||
out[_watch_key(d) + "/"] = _dir_digest(d)
|
||||
return out
|
||||
|
||||
|
||||
def diff_snapshots(before: dict[str, str | None], after: dict[str, str | None]) -> tuple[bool, bool]:
|
||||
"""Return (data_changed, assets_changed)."""
|
||||
"""Return (data_changed, assets_changed).
|
||||
|
||||
``data_changed`` covers generated JSON, frontend/source inputs, and Pages
|
||||
Functions. Only ``web/assets/...`` directory digests count as assets
|
||||
(OSS upload).
|
||||
"""
|
||||
data_changed = False
|
||||
assets_changed = False
|
||||
asset_keys = {_watch_key(d) + "/" for d in ASSET_DIRS}
|
||||
keys = set(before) | set(after)
|
||||
for k in keys:
|
||||
if before.get(k) == after.get(k):
|
||||
continue
|
||||
if k.endswith("/"):
|
||||
if k in asset_keys:
|
||||
assets_changed = True
|
||||
else:
|
||||
data_changed = True
|
||||
return data_changed, assets_changed
|
||||
|
||||
|
||||
def run_script(script: str, *args: str, dry_run: bool = False, soft_fail: bool = False) -> bool:
|
||||
cmd = [sys.executable, str(ROOT / script), *args]
|
||||
print(f"+ {' '.join(cmd)}", flush=True)
|
||||
@@ -234,9 +275,9 @@ def run_daily(*, dry_run: bool = False) -> bool:
|
||||
run_script("fetch_hero_matches.py", "--source", "league", dry_run=dry_run)
|
||||
run_script("fetch_pro_matches.py", "--include-pubs", dry_run=dry_run)
|
||||
# Soft-fail Douyin enrichment (script itself exits 0; keep previous values on miss).
|
||||
run_script("fetch_streamers.py", dry_run=dry_run)
|
||||
# Soft-fail live probe (exits 0; probe failures keep previous is_live).
|
||||
run_script("fetch_streamer_live.py", dry_run=dry_run)
|
||||
run_script("fetch_streamers.py", dry_run=dry_run, soft_fail=True)
|
||||
# Soft-fail live probe (exits 0; probe failures clear is_live / live_probed_at).
|
||||
run_script("fetch_streamer_live.py", dry_run=dry_run, soft_fail=True)
|
||||
check = patch_check()
|
||||
if check.get("has_new"):
|
||||
print(
|
||||
@@ -323,11 +364,16 @@ def _stale_metrics() -> dict:
|
||||
return metrics
|
||||
|
||||
|
||||
def _write_summary(summary: dict) -> None:
|
||||
def _finalize_summary(summary: dict) -> dict:
|
||||
summary["steps"] = list(RUN_RESULTS)
|
||||
summary["patch_check"] = dict(PATCH_RESULT)
|
||||
summary["health"] = _stale_metrics()
|
||||
summary["completed_at"] = datetime.now(timezone.utc).isoformat()
|
||||
return summary
|
||||
|
||||
|
||||
def _write_summary(summary: dict) -> None:
|
||||
_finalize_summary(summary)
|
||||
write_json_atomic(SUMMARY_PATH, summary)
|
||||
print("REFRESH_SUMMARY " + json.dumps(summary, ensure_ascii=False), flush=True)
|
||||
|
||||
@@ -441,7 +487,9 @@ def main() -> None:
|
||||
_write_summary(summary)
|
||||
return
|
||||
try:
|
||||
cache_enabled = os.environ.get("REFRESH_CACHE_ENABLED") == "1"
|
||||
cache_enabled = (
|
||||
os.environ.get("REFRESH_CACHE_ENABLED") == "1" and not args.dry_run
|
||||
)
|
||||
if cache_enabled:
|
||||
run_script("refresh_cache.py", "restore")
|
||||
before = snapshot() if not args.dry_run else {}
|
||||
@@ -455,7 +503,10 @@ def main() -> None:
|
||||
if args.dry_run:
|
||||
summary["ok"] = True
|
||||
summary["dry_run"] = True
|
||||
print("dry-run complete (no deploy/oss)", flush=True)
|
||||
print(
|
||||
"dry-run complete (no cache restore/save, deploy, or oss)",
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
|
||||
after = snapshot()
|
||||
@@ -484,7 +535,7 @@ def main() -> None:
|
||||
if assets_changed and not args.skip_oss:
|
||||
run_script("_oss_static_assets.py", "upload")
|
||||
elif assets_changed:
|
||||
print("assets changed but deploy is disabled; skipping OSS upload", flush=True)
|
||||
print("assets changed but --skip-oss set; skipping OSS upload", flush=True)
|
||||
|
||||
should_deploy = args.force_deploy or data_changed or assets_changed
|
||||
if should_deploy and not args.skip_deploy:
|
||||
@@ -502,7 +553,15 @@ def main() -> None:
|
||||
raise
|
||||
finally:
|
||||
summary["duration_s"] = round(time.monotonic() - started, 3)
|
||||
_write_summary(summary)
|
||||
if args.dry_run:
|
||||
# Print for humans; do not persist dry-run artifacts.
|
||||
_finalize_summary(summary)
|
||||
print(
|
||||
"REFRESH_SUMMARY " + json.dumps(summary, ensure_ascii=False),
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
_write_summary(summary)
|
||||
release_refresh_lock(lock_handle)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user