games-development-ai/tier2/gen-worker/scripts/game_source_archive_sync.py
lili a513ae38fd
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
fix(storage): harden source authorization and committed CAS
Provision the isolated game-source writer path, bind source uploads to
GameSourceArchive manifests, and require every immutable storage field for
pending-to-committed CAS. Reuse the gate-validated runtime manifest snapshot,
normalize remote storage errors, and record the staging object/CAS evidence.
2026-07-29 19:10:09 -07:00

134 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""引擎无关游戏源归档同步 CLI。
默认只构建本地 GameSourceArchive/1 清单。波次 1 上传必须显式传
``--external-single-writer``,并且只返回 ``verified_pending``,不能把对象
marker 当成数据库 committed 状态。
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
from worker.game_source_archive_store import (
ArtifactError,
build_source_archive,
download_source_archive,
fetch_source_archive,
upload_source_archive,
)
def _config() -> dict:
"""读取独立 game_source_minio 配置,禁止复用制品 writer 或 MinIO root。"""
try:
from service import infra_config
section = "game_source_minio"
config = {
"endpoint": os.getenv("GAME_SOURCE_ENDPOINT", infra_config.get(section, "endpoint", "")),
"access_key": os.getenv("GAME_SOURCE_ACCESS_KEY", infra_config.get(section, "access_key", "")),
"secret_key": os.getenv("GAME_SOURCE_SECRET_KEY", infra_config.get(section, "secret_key", "")),
"secure": os.getenv("GAME_SOURCE_SECURE", str(infra_config.get(section, "secure", True))).lower()
in {"1", "true", "yes"},
"artifact_bucket": infra_config.get("game_artifact_minio", "artifact_bucket", "game-artifacts"),
"evidence_bucket": infra_config.get("game_artifact_minio", "evidence_bucket", "game-evidence"),
"source_bucket": os.getenv("GAME_SOURCE_BUCKET", infra_config.get(section, "source_bucket", "")),
}
if not config["endpoint"] or not config["access_key"] or not config["secret_key"]:
raise ArtifactError("专用 game_source_minio 配置未就位,拒绝复用制品 writer 或现有 MinIO root 凭据")
if not config["source_bucket"]:
raise ArtifactError("game-sources 尚未预建或加入专用身份策略,拒绝联网上传")
if config["source_bucket"] in {config["artifact_bucket"], config["evidence_bucket"]}:
raise ArtifactError("source_bucket 不能与 artifact/evidence 桶复用")
return config
except Exception as exc: # noqa: BLE001 - CLI 给出稳定错误
raise ArtifactError(f"读取 MinIO 配置失败:{exc}") from exc
def main() -> int:
parser = argparse.ArgumentParser(description="构建、上传或下载引擎无关游戏源归档")
parser.add_argument("--root", type=Path, help="具体游戏源目录(只作为本次上传输入)")
parser.add_argument("--tenant-id")
parser.add_argument("--game-id")
parser.add_argument("--revision-id")
parser.add_argument("--engine")
parser.add_argument("--runtime-manifest", default="game-package.json",
help="与 GameArtifactManifest 同一输入根中的 GamePackage/1 相对路径")
parser.add_argument("--exclude-path", action="append", default=[], help="不进入源归档的目录内相对路径")
parser.add_argument("--manifest-out", type=Path)
parser.add_argument("--upload", action="store_true", help="显式访问 MinIO 并上传")
parser.add_argument(
"--external-single-writer", action="store_true",
help="确认调用方已持有 tenant/game/revision 外部单写锁",
)
parser.add_argument("--download-key", help="受信 source-manifest 对象 key不接受任意 URL")
parser.add_argument("--expected-manifest-hash", help="数据库登记的 source manifest SHA-256")
parser.add_argument("--target", type=Path, help="下载物化目录")
args = parser.parse_args()
try:
if args.download_key:
if args.upload or args.external_single_writer or not all((
args.target, args.expected_manifest_hash, args.tenant_id,
args.game_id, args.revision_id,
)):
raise ArtifactError("下载模式必须提供 target、期望 hash 和 tenant/game/revision且不能带上传参数")
config = _config()
manifest = fetch_source_archive(
config, key=args.download_key,
expected_manifest_hash=args.expected_manifest_hash,
expected_tenant_id=args.tenant_id,
expected_game_id=args.game_id,
expected_revision_id=args.revision_id,
)
target = download_source_archive(
manifest, config, args.target, expected_manifest_hash=args.expected_manifest_hash,
)
print(json.dumps({
"gameId": manifest["gameId"],
"revisionId": manifest["revisionId"],
"sourceHash": manifest["sourceHash"],
"manifestHash": manifest["manifestHash"],
"target": str(target),
}, ensure_ascii=False))
return 0
if not all((args.root, args.tenant_id, args.game_id, args.revision_id, args.engine)):
raise ArtifactError("构建/上传模式缺少 root、tenant/game/revision 或 engine")
manifest = build_source_archive(
args.root, args.tenant_id, args.game_id, args.revision_id,
engine=args.engine, exclude_paths=args.exclude_path,
runtime_manifest_path=args.runtime_manifest,
)
payload = json.dumps(manifest, ensure_ascii=False, indent=2) + "\n"
if args.manifest_out:
args.manifest_out.parent.mkdir(parents=True, exist_ok=True)
args.manifest_out.write_text(payload, encoding="utf-8")
print(json.dumps({
"tenantId": manifest["tenantId"],
"gameId": manifest["gameId"],
"revisionId": manifest["revisionId"],
"objects": manifest["totals"]["objects"],
"bytes": manifest["totals"]["bytes"],
"sourceHash": manifest["sourceHash"],
"manifestHash": manifest["manifestHash"],
"upload": args.upload,
}, ensure_ascii=False))
if args.upload:
if not args.external_single_writer:
raise ArtifactError("波次 1 上传必须显式确认 --external-single-writer")
result = upload_source_archive(
args.root, manifest, _config(), external_single_writer=True,
)
print(json.dumps(result, ensure_ascii=False))
return 0
except ArtifactError as exc:
print(f"GAME-SOURCE-ARCHIVE-ERROR:{exc}")
return 2
if __name__ == "__main__":
raise SystemExit(main())