Files
m2pack-secure/scripts/self_hosted_runtime_ci.py
server 9a2f1b9479
Some checks failed
ci / headless-e2e (push) Has been cancelled
runtime-self-hosted / runtime-ci (push) Has been cancelled
Add self-hosted runtime CI orchestration
2026-04-14 18:45:30 +02:00

210 lines
6.4 KiB
Python
Executable File

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_SMOKE_PACKS = [
"root",
"patch1",
"patch2",
"season3_eu",
"metin2_patch_snow",
"metin2_patch_snow_dungeon",
"metin2_patch_etc_costume1",
"metin2_patch_pet1",
"metin2_patch_pet2",
"metin2_patch_ramadan_costume",
"metin2_patch_flame",
"metin2_patch_flame_dungeon",
"locale",
"uiscript",
"uiloading",
"ETC",
"item",
"effect",
"icon",
"property",
]
def env_path(name: str) -> Path | None:
value = os.environ.get(name, "").strip()
return Path(value).expanduser().resolve() if value else None
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run the Linux headless E2E, runtime gate, and optional Wine .m2p smoke on a self-hosted runner."
)
parser.add_argument(
"--runtime-root",
type=Path,
default=env_path("M2_RUNTIME_ROOT"),
help="Client runtime root containing assets/, config/, and pack/. Defaults to $M2_RUNTIME_ROOT.",
)
parser.add_argument(
"--client-repo",
type=Path,
default=env_path("M2_CLIENT_REPO"),
help="Path to m2dev-client-src. Defaults to $M2_CLIENT_REPO.",
)
parser.add_argument(
"--master-key",
type=Path,
default=env_path("M2_MASTER_KEY_PATH"),
help="Path to runtime master key file. Defaults to $M2_MASTER_KEY_PATH.",
)
parser.add_argument(
"--key-id",
type=str,
default=os.environ.get("M2_KEY_ID", "1"),
help="Runtime key id for Wine smoke. Defaults to $M2_KEY_ID or 1.",
)
parser.add_argument(
"--timeout",
type=int,
default=int(os.environ.get("M2_TIMEOUT", "20")),
help="Wine smoke timeout in seconds. Defaults to $M2_TIMEOUT or 20.",
)
parser.add_argument(
"--skip-headless-e2e",
action="store_true",
help="Skip scripts/headless_e2e.py.",
)
parser.add_argument(
"--skip-runtime-gate",
action="store_true",
help="Skip scripts/validate_runtime_gate.py.",
)
parser.add_argument(
"--skip-runtime-smoke",
action="store_true",
help="Skip scripts/runtime_smoke_wine.py.",
)
parser.add_argument(
"--smoke-pack",
dest="smoke_packs",
action="append",
default=[],
help="Pack basename for Wine smoke. Repeatable. Defaults to a known startup-safe set or $M2_RUNTIME_SMOKE_PACKS.",
)
parser.add_argument(
"--json",
action="store_true",
help="Emit machine-readable JSON output.",
)
return parser.parse_args()
def resolve_smoke_packs(cli_values: list[str]) -> list[str]:
if cli_values:
return cli_values
env_value = os.environ.get("M2_RUNTIME_SMOKE_PACKS", "").strip()
if env_value:
return [part.strip() for part in env_value.split(",") if part.strip()]
return list(DEFAULT_SMOKE_PACKS)
def run_json(cmd: list[str]) -> dict:
completed = subprocess.run(
cmd,
cwd=REPO_ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if completed.returncode != 0:
detail = completed.stderr.strip() or completed.stdout.strip() or f"exit code {completed.returncode}"
raise RuntimeError(f"command failed: {' '.join(cmd)}\n{detail}")
stdout = completed.stdout.strip()
return json.loads(stdout) if stdout else {"ok": True}
def main() -> int:
args = parse_args()
smoke_packs = resolve_smoke_packs(args.smoke_packs)
summary: dict[str, object] = {
"ok": True,
"runtime_root": str(args.runtime_root) if args.runtime_root else None,
"client_repo": str(args.client_repo) if args.client_repo else None,
"smoke_packs": smoke_packs,
"steps": {},
}
try:
if not args.skip_headless_e2e:
summary["steps"]["headless_e2e"] = run_json([sys.executable, "scripts/headless_e2e.py"])
else:
summary["steps"]["headless_e2e"] = {"ok": True, "skipped": True}
if not args.skip_runtime_gate:
if not args.runtime_root:
raise ValueError("runtime root is required for runtime gate")
summary["steps"]["runtime_gate"] = run_json(
[
sys.executable,
"scripts/validate_runtime_gate.py",
"--runtime-root",
str(args.runtime_root),
"--json",
]
)
else:
summary["steps"]["runtime_gate"] = {"ok": True, "skipped": True}
if not args.skip_runtime_smoke:
if not args.runtime_root:
raise ValueError("runtime root is required for runtime smoke")
if not args.client_repo:
raise ValueError("client repo is required for runtime smoke")
if not args.master_key:
raise ValueError("master key is required for runtime smoke")
cmd = [
sys.executable,
"scripts/runtime_smoke_wine.py",
"--runtime-root",
str(args.runtime_root),
"--client-repo",
str(args.client_repo),
"--master-key",
str(args.master_key),
"--key-id",
args.key_id,
"--timeout",
str(args.timeout),
"--json",
]
for pack in smoke_packs:
cmd.extend(["--pack", pack])
summary["steps"]["runtime_smoke"] = run_json(cmd)
else:
summary["steps"]["runtime_smoke"] = {"ok": True, "skipped": True}
summary["ok"] = all(bool(step.get("ok", False)) for step in summary["steps"].values())
except Exception as exc:
summary["ok"] = False
summary["error"] = str(exc)
if args.json:
print(json.dumps(summary, indent=2))
else:
print(f"ok={summary['ok']}")
for name, result in summary["steps"].items():
print(f"{name}: ok={result.get('ok')} skipped={result.get('skipped', False)}")
if "error" in summary:
print(f"error: {summary['error']}")
return 0 if summary["ok"] else 1
if __name__ == "__main__":
sys.exit(main())