From 0fc6da789495df5a6c2a0a68b57c1a898ad04f58 Mon Sep 17 00:00:00 2001 From: Zain Dana Harper <17142659+HarperZ9@users.noreply.github.com> Date: Sun, 4 Oct 2026 05:56:31 -0700 Subject: [PATCH] ci: verify docs/art/receipts.json digests Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 6 +++++ scripts/check_art_receipts.py | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 scripts/check_art_receipts.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc7d639..09a523f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,3 +42,9 @@ jobs: /tmp/vc/bin/pip install dist/*.whl /tmp/vc/bin/plexus --version /tmp/vc/bin/plexus discover --builtin > /dev/null + art-receipts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Verify art receipt digests + run: python3 scripts/check_art_receipts.py diff --git a/scripts/check_art_receipts.py b/scripts/check_art_receipts.py new file mode 100644 index 0000000..64f5659 --- /dev/null +++ b/scripts/check_art_receipts.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Verify docs/art/receipts.json: each listed file's SHA-256 must match its stored bytes. + +Usage: python scripts/check_art_receipts.py [receipts.json] +Exit 1 on any digest mismatch, malformed receipt, or if no listed file could be checked. +A listed file that is absent from the checkout is reported and skipped: receipts also +list renders (PNGs, extra SVGs) that are never committed. Checking is on committed bytes +only, so it shows the bytes match the receipt, not that the scene produced them. +""" +import hashlib +import json +import sys +from pathlib import Path + + +def main(): + path = Path(sys.argv[1] if len(sys.argv) > 1 else "docs/art/receipts.json") + try: + data = json.loads(path.read_text(encoding="utf-8")) + want = {} + for rec in data["receipts"].values(): + for name, digest in rec["outputs"].items(): + if want.setdefault(name, digest) != digest: + print(f"FAIL {name}: receipts disagree on its digest") + return 1 + except (OSError, ValueError, KeyError, AttributeError, TypeError) as exc: + print(f"FAIL cannot read {path}: {exc!r}") + return 1 + ok, bad, skipped = 0, 0, [] + for name, digest in sorted(want.items()): + f = path.parent / name + if not f.is_file(): + skipped.append(name) + continue + got = hashlib.sha256(f.read_bytes()).hexdigest() + if got == digest: + ok += 1 + else: + bad += 1 + print(f"FAIL {name}: receipt {digest} stored bytes {got}") + print(f"art receipts: {ok} match, {bad} mismatch, {len(skipped)} not committed") + if ok == 0 and not bad: + print("FAIL no listed file was checked") + return 1 + return 1 if bad else 0 + + +if __name__ == "__main__": + sys.exit(main())