aboutsummaryrefslogtreecommitdiff
path: root/scripts/test_scripts/oplog-update-build-ids.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/test_scripts/oplog-update-build-ids.py')
-rw-r--r--scripts/test_scripts/oplog-update-build-ids.py151
1 files changed, 151 insertions, 0 deletions
diff --git a/scripts/test_scripts/oplog-update-build-ids.py b/scripts/test_scripts/oplog-update-build-ids.py
new file mode 100644
index 000000000..67e128c8e
--- /dev/null
+++ b/scripts/test_scripts/oplog-update-build-ids.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+"""Update oplog-import-export-build-ids.json with build IDs at the highest common changelist across all buckets."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import platform
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+_PLATFORM = "windows" if sys.platform == "win32" else "macosx" if sys.platform == "darwin" else "linux"
+_ARCH = "x64" if sys.platform == "win32" else platform.machine().lower()
+_EXE_SUFFIX = ".exe" if sys.platform == "win32" else ""
+_DEFAULT_ZEN = Path(f"build/{_PLATFORM}/{_ARCH}/release/zen{_EXE_SUFFIX}")
+
+
+def _cache_dir() -> Path:
+ if sys.platform == "win32":
+ base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
+ return base / "Temp" / "zen"
+ elif sys.platform == "darwin":
+ return Path.home() / "Library" / "Caches" / "zen"
+ else:
+ base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
+ return base / "zen"
+
+
+_OUTPUT_PATH = _cache_dir() / "oplog-import-export-build-ids.json"
+
+# Maps build name -> Jupiter bucket
+_BUILDS: list[tuple[str, str]] = [
+ ("XB1Client", "fortnitegame.oplog.fortnite-main.xb1client"),
+ ("WindowsClient", "fortnitegame.oplog.fortnite-main.windowsclient"),
+ ("SwitchClient", "fortnitegame.oplog.fortnite-main.switchclient"),
+ ("XSXClient", "fortnitegame.oplog.fortnite-main.xsxclient"),
+ ("Switch2Client", "fortnitegame.oplog.fortnite-main.switch2client"),
+ ("PS4Client", "fortnitegame.oplog.fortnite-main.ps4client"),
+ ("PS5Client", "fortnitegame.oplog.fortnite-main.ps5client"),
+ ("LinuxServer", "fortnitegame.oplog.fortnite-main.linuxserver"),
+ ("IOSClient", "fortnitegame.oplog.fortnite-main.iosclient"),
+ ("Android_ASTCClient", "fortnitegame.oplog.fortnite-main.android_astcclient"),
+]
+
+
+def list_builds_for_bucket(zen: str, host: str, namespace: str, bucket: str) -> list[dict]:
+ """Run zen builds list for a single bucket and return the results array."""
+ with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
+ result_path = Path(tmp.name)
+
+ cmd = [
+ zen, "builds", "list",
+ "--namespace", namespace,
+ "--bucket", bucket,
+ "--host", host,
+ "--result-path", str(result_path),
+ ]
+
+ try:
+ subprocess.run(cmd, check=True, capture_output=True)
+ except FileNotFoundError:
+ sys.exit(f"error: zen binary not found: {zen}")
+ except subprocess.CalledProcessError as e:
+ sys.exit(
+ f"error: zen builds list failed for bucket '{bucket}' with exit code {e.returncode}\n"
+ f"stderr: {e.stderr.decode(errors='replace')}"
+ )
+
+ with result_path.open() as f:
+ data = json.load(f)
+ result_path.unlink(missing_ok=True)
+
+ return data.get("results", [])
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(
+ description="Refresh oplog-import-export-build-ids.json with build IDs at the highest changelist present in all buckets."
+ )
+ parser.add_argument("--host", default="https://jupiter.devtools.epicgames.com", help="Jupiter host URL")
+ parser.add_argument("--zen", default=str(_DEFAULT_ZEN), help="Path to the zen binary")
+ parser.add_argument("--namespace", default="fortnite.oplog", help="Builds storage namespace")
+ args = parser.parse_args()
+
+ # For each bucket, fetch results and build a changelist -> buildId map.
+ # bucket_cl_map[bucket] = { changelist_int: buildId_str, ... }
+ bucket_cl_map: dict[str, dict[int, str]] = {}
+
+ for name, bucket in _BUILDS:
+ print(f"Querying {name} ({bucket}) ...")
+ results = list_builds_for_bucket(args.zen, args.host, args.namespace, bucket)
+ if not results:
+ sys.exit(f"error: no results for bucket '{bucket}' (build '{name}')")
+
+ cl_map: dict[int, str] = {}
+ for entry in results:
+ build_id = entry.get("buildId", "")
+ metadata = entry.get("metadata") or {}
+ cl = metadata.get("changelist")
+ if build_id and cl is not None:
+ # Keep first occurrence (most recent) per changelist
+ if cl not in cl_map:
+ cl_map[int(cl)] = build_id
+
+ if not cl_map:
+ sys.exit(
+ f"error: bucket '{bucket}' (build '{name}') returned {len(results)} entries "
+ "but none had both buildId and changelist in metadata"
+ )
+
+ print(f" {len(cl_map)} distinct changelists, latest CL {max(cl_map)}")
+ bucket_cl_map[bucket] = cl_map
+
+ # Find the highest changelist present in every bucket's result set.
+ common_cls = set(next(iter(bucket_cl_map.values())).keys())
+ for bucket, cl_map in bucket_cl_map.items():
+ common_cls &= set(cl_map.keys())
+
+ if not common_cls:
+ sys.exit(
+ "error: no changelist is present in all buckets.\n"
+ "Per-bucket CL ranges:\n"
+ + "\n".join(
+ f" {name} ({bucket}): {min(bucket_cl_map[bucket])} – {max(bucket_cl_map[bucket])}"
+ for name, bucket in _BUILDS
+ )
+ )
+
+ best_cl = max(common_cls)
+ print(f"\nHighest common changelist: {best_cl}")
+
+ build_ids: dict[str, dict[str, str]] = {}
+ for name, bucket in _BUILDS:
+ build_id = bucket_cl_map[bucket][best_cl]
+ build_ids[name] = {"bucket": bucket, "buildId": build_id}
+ print(f" {name}: {build_id}")
+
+ output = {"namespace": args.namespace, "builds": build_ids}
+ _OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
+ with _OUTPUT_PATH.open("w") as f:
+ json.dump(output, f, indent=2)
+ f.write("\n")
+
+ print(f"\nWrote {_OUTPUT_PATH}")
+
+
+if __name__ == "__main__":
+ main()