feat: add cascade determinism checker#4
Conversation
Compares the FROM-graph (Containerfile inheritance across crunchtools repos) with the dispatch-graph (repository_dispatch wiring in build.yml). FAILS when any direct FROM edge is missing a matching dispatch edge — that's the bug that lets downstream images stop rebuilding when their parent updates. Catches future drift between the two sources of truth. Broken FROMs (e.g. acquacotta-base missing from the org) are demoted to WARN: they break one specific repo's build but don't impair cascade correctness for the rest of the org. Over-dispatch (a repo dispatched without a matching FROM) is also WARN — usually intentional (e.g. rotv uses BASE_IMAGE ARG that resolves to ubi10-core, so the ubi10-core->rotv dispatch is correct in practice even though the FROM line is parameterized). Runs on push/PR to constitution and via workflow_dispatch from Hermes weekly. No schedule: trigger by design — avoids disabled_inactivity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces validate-cascade.py, a script that validates the determinism of the crunchtools image-rebuild cascade by comparing the FROM-graph of container images with the dispatch-graph of GitHub Actions workflows. The review comments suggest three valuable improvements: enhancing the regular expression for dispatch loops to support newlines as well as semicolons, safely checking the API response structure in fetch_text to prevent crashes on directories or missing keys, and catching network or JSON parsing exceptions at the entry point to guarantee the script exits with the documented code of 2.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| DISPATCH_LOOP_RE = re.compile( | ||
| r"for\s+repo\s+in\s+([A-Za-z0-9._\- ]+?)\s*;\s*do", | ||
| ) |
There was a problem hiding this comment.
The current regular expression for matching dispatch loops expects a semicolon before the do keyword. In bash/sh, it is very common to write loops with a newline instead of a semicolon (e.g., for repo in foo bar\ndo). Changing the regex to support both semicolons and newlines makes the parser more robust against formatting variations.
| DISPATCH_LOOP_RE = re.compile( | |
| r"for\s+repo\s+in\s+([A-Za-z0-9._\- ]+?)\s*;\s*do", | |
| ) | |
| DISPATCH_LOOP_RE = re.compile( | |
| r"for\s+repo\s+in\s+([A-Za-z0-9._\- ]+?)\s*(?:;|[\r\n]+)\s*do", | |
| ) |
| import base64 | ||
| return base64.b64decode(data["content"]).decode("utf-8", errors="replace") |
There was a problem hiding this comment.
If the specified path is a directory rather than a file, the GitHub API returns a list of items instead of a dictionary. Accessing data["content"] directly will raise a TypeError. Additionally, if the file is a symlink or submodule, the "content" key might be missing, raising a KeyError. We should safely check that data is a dictionary and contains the "content" key before decoding.
| import base64 | |
| return base64.b64decode(data["content"]).decode("utf-8", errors="replace") | |
| if isinstance(data, dict) and "content" in data: | |
| import base64 | |
| return base64.b64decode(data["content"]).decode("utf-8", errors="replace") | |
| return None |
| if __name__ == "__main__": | ||
| sys.exit(main()) |
There was a problem hiding this comment.
The script documentation states that exit code 2 is returned on network, auth, or API errors. However, if any unhandled urllib.error.URLError or json.JSONDecodeError is raised during the API calls, the script will crash with a traceback and exit with code 1. Wrapping the main execution block to catch these exceptions ensures the documented exit code contract is respected.
| if __name__ == "__main__": | |
| sys.exit(main()) | |
| if __name__ == "__main__": | |
| try: | |
| sys.exit(main()) | |
| except (urllib.error.URLError, json.JSONDecodeError) as e: | |
| print(f"ERROR: GitHub API communication failed: {e}", file=sys.stderr) | |
| sys.exit(2) |
Adds
validate-cascade.pyplus a GHA workflow that runs it.The checker diffs the FROM-graph (Containerfile inheritance across crunchtools repos) against the dispatch-graph (
repository_dispatchwiring inbuild.yml). FAILS when a direct FROM edge is missing a matching dispatch edge — that's the bug that lets downstream images stop rebuilding when their parent updates. Catches future drift.Currently passes on the live org state. Two WARNs (both non-blocking, documented in script):
acquacottahasFROM quay.io/crunchtools/acquacotta-basebut the repo is missing from the org. Acquacotta's build is broken until this is resolved (restore the repo, or rebase acquacotta on another base).ubi10-coredispatchesrotveven though rotv's FROM is${BASE_IMAGE}(parameterized, doesn't resolve to ubi10-core literally). Intentional over-dispatch — rotv's effective base often is ubi10-core.Workflow runs on push/PR and
workflow_dispatch. Deliberately noschedule:trigger — avoidsdisabled_inactivity(the failure mode this whole rework was driven by).🤖 Generated with Claude Code