#!/usr/bin/env python3 import json import pathlib import subprocess import sys from collections import Counter ROOT = pathlib.Path(__file__).resolve().parents[1] FINAL_PACK = ROOT / "packs" / "NOT-EDIT-final-merged-packs" PACK_JSON = FINAL_PACK / "pack.json" LINEAGE_JSON = FINAL_PACK / ".build-lineage.json" NON_BLOCKING_METADATA_PATHS = [ "packs/NOT-EDIT-final-merged-packs/.build-lineage.json", ] def final_pack_diff_args(): args = ["packs/NOT-EDIT-final-merged-packs"] args.extend(f":(exclude){path}" for path in NON_BLOCKING_METADATA_PATHS) return args def run_git_diff(): cmd = [ "git", "diff", "--name-only", "--exit-code", "--", *final_pack_diff_args(), ] proc = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True, check=False) files = [line.strip() for line in proc.stdout.splitlines() if line.strip()] return proc.returncode, files def dump_full_diff_for_debug(): """Print the actual diff content for any drifting file (excluding lineage). Bounded at a few hundred lines per file so the CI log stays readable. """ diff_cmd = [ "git", "diff", "--", *final_pack_diff_args(), ] proc = subprocess.run(diff_cmd, cwd=ROOT, capture_output=True, text=True, check=False) print("---BEGIN diff content for drifting files---", file=sys.stderr) lines = proc.stdout.splitlines() if not lines: print("(git diff produced no textual output; binary or empty)", file=sys.stderr) else: max_lines = 400 for line in lines[:max_lines]: print(line, file=sys.stderr) if len(lines) > max_lines: print(f"... ({len(lines) - max_lines} more lines suppressed)", file=sys.stderr) print("---END diff content---", file=sys.stderr) def load_json(path: pathlib.Path): return json.loads(path.read_text()) def component_identity(component): return ( component.get("kind"), component.get("businessKey"), component.get("path"), json.dumps(component.get("dependsOn", []), sort_keys=True, separators=(",", ":")), ) def classify_pack_json_diff(): head_components = load_json(PACK_JSON).get("components", []) worktree_components = load_json(PACK_JSON).get("components", []) if not head_components and not worktree_components: return "empty", "pack.json has no components" return "unknown", "pack.json changed" def load_head_pack_json(): proc = subprocess.run( ["git", "show", f"HEAD:{PACK_JSON.relative_to(ROOT).as_posix()}"], cwd=ROOT, capture_output=True, text=True, check=False, ) if proc.returncode != 0: raise RuntimeError(proc.stderr.strip() or "failed to load HEAD pack.json") return json.loads(proc.stdout) def summarize_pack_json_diff(): head_payload = load_head_pack_json() worktree_payload = load_json(PACK_JSON) head_components = head_payload.get("components", []) worktree_components = worktree_payload.get("components", []) head_ids = [component_identity(component) for component in head_components] worktree_ids = [component_identity(component) for component in worktree_components] if Counter(head_ids) == Counter(worktree_ids): return [ "pack.json drift classification: order-only", "The rebuilt final pack has the same component multiset as the committed snapshot.", "Action: accept the rebuilt pack snapshot and commit packs/NOT-EDIT-final-merged-packs/pack.json.", ] head_only = sorted(set(head_ids) - set(worktree_ids)) worktree_only = sorted(set(worktree_ids) - set(head_ids)) lines = ["pack.json drift classification: semantic"] if head_only: lines.append("Committed-only components:") for kind, business_key, path, _depends_on in head_only[:20]: lines.append(f" - {kind}:{business_key} ({path})") if worktree_only: lines.append("Rebuilt-only components:") for kind, business_key, path, _depends_on in worktree_only[:20]: lines.append(f" - {kind}:{business_key} ({path})") lines.append("Action: inspect build inputs / assembly logic before accepting the snapshot.") return lines def main(): returncode, files = run_git_diff() if returncode == 0 and not files: print("final pack snapshot verification PASSED") return if returncode not in (0, 1): print("final pack snapshot verification FAILED", file=sys.stderr) print("git diff failed while checking final pack snapshot", file=sys.stderr) sys.exit(returncode) print("final pack snapshot verification FAILED", file=sys.stderr) print("Changed files under packs/NOT-EDIT-final-merged-packs:", file=sys.stderr) for path in files: print(f"- {path}", file=sys.stderr) dump_full_diff_for_debug() if PACK_JSON.relative_to(ROOT).as_posix() in files: for line in summarize_pack_json_diff(): print(line, file=sys.stderr) print( "Recommended local repro:\n" "1. bash scripts/build_pingcap_salesforce_sample_pack.sh --reset\n" "2. python3 scripts/audit_final_pack_metadata.py\n" "3. python3 scripts/verify_final_pack_snapshot.py", file=sys.stderr, ) sys.exit(1) if __name__ == "__main__": main()