"""Mechanism distribution and drift detection. Gitea 1.25.5 cannot read a reusable workflow out of a private repository (that is PR #32562, milestone 1.26.0), so the invariant scripts are copied into consuming repositories. Copying is only safe if drift is detectable, so every copy is pinned three ways: file hashes against a manifest, the manifest against its own digest, and that digest against the policy record, which the consuming repository cannot write. """ import hashlib import json import os import shutil PIN_FILENAME = ".publication-mechanism.json" def canonical_json(obj): return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode("utf-8") def sha256_file(path): h = hashlib.sha256() with open(path, "rb") as fh: for chunk in iter(lambda: fh.read(65536), b""): h.update(chunk) return h.hexdigest() def sha256_bytes(data): return hashlib.sha256(data).hexdigest() def compute_manifest(root, relpaths, version): files = {} for rel in sorted(relpaths): path = os.path.join(root, rel) if not os.path.isfile(path): raise FileNotFoundError(f"mechanism: {rel} not found under {root}") files[rel] = sha256_file(path) return {"mechanism_version": version, "files": files} def manifest_digest(manifest): return sha256_bytes(canonical_json(manifest)) def write_manifest(path, manifest): with open(path, "w", encoding="utf-8") as fh: fh.write(json.dumps(manifest, sort_keys=True, indent=2) + "\n") def read_manifest(path): with open(path, "r", encoding="utf-8") as fh: return json.load(fh) def install(source_root, target_root, manifest, pin_dir=None): """Copy the manifest's files into target_root and write the pin file.""" for rel in sorted(manifest["files"]): src = os.path.join(source_root, rel) dst = os.path.join(target_root, rel) os.makedirs(os.path.dirname(dst), exist_ok=True) shutil.copyfile(src, dst) os.chmod(dst, 0o755 if rel.endswith(".sh") or "/bin/" in rel else 0o644) pin = { "mechanism_version": manifest["mechanism_version"], "manifest_sha256": manifest_digest(manifest), } pin_path = os.path.join(pin_dir or target_root, PIN_FILENAME) with open(pin_path, "w", encoding="utf-8") as fh: fh.write(json.dumps(pin, sort_keys=True, indent=2) + "\n") return pin_path def verify(target_root, manifest, policy_manifest_sha256, policy_mechanism_version): """Return failure reasons. Empty means the local copy is the pinned mechanism.""" reasons = [] digest = manifest_digest(manifest) if digest != policy_manifest_sha256: reasons.append( "mechanism: canonical manifest digest does not match the digest pinned in " "policy; the mechanism was changed without a policy change") if manifest["mechanism_version"] != policy_mechanism_version: reasons.append( f"mechanism: manifest version {manifest['mechanism_version']} does not match " f"policy version {policy_mechanism_version}") pin_path = os.path.join(target_root, PIN_FILENAME) if not os.path.isfile(pin_path): reasons.append(f"mechanism: {PIN_FILENAME} is absent from {target_root}") else: try: with open(pin_path, "r", encoding="utf-8") as fh: pin = json.load(fh) except json.JSONDecodeError as exc: reasons.append(f"mechanism: {PIN_FILENAME} is not valid JSON ({exc})") pin = None if pin is not None: if pin.get("manifest_sha256") != policy_manifest_sha256: reasons.append( "mechanism: in-repo pin disagrees with policy; a repository cannot " "bless its own mechanism version") if pin.get("mechanism_version") != policy_mechanism_version: reasons.append("mechanism: in-repo pin version disagrees with policy") for rel, expected in sorted(manifest["files"].items()): path = os.path.join(target_root, rel) if not os.path.isfile(path): reasons.append(f"mechanism: {rel} is missing from the consuming repository") continue actual = sha256_file(path) if actual != expected: reasons.append(f"mechanism: {rel} has drifted from the canonical copy") return reasons