STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
registry_merge.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Merge freshly built firmware records into the registry tree. Used by CI.
3
4 registry_merge.py --records <dir of *.json> --registry <registry tree>
5
6A record's file name is its digest, so two commits that build byte-identical
7firmware land on the same file. That is deliberate: a board running those bytes
8genuinely cannot be attributed to one commit rather than the other, so the
9record keeps a list of every commit known to produce it instead of pretending
10the first one is the answer.
11"""
12
13from __future__ import annotations
14
15import argparse
16import json
17import sys
18from pathlib import Path
19
20
21def merge_one(new: dict, existing: dict | None) -> tuple[dict, str]:
22 if existing is None:
23 rec = dict(new)
24 rec["commits"] = sorted(set(new.get("commits") or []))
25 return rec, "new"
26
27 rec = dict(existing)
28 before = set(rec.get("commits") or [])
29 after = before | set(new.get("commits") or [])
30 rec["commits"] = sorted(after)
31
32 # Fields that describe the image itself must never drift for a given digest.
33 for key in ("image_digest", "file_sha256", "image_len", "board"):
34 if key in new and key in rec and new[key] != rec[key]:
35 raise SystemExit(
36 f"refusing to merge: {key} differs for digest {new.get('image_digest')}\n"
37 f" existing: {rec[key]!r}\n new: {new[key]!r}\n"
38 "Identical bytes cannot have two different values here; the "
39 "registry or the build is wrong."
40 )
41 # Keep the first sighting's provenance, add the latest.
42 for key in ("run_url", "run_id", "subject"):
43 if new.get(key):
44 rec.setdefault(f"first_{key}", rec.get(key, new[key]))
45 rec[key] = new[key]
46 return rec, "updated" if after != before else "unchanged"
47
48
49def main() -> int:
50 ap = argparse.ArgumentParser()
51 ap.add_argument("--records", required=True, help="dir of new record JSON files")
52 ap.add_argument("--registry", required=True, help="registry tree to merge into")
53 args = ap.parse_args()
54
55 records = sorted(Path(args.records).rglob("*.json"))
56 if not records:
57 print("no records to merge", file=sys.stderr)
58 return 0
59
60 registry = Path(args.registry)
61 counts = {"new": 0, "updated": 0, "unchanged": 0}
62
63 for path in records:
64 new = json.loads(path.read_text())
65 digest = new.get("image_digest")
66 board = new.get("board")
67 if not digest or not board:
68 raise SystemExit(f"{path}: record needs image_digest and board")
69
70 dest = registry / board / f"{digest}.json"
71 existing = json.loads(dest.read_text()) if dest.is_file() else None
72 merged, what = merge_one(new, existing)
73 counts[what] += 1
74 if what != "unchanged":
75 dest.parent.mkdir(parents=True, exist_ok=True)
76 dest.write_text(json.dumps(merged, indent=2, sort_keys=True) + "\n")
77 print(f"{what:9} {board}/{digest[:16]}…")
78
79 print(
80 f"\n{counts['new']} new, {counts['updated']} updated, "
81 f"{counts['unchanged']} already current"
82 )
83 return 0
84
85
86if __name__ == "__main__":
87 sys.exit(main())
tuple[dict, str] merge_one(dict new, dict|None existing)
int main()