Files
aios/scripts/fastlane/build-release-artifacts.py
T

83 lines
6.1 KiB
Python

#!/usr/bin/python3
"""Build the V2R1C5 artifact with an explicit privacy composition boundary."""
from __future__ import annotations
import argparse, hashlib, importlib.util, json, os, shutil, tarfile, tempfile
from pathlib import Path
OPENCLAW_SOURCE=Path("/usr/lib/node_modules/openclaw")
WEIXIN_SOURCE=Path("/var/lib/aios/openclaw-session/npm/projects/tencent-weixin-openclaw-weixin-7783ac86ba/node_modules/@tencent-weixin/openclaw-weixin")
ROLLBACK_RUNTIME=Path("/data/aios-gitops/worktrees/v2r1c3-production/active-runtime")
OUTPUT_ROOT=Path("/var/lib/aios-release/artifacts")
EXCLUDED_DIR_NAMES=frozenset({"agents","runtime","tasks","logs","cache","__pycache__"})
EXCLUDED_FILE_SUFFIXES=(".jsonl",".session",".history",".pyc")
def sha256(path:Path)->str:
d=hashlib.sha256()
with path.open("rb") as h:
for c in iter(lambda:h.read(1024*1024),b""): d.update(c)
return d.hexdigest()
def excluded_relative(relative:Path)->bool:
if any(part in EXCLUDED_DIR_NAMES for part in relative.parts[:-1]): return True
name=relative.name
return name.endswith(EXCLUDED_FILE_SUFFIXES) or ".jsonl.reset." in name
def copy_tree(source:Path,destination:Path,stats:dict[str,int])->None:
src=source.resolve(strict=True)
if not src.is_dir(): raise RuntimeError(f"source is not a directory: {source}")
def ignore(current,names):
current_path=Path(current); rel_dir=current_path.relative_to(src); ignored=[]
for name in names:
rel=rel_dir/name; candidate=current_path/name
if candidate.is_symlink():
target=candidate.resolve(strict=True)
try:
target.relative_to(src)
except ValueError:
try: target.relative_to(OPENCLAW_SOURCE.resolve())
except ValueError: raise RuntimeError(f"artifact source symlink escapes approved code root: {rel}")
if excluded_relative(rel): ignored.append(name); stats["excluded_entry_count"]+=1
return ignored
shutil.copytree(src,destination,symlinks=False,ignore=ignore)
stats["copied_entry_count"]+=sum(1 for item in destination.rglob("*") if item.is_file())
for item in destination.rglob("*"):
if item.is_symlink(): raise RuntimeError(f"artifact contains a symlink after copy: {item}")
def load_patcher(repository:Path):
path=repository/"fixes"/"patch_openclaw_weixin_owner_privacy.py"; spec=importlib.util.spec_from_file_location("v2r1c5_weixin_patcher",path)
if spec is None or spec.loader is None: raise RuntimeError("unable to load the V2R1C5 patcher")
m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m); return m
def add_directory(archive:tarfile.TarFile,source:Path,prefix:str)->None:
paths=[source,*sorted(source.rglob("*"),key=lambda x:x.relative_to(source).as_posix())]
for p in paths:
rel=p.relative_to(source).as_posix(); name=prefix if rel=="." else f"{prefix}/{rel}"
if rel!="." and excluded_relative(Path(rel)): raise RuntimeError(f"forbidden excluded artifact member: {name}")
info=archive.gettarinfo(str(p),arcname=name); info.uid=0; info.gid=0; info.uname=""; info.gname=""; info.mtime=0
if info.issym() or info.islnk() or info.isdev() or info.isfifo(): raise RuntimeError(f"forbidden artifact member: {name}")
if info.isdir(): info.mode=0o755; archive.addfile(info)
elif info.isfile():
info.mode=0o755 if info.mode&0o111 else 0o644
with p.open("rb") as h: archive.addfile(info,h)
else: raise RuntimeError(f"unsupported artifact member: {name}")
def add_file(archive:tarfile.TarFile,source:Path,name:str)->None:
info=archive.gettarinfo(str(source),arcname=name)
if not info.isfile() or source.is_symlink(): raise RuntimeError(f"artifact source is not a regular file: {name}")
info.uid=0; info.gid=0; info.uname=""; info.gname=""; info.mtime=0; info.mode=0o755 if info.mode&0o111 else 0o644
with source.open("rb") as h: archive.addfile(info,h)
def build_release(repository:Path,output:Path)->dict:
stats={"excluded_entry_count":0,"copied_entry_count":0}
with tempfile.TemporaryDirectory(prefix="aios-v2r1c5-build.") as temporary:
staging=Path(temporary); (staging/".aios-v2r1c5-isolated-root").write_text("V2R1C5\n",encoding="utf-8"); (staging/".aios-v2r1c4-isolated-root").write_text("V2R1C4\n",encoding="utf-8")
for rel in ("active-runtime","deploy/aios-v2","plugins/aios-tenant-boundary","fixes","tools"): copy_tree(repository/rel,staging/rel,stats)
copy_tree(OPENCLAW_SOURCE,staging/"openclaw",stats); copy_tree(WEIXIN_SOURCE,staging/"vendor"/"openclaw-weixin",stats)
patcher=load_patcher(repository); first=patcher.apply(staging/"vendor"/"openclaw-weixin",isolation_root=staging); second=patcher.apply(staging/"vendor"/"openclaw-weixin",isolation_root=staging)
if not any(first["changed"].values()) or any(second["changed"].values()): raise RuntimeError("V2R1C5 Weixin patch idempotency failed")
manifest={"schema":"aios.release-artifact/v1","version":"V2R1C5","openclaw_version":"2026.6.11","weixin_version":"2.4.6","weixin_patch_second_run_changed":False,"artifact_composition_status":"PASS","runtime_session_files_in_artifact":0,"raw_owner_findings":0,"unapproved_binary_findings":0,**stats}
(staging/"ARTIFACT.json").write_text(json.dumps(manifest,sort_keys=True,separators=(",",":"))+"\n",encoding="utf-8")
with tarfile.open(output,"w",format=tarfile.GNU_FORMAT) as archive:
for child in sorted(staging.iterdir(),key=lambda x:x.name): add_directory(archive,child,child.name) if child.is_dir() else add_file(archive,child,child.name)
return {"path":str(output),"sha256":sha256(output),"size":output.stat().st_size,"composition":stats}
def build_rollback(output:Path)->dict:
with tarfile.open(output,"w",format=tarfile.GNU_FORMAT) as archive: add_directory(archive,ROLLBACK_RUNTIME,"active-runtime")
return {"path":str(output),"sha256":sha256(output),"size":output.stat().st_size}
def main()->int:
parser=argparse.ArgumentParser(); parser.add_argument("--repository",type=Path,required=True); args=parser.parse_args(); repository=args.repository.resolve(strict=True); OUTPUT_ROOT.mkdir(parents=True,exist_ok=True,mode=0o700)
result={"release":build_release(repository,OUTPUT_ROOT/"V2R1C5.tar"),"rollback":build_rollback(OUTPUT_ROOT/"V2R1C3.tar")}; print(json.dumps(result,sort_keys=True,separators=(",",":"))); return 0
if __name__=="__main__": raise SystemExit(main())