feat(deploy): build worker shadow artifact

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 15:09:34 +03:00
parent c9242480c2
commit 720487379c
3 changed files with 458 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""Build the deterministic, data-only Worker 006 M4 detector shadow artifact."""
from __future__ import annotations
import argparse
import gzip
import hashlib
import io
import json
import os
import re
import subprocess
import tarfile
import tempfile
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
BASELINE = REPOSITORY_ROOT / "config/perception/m4-recorded-realtime-baseline-v1.json"
DESCRIPTOR_TEMPLATE = (
REPOSITORY_ROOT / "config/deployment/mission-core-worker-shadow-v1.template.json"
)
WHEEL_NAME = "nodedc_mission_core-0.1.0-py3-none-any.whl"
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
EXPECTED_BASELINE_SHA256 = "ea10359339e6cce31b5780a2710299771cab7cc0c1c2a2b56a1621f786b31fa8"
EXPECTED_WHEEL_SHA256 = "df756938f2c212fb6d1770c83569e5366c708d70d74e0434895a21be55a16102"
PAYLOAD_FILES = (
WHEEL_NAME,
"m4-recorded-realtime-baseline-v1.json",
"mission-core-worker-shadow-v1.json",
)
class ArtifactBuildError(RuntimeError):
pass
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def git_revision() -> str:
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=REPOSITORY_ROOT,
check=True,
capture_output=True,
text=True,
)
revision = result.stdout.strip()
if re.fullmatch(r"[a-f0-9]{40}", revision) is None:
raise ArtifactBuildError("Git revision is not a full SHA-1")
return revision
def build_wheel(output: Path) -> Path:
environment = os.environ.copy()
environment["SOURCE_DATE_EPOCH"] = "0"
result = subprocess.run(
["uv", "build", "--wheel", "--out-dir", str(output)],
cwd=REPOSITORY_ROOT,
env=environment,
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip()
raise ArtifactBuildError(f"wheel build failed: {detail}")
wheel = output / WHEEL_NAME
if not wheel.is_file() or wheel.is_symlink():
raise ArtifactBuildError("expected wheel was not built")
if sha256_file(wheel) != EXPECTED_WHEEL_SHA256:
raise ArtifactBuildError("runtime wheel digest changed")
return wheel
def render_descriptor(patch_id: str, revision: str) -> bytes:
template = DESCRIPTOR_TEMPLATE.read_text("utf-8")
if template.count("__PATCH_ID__") != 1 or template.count("__CODE_REVISION__") != 1:
raise ArtifactBuildError("descriptor template placeholders changed")
rendered = template.replace("__PATCH_ID__", patch_id).replace("__CODE_REVISION__", revision)
document = json.loads(rendered)
if document["patch_id"] != patch_id or document["code_revision"] != revision:
raise ArtifactBuildError("descriptor identity rendering failed")
return (json.dumps(document, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode(
"utf-8"
)
def _tar_info(path: Path, arcname: str) -> tarfile.TarInfo:
info = tarfile.TarInfo(arcname)
info.uid = 0
info.gid = 0
info.uname = "root"
info.gname = "root"
info.mtime = 0
if path.is_dir():
info.type = tarfile.DIRTYPE
info.mode = 0o755
else:
info.type = tarfile.REGTYPE
info.mode = 0o644
info.size = path.stat().st_size
return info
def write_canonical_archive(stage: Path, target: Path) -> None:
members = [stage / "manifest.env", stage / "files.txt", stage / "payload"]
members.extend(sorted((stage / "payload").rglob("*")))
target.parent.mkdir(parents=True, exist_ok=True)
with (
target.open("wb") as raw,
gzip.GzipFile(filename="", mode="wb", fileobj=raw, compresslevel=9, mtime=0) as gz,
tarfile.open(fileobj=gz, mode="w", format=tarfile.PAX_FORMAT) as archive,
):
for path in members:
arcname = path.relative_to(stage).as_posix()
info = _tar_info(path, arcname)
if path.is_file():
with path.open("rb") as source:
archive.addfile(info, source)
else:
archive.addfile(info, io.BytesIO())
def build_artifact(patch_id: str, output_directory: Path) -> dict[str, object]:
if PATCH_ID.fullmatch(patch_id) is None:
raise ArtifactBuildError("patch id is invalid")
if sha256_file(BASELINE) != EXPECTED_BASELINE_SHA256:
raise ArtifactBuildError("baseline digest changed")
revision = git_revision()
with tempfile.TemporaryDirectory(prefix="mission-core-worker-shadow-") as directory:
stage = Path(directory)
payload = stage / "payload"
payload.mkdir()
wheel = build_wheel(stage / "wheel")
(payload / WHEEL_NAME).write_bytes(wheel.read_bytes())
(payload / "m4-recorded-realtime-baseline-v1.json").write_bytes(BASELINE.read_bytes())
descriptor_bytes = render_descriptor(patch_id, revision)
(payload / "mission-core-worker-shadow-v1.json").write_bytes(descriptor_bytes)
(stage / "manifest.env").write_text(
f"id={patch_id}\ncomponent=mission-core-worker\ntype=shadow-release\n",
"utf-8",
)
(stage / "files.txt").write_text("\n".join(PAYLOAD_FILES) + "\n", "utf-8")
target = output_directory.resolve() / f"nodedc-mission-core-worker-{patch_id}.tgz"
write_canonical_archive(stage, target)
return {
"ok": True,
"patch_id": patch_id,
"component": "mission-core-worker",
"type": "shadow-release",
"artifact": str(target),
"sha256": sha256_file(target),
"code_revision": revision,
"wheel_sha256": EXPECTED_WHEEL_SHA256,
"baseline_sha256": EXPECTED_BASELINE_SHA256,
"payload_files": list(PAYLOAD_FILES),
"transition": "m4-detector-shadow-v1",
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("patch_id")
parser.add_argument(
"--output-directory",
type=Path,
default=REPOSITORY_ROOT / ".runtime/deploy-artifacts",
)
arguments = parser.parse_args()
try:
result = build_artifact(arguments.patch_id, arguments.output_directory)
except (ArtifactBuildError, OSError, subprocess.SubprocessError, json.JSONDecodeError) as exc:
parser.error(str(exc))
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())