games-development-ai/tier2/gen-worker/scripts/game_source_archive_sync.py
lili 17727d08c3
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(storage): 接入游戏内容对象存储与运行时提交门
冻结 GameArtifactManifest/GameSourceArchive 契约和 V34 pending/committed 状态记录,增加专用 writer 的全量回读物化校验,并让 Runtime API 在消费和发布前对账 committed、GamePackage checksum 与实际 engineBundle hash。隔离 staging 已验证真实消费拒绝未提交版本;源码桶权限仍保持 fail-closed,正式金标不切换。
2026-07-29 12:02:22 -07:00

134 lines
6.3 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_artifact_minio 配置,禁止回落到 MinIO root。"""
try:
from service import infra_config
section = "game_artifact_minio"
config = {
"endpoint": os.getenv("GAME_ARTIFACT_ENDPOINT", infra_config.get(section, "endpoint", "")),
"access_key": os.getenv("GAME_ARTIFACT_ACCESS_KEY", infra_config.get(section, "access_key", "")),
"secret_key": os.getenv("GAME_ARTIFACT_SECRET_KEY", infra_config.get(section, "secret_key", "")),
"secure": os.getenv("GAME_ARTIFACT_SECURE", str(infra_config.get(section, "secure", True))).lower()
in {"1", "true", "yes"},
"artifact_bucket": os.getenv("GAME_ARTIFACT_BUCKET", infra_config.get(section, "artifact_bucket", "")),
"evidence_bucket": os.getenv("GAME_EVIDENCE_BUCKET", infra_config.get(section, "evidence_bucket", "")),
"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_artifact_minio 配置未就位,拒绝复用现有 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())